iOS实战:动画实战-自定义转场动画实现

前言.png

前言

(呃呃呃,其实本文不算是动画实战,只是用到了一点动画,算了没差~)
在平时使用的app中,部分app的部分转场动画与传统的动画不一样,其实他们使用的是自定义转场动画。本文记录的是自定义转场动画的实现。

效果图

效果图.gif

主要思路

最重要的是需要创建一个继承NSObject的类,并且遵守UIViewControllerAnimatedTransitioning协议。我暂时给这个类命名为YQAnimatedTransition。这个协议就是用来自定义转场动画的。点进去看看:

@protocol UIViewControllerAnimatedTransitioning <NSObject>
// This is used for percent driven interactive transitions, as well as for
// container controllers that have companion animations that might need to
// synchronize with the main animation.
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
// This method can only  be a nop if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
@end

发现这个协议有两个必须实现的方法。第一个方法是设置动画的时间。第二个方法是设置动画。
好了,当这个YQAnimatedTransition类设置好后,在控制器需要用它的时候调用它。这个下面会具体说。

开始吃键盘

1.YQAnimatedTransition创建

首先创建一个继承NSObject,并且遵守UIViewControllerAnimatedTransitioning协议的类YQAnimatedTransition。
其次考虑到转场一共有四种方式:push,pop,present,dismiss。所以我加了一个枚举,用来设置转场的类型。

typedef enum {
    YQAnimatedTransitionTypePush,
    YQAnimatedTransitionTypePop,
    YQAnimatedTransitionTypePresent,
    YQAnimatedTransitionTypeDismiss
}YQAnimatedTransitionType;

为了方便这个类的使用,我加了一个类方法,在类方法中进行初始化且设置转场类型:

//.h
+ (YQAnimatedTransition *)animatedTransitionWithType:(YQAnimatedTransitionType)type;

//.m
+ (YQAnimatedTransition *)animatedTransitionWithType:(YQAnimatedTransitionType)type
{
    YQAnimatedTransition *animatedTransition = [[YQAnimatedTransition alloc] init];
    animatedTransition.type = type;
    return animatedTransition;
}

2.协议方法实现

下面是重点了!既然这个类遵循UIViewControllerAnimatedTransitioning协议,就需要实现协议方法。
直接上代码了。

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

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    self.transitionContext = transitionContext;
    
    if (self.type == YQAnimatedTransitionTypePush) {
    } else if (self.type == YQAnimatedTransitionTypePresent) {
    } else if (self.type == YQAnimatedTransitionTypeDismiss) {
    } else {
    }
}

解释一下。第一个方法的意思是我设置转场动画为0.5秒。第二个方法是在设置动画过程。由于篇幅过长,我暂时先省略啦~
重点说说上面的第二方法:动画设置。
不管是pop或者dismiss等等,只要控制器转场都会执行这第二个方法。所以首先在这个方法中进行判断,是属于哪种转场方式。然后再自定义动画。
以push为例子:

if (self.type == YQAnimatedTransitionTypePush) {
        
        // 获得即将消失的vc的v
        UIView *fromeView = [transitionContext viewForKey:UITransitionContextFromViewKey];
        // 获得即将出现的vc的v
        UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
        // 获得容器view
        UIView *containerView = [transitionContext containerView];
        
        [containerView addSubview:fromeView];
        [containerView addSubview:toView];
        
        UIBezierPath *startBP = [UIBezierPath bezierPathWithOvalInRect:CGRectMake((containerView.frame.size.width-100)/2, 100, 100, 100)];
        CGFloat radius = 1000;
        UIBezierPath *finalBP = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(150 - radius, 150 -radius, radius*2, radius*2)];
        
        CAShapeLayer *maskLayer = [CAShapeLayer layer];
        maskLayer.path = finalBP.CGPath;
        toView.layer.mask = maskLayer;
        
        //执行动画
        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"path"];
        animation.fromValue = (__bridge id _Nullable)(startBP.CGPath);
        animation.toValue = (__bridge id _Nullable)(finalBP.CGPath);
        animation.duration = [self transitionDuration:transitionContext];
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
        [maskLayer addAnimation:animation forKey:@"path"];
    }

这里用到了动画的知识,改变的是layer的path属性,让layer从小圆变成了大圆。
一直看代码和文字也累了吧,先看看现在push的效果好了。(注意哈,这里为了看效果,我已经在控制器写了调用该类的代码了,至于怎么调用,下面会说,先看效果吧~)

步骤2-1.gif

首先可以发现一个问题,就是返回不了了。解决办法是:在动画完成后加一行代码[transitionContext completeTransition:YES];。但是,问题又来了,这行代码加在哪里呢。
直接加在动画设置后面效果:

步骤2-2.gif

好像没问题,但是仔细观察发现navBar存在push的太早问题。如果你和我一样觉得这个很丑,那就换一个方法。
给animation设置代理,然后该类监听动画,当动画结束的时候再调用这行代码,这样就没问题啦。当然别忘了遵循动画协议CAAnimationDelegate。

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
    //告诉系统转场动画完成
    [self.transitionContext completeTransition:YES];
}

这样动画就写好了,至于present,dismiss等,也类似,就不再说啦。

上面动画实现中有一个layer.mask属性,我在本文最后会解释。

3.控制器调用

最后一步就是控制器调用刚写的类了。

a.push/pop方式如下:

在控制器中遵循UINavigationControllerDelegate协议,并实现协议方法:

- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC{
    
    if (operation == UINavigationControllerOperationPush) {
        YQAnimatedTransition *animatedTransition = [YQAnimatedTransition animatedTransitionWithType:YQAnimatedTransitionTypePush];
        return animatedTransition;
    }
    return nil;
}

b.present/dismiss方式如下:

在控制器中遵循UIViewControllerTransitioningDelegate协议,并实现方法:

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    return [YQAnimatedTransition animatedTransitionWithType:YQAnimatedTransitionTypePresent];
}


- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return [YQAnimatedTransition animatedTransitionWithType:YQAnimatedTransitionTypeDismiss];
}

到这里,转场动画就实现了。

步骤3-1.gif

4.细节补充

上图和效果图比较还是有差别的,少了一个过渡动画。当用户点击cell的时候,头像会移动且放大到详细页面那个头像那个位置。实现代码:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 获得点击的cell
    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    CGRect rectInTableView = [tableView rectForRowAtIndexPath:indexPath];
    // 获得点击cell的frame
    CGRect rect = [tableView convertRect:rectInTableView toView:[tableView superview]];
    
   // 设置selectImageView的位置和图片
    self.selectImageView.image = cell.imageView.image;
    self.selectImageView.frame = CGRectMake(cell.imageView.frame.origin.x, rect.origin.y, cell.imageView.frame.size.width, cell.imageView.frame.size.height);
    // 动画
    [UIView animateWithDuration:0.5 animations:^{
        self.selectImageView.frame = CGRectMake(0, 64, self.view.bounds.size.width, self.view.bounds.size.width);
    } completion:^(BOOL finished) {
        [self.navigationController pushViewController:detail animated:YES];
    }];
}

获取当前cell方法以及cell相对屏幕的位置两个方法每次都忘记,所以加粗,方便以后找。

上面代码的效果图:

步骤4-1.gif

现在的问题是返回的时候 self.selectImageView还在那里,所以需要在转场结束后使 self.selectImageView消失。
解决方法:

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if (viewController != self) {
        self.selectImageView.frame = CGRectNull;
    }
}

转场后,设置frame为CGRectNull,这样就消失啦~

layer.mask属性

其实这个mask属性用到的地方还是蛮多的。比如新手引导(虽然现在都是图片),还有微信的照片红包。下面说说这个属性。
mask是一个layer层,并且作为背景层和组成层之间的一个遮罩层通道,默认是nil。
还是在这个项目中,在列表控制器的- (void)viewDidLoad方法中加如下代码

    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(100, 100, 200, 200)].CGPath;
    self.view.layer.mask = shapeLayer;

效果图:

mask属性-1.png

发现就只有layer那一块显示出来,其余全部白色了。至于其余部分的颜色 是由 window.backgroundColor控制。
改成黑色:

mask属性-2.png

当我代码改成这样:(一条线的时候)

    UIBezierPath *path = [UIBezierPath bezierPath];
    [path moveToPoint:CGPointMake(100, 100)];
    [path addLineToPoint:CGPointMake(100, 500)];
    
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.path = path.CGPath;
    shapeLayer.lineWidth = 20;
    self.view.layer.mask = shapeLayer;
mask属性-3.png

发现不起作用,即使线宽为20。
当代码为三角形:

    UIBezierPath *path = [UIBezierPath bezierPath];
    [path moveToPoint:CGPointMake(100, 100)];
    [path addLineToPoint:CGPointMake(100, 500)];
    [path addLineToPoint:CGPointMake(200, 500)];
    [path closePath];
    
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.path = path.CGPath;
    shapeLayer.lineWidth = 20;
    self.view.layer.mask = shapeLayer;
mask属性-4.png

综上可以说明:layer的路径必须要封闭才能起作用。

最后

本文github地址:https://github.com/JabberYQ/animatedTransitionDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容