天天看點

動畫特效十三:自定義過度動畫之基本使用

本人錄制技術視訊位址:https://edu.csdn.net/lecturer/1899 歡迎觀看。

好久沒有進行動畫系列了,今天我們繼續;這次講解的内容是過度動畫;先看看最終動畫效果。

動畫特效十三:自定義過度動畫之基本使用

需求分析:

1. UI下方有一排内容,用來顯示縮略圖,可以橫向滾動;

2. 選中某一項後,會彈出具體的内容。

操作細節:

1. 動畫在縮略圖的情形下,隻有圖檔沒有文本描述資訊。

2. 動畫在執行過程中,圖檔的圓角也會伴随着動畫的變化而變化。

實作思路:

1. 點選每個縮略圖的時候,建立一個相同的View(帶有文本描述資訊),動畫執行開始時,隐藏掉被點選的縮略圖,然後動畫執行建立的View;執行的是縮放動畫,并且在動畫執行的時候,注意圓角的處理。當點選具體内容的View的時候,縮小自定義的View,并且在動畫執行完畢的時候,移除掉這個自定義的View,并且顯示剛才被點選的縮略圖。

2. 使用UIViewControllerAnimatedTransitioning動畫,注意,此時不是通過自定義View的形式來實作的了。而是帶縮略圖的界面是一個控制器,彈出後的詳情界面是另一個控制器。而我們隻需要通過過度動畫來完成兩個界面之間的切換。我選擇這個實作方式,主要是講解過度動畫的實作原理。

代碼實作:

一. 建構基本UI界面,主界面是一個ViewController,下面是一個UIScrollView,用來存放縮略圖(也可以直接使用UICollectionView)。界面如下:

動畫特效十三:自定義過度動畫之基本使用

二. 詳情頁(HerbDetailsViewController)界面如下:

動畫特效十三:自定義過度動畫之基本使用

三. 主界面實作基本代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupList];
}

- (void)setupList {
    for (NSInteger i = 0; i < self.mockupData.count; i++) {
        Herb *herb = self.mockupData[i];
        UIImageView *imageView = [[UIImageView alloc] init];
        imageView.image = [UIImage imageNamed:herb.image];
        imageView.tag = i + kBaseTag;
        imageView.contentMode = UIViewContentModeScaleAspectFill;
        imageView.userInteractionEnabled = YES;
        imageView.layer.cornerRadius = 20.0;
        imageView.layer.masksToBounds = YES;
        
        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(clickImage:)];
        [imageView addGestureRecognizer:tap];
        
        [self.scrollView addSubview:imageView];
        [self positionListItems];
    }
}

- (void)positionListItems {
    CGFloat itemHeight = self.scrollView.frame.size.height * 1.33;
    CGFloat ratio = [UIScreen mainScreen].bounds.size.height / [UIScreen mainScreen].bounds.size.width;
    CGFloat itemWidth = itemHeight / ratio;
    
    CGFloat padding = 10.0;
    for (NSInteger i = 0; i < self.mockupData.count; i++) {
        UIImageView *imageView = (UIImageView *)[self.scrollView viewWithTag:i + kBaseTag];
        imageView.frame = CGRectMake(padding + (itemWidth + padding) * i, 0, itemWidth, itemHeight);
    }
    
    self.scrollView.contentSize = CGSizeMake(padding + (itemWidth + padding) * self.mockupData.count, 0);
}

- (void)clickImage:(UITapGestureRecognizer *)tap {
    self.selectedImageView = (UIImageView *)tap.view;
    
    NSInteger index = tap.view.tag - kBaseTag;
    Herb *herb = self.mockupData[index];
    HerbDetailsViewController *herbVC = [[HerbDetailsViewController alloc] init];
    herbVC.herb = herb;
    [self presentViewController:herbVC animated:YES completion:nil];
}
           

其中的mockupData是一個數組,存放的是Herb模型資料集合。而Herb模型的結構如下,其實就是主要擷取name和image資訊,用來展示:

@interface Herb : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *image;
@property (nonatomic, copy) NSString *license;
@property (nonatomic, copy) NSString *credit;
@property (nonatomic, copy) NSString *descriptions;
- (instancetype)initHerbWithName:(NSString *)name image:(NSString *)image license:(NSString *)license credit:(NSString *)credit descriptions:(NSString *)descriptions;
@end
           

注意clickImage方法執行的tap手勢操作,這裡是用presentViewController 的形式彈出HerbDetailsViewController的。此刻運作代碼,效果如下:

動畫特效十三:自定義過度動畫之基本使用

現在的效果就是系統預設的模态對話框的展現形式。但這并不是我們想要的效果,是以我們可以自定義過度動畫。讓ViewController 遵守UIViewControllerTransitioningDelegate協定,然後在clickImage方法中,添加如下代碼:

herbVC.transitioningDelegate = self;
           

然後實作兩個自定義模态動畫效果的代理方法:

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
           

這兩個方法對應的分别是彈出模态層和退出模态層。注意這兩個方法傳回的均是

UIViewControllerAnimatedTransitioning,是以我們可以自定義遵守這個協定的類,來完成自定的動畫效果來取代系統預設的這種動畫執行效果。

四. 自定義PopAnimator類

PopAnimator類隻是一個普通的繼承自NSObject的類,并且它遵守UIViewControllerAnimatedTransitioning協定。并且動畫都有自己的執行時間,是以PopAnimator類大緻定義如下:

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface PopAnimator : NSObject<UIViewControllerAnimatedTransitioning>
@property (nonatomic, assign) CGFloat duration;
@end
           

而UIViewControllerAnimatedTransitioning協定中有兩個方法:

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext;
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
           

1. 第一個方法就是動畫執行所需要的時間。

2. 第二個方法就是處理自定動畫效果的地方。

動畫特效十三:自定義過度動畫之基本使用

五. 簡單Demo示範

為了說明自定義過度動畫的執行流程,我們先實作兩個控制器之間的淡入淡出效果。

1. 在ViewController中執行個體化一個PopAnimator(懶加載),用來執行自定義的動畫。

- (PopAnimator *)animator {
    if (!_animator) {
        _animator = [[PopAnimator alloc] init];
    }
    return _animator;
}
           

2. ViewController實作UIViewControllerTransitioningDelegate的兩個方法,假設動畫時間為1秒,并且傳回的UIViewControllerAnimatedTransitioning對象均為 PopAnimator。

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    self.animator.duration = 1;
    return self.animator;
}

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    return self.animator;
}
           

3.  PopAnimator中實作UIViewControllerAnimatedTransitioning中的方法,完成淡入淡出動畫效果:

- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
    return self.duration;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    UIView *containerView = [transitionContext containerView];
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    [containerView addSubview:toView];
    
    toView.alpha = 0.0;
    [UIView animateWithDuration:self.duration animations:^{
        toView.alpha = 1.0;
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:YES];
    }]; 
}
           

transitionContext就是過度動畫執行的上下文,所有的動畫變換操作都是在這個上下文中完成的,通過這個上下文我們可以擷取很多的資訊。

1. containerView就是動畫執行的容器。

2. UITransitionContextToViewKey就是擷取将要呈現的View,同理UITransitionContextFromViewKey就是擷取目前正在界面上面呈現的View。

初始狀态的時候,View的結構圖如下:

動畫特效十三:自定義過度動畫之基本使用

即fromView是在container view中的,animateTransition方法執行後,toView立刻被加入到container view上面了,然後執行淡入淡出動畫,注意:在過度動畫中,當動畫執行完畢後,fromView會被自動從container view中移除。是以動畫執行完畢後,View的結構圖如下:

動畫特效十三:自定義過度動畫之基本使用

當你下次點選詳情頁面,執行動畫時,此時,需要注意的是,fromView永遠是在container view中的那一個!!!是以此時的fromView應該是詳情頁,而toView是首頁。這樣就可以完成動畫的無限切換效果了。

效果圖如下:

動畫特效十三:自定義過度動畫之基本使用

六、具體實作:

在明白了上面的實作思路之後,我們用自定義過度動畫來完成本節開頭中的動畫效果。

主要代碼清單:

1. PopAnimator.h:

@interface PopAnimator : NSObject<UIViewControllerAnimatedTransitioning>
@property (nonatomic, assign) CGFloat duration;
// 是彈出詳情控制器還是,還是傳回顯示主要制器
@property (nonatomic, assign, getter=isPresenting) BOOL presenting;
// 彈出縮略圖的其實frame大小
@property (nonatomic, assign) CGRect originFrame;
// dismiss 控制器之後,應該顯示scroll view上面的item項
@property (nonatomic, copy) void (^didFinishDismiss)();
@end
           

2. PopAnimator.m:

@implementation PopAnimator

- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
    return self.duration;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    UIView *containerView = [transitionContext containerView];
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    
    UIView *herbView = self.isPresenting ? toView : [transitionContext viewForKey:UITransitionContextFromViewKey];
    CGRect initFrame = self.isPresenting ? self.originFrame : herbView.frame;
    CGRect finalFrame = self.isPresenting ? herbView.frame : self.originFrame;
    
    CGFloat scaleX = self.isPresenting ? initFrame.size.width / finalFrame.size.width : finalFrame.size.width / initFrame.size.width;
    CGFloat scaleY = self.isPresenting ? initFrame.size.height / finalFrame.size.height : finalFrame.size.height / initFrame.size.height;
    
    CGAffineTransform scaleTransform = CGAffineTransformMakeScale(scaleX, scaleY);
    if (self.isPresenting) {
        herbView.transform = scaleTransform;
        herbView.center = CGPointMake(CGRectGetMidX(initFrame), CGRectGetMidY(initFrame));
        herbView.clipsToBounds = YES;
    }
    
    // 控制HerbDetailsViewController上面的Container View的顯示與隐藏
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    HerbDetailsViewController *herbVC = self.isPresenting ? (HerbDetailsViewController *)toVC : (HerbDetailsViewController *)fromVC;
    
    
    // alpha:0, 是透明的
    herbVC.herbContainerView.alpha = self.isPresenting ? 0.0 : 1.0;
    
    NSLog(@"Container subviews' count:%lu",(unsigned long)containerView.subviews.count);
    [containerView addSubview:toView];
    [containerView bringSubviewToFront:herbView];
    NSLog(@"Container subviews' count:%lu",(unsigned long)containerView.subviews.count);
    
    [UIView animateWithDuration:self.duration delay:0.0 usingSpringWithDamping:0.4 initialSpringVelocity:0.0 options:UIViewAnimationOptionCurveEaseIn animations:^{
        herbView.transform = self.isPresenting ? CGAffineTransformIdentity : scaleTransform;
        herbView.center = CGPointMake(CGRectGetMidX(finalFrame), CGRectGetMidY(finalFrame));
        herbVC.herbContainerView.alpha = self.isPresenting ? 1.0 : 0.0;
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:YES];
        NSLog(@"Container subviews' count:%lu",(unsigned long)containerView.subviews.count);
        if (_didFinishDismiss) {
            _didFinishDismiss();
        }
    }];
    
    // 動畫過程中保持圓角效果
    CABasicAnimation *basicAnimation = [CABasicAnimation animation];
    basicAnimation.keyPath = @"cornerRadius";
    basicAnimation.duration = self.duration;
    basicAnimation.fromValue = self.isPresenting ? @(20 / scaleX) : @0;
    basicAnimation.toValue = self.isPresenting ? @0 : @(20 / scaleX);
    [herbView.layer addAnimation:basicAnimation forKey:nil];
 
}
@end
           

3. ViewController 中建立PopAnimator對象

- (PopAnimator *)animator {
    if (!_animator) {
        _animator = [[PopAnimator alloc] init];
        __weak typeof(self) wSelf = self;
        _animator.didFinishDismiss = ^{
            wSelf.selectedImageView.hidden = NO;
        };
    }
    return _animator;
}
           

4. ViewController 實作的UIViewControllerTransitioningDelegate的協定的方法

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    self.animator.duration = 1;
    // convertRect 将selectedImageView所在的坐标轉換到self.view上面的坐标。
    self.animator.originFrame = [self.selectedImageView.superview convertRect:self.selectedImageView.frame toView:nil];
    self.animator.presenting = YES;
    self.selectedImageView.hidden = YES;
    return self.animator;
}

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    self.animator.presenting = NO;
    self.selectedImageView.hidden = YES;
    return self.animator;
}
           

至此,動畫效果大緻完成了,但是螢幕旋轉之後會出現圖檔錯位的問題,是以我們應該再處理螢幕旋轉事件,代碼如下:

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
    
    [coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        // 螢幕旋轉的時候,改變背景圖檔的透明度
        self.coverView.alpha = size.width > size.height ? 0.55 : 0.25;
        [self positionListItems];
    }];
}