【iOS】今日头条的转场动画设置+手势控制

前言

最近公司有个需求,做一个今日头条的用户动态的进入和退出的动画效果,并且退场时,可以自己点击退出,也可以手势下滑退出。头条的效果如下:


今日头条效果.gif

分析

1、动画转场的实现

首先我们需要实现UINavigationDelegate

- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC

此方法返回一个遵守UIViewControllerAnimatedTransitioning的class,在里面书写我们要实现的动画效果

2、触发pop的手势处理

同样的需要实现UINavigationDelegate

- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController

此方法返回一个遵守UIViewControllerInteractiveTransitioning的class,一般会用UIPercentDrivenInteractiveTransition。这个percent手势处理转场的方式,只要按时机调用以下三个方法

/// 返回这个转场完成的百分比 0~1
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
/// 取消转场
- (void)cancelInteractiveTransition;
/// 完成转场
- (void)finishInteractiveTransition;

而如果我们需要实现下滑退出的话,就需要配合UIPanGestureRecognizer进行使用了,Demo核心的手势处理代码如下:

- (CGFloat)percentForGesture:(UIPanGestureRecognizer *)gesture{
    // 最多只能移动SL_SCREEN_HEIGHT * 0.5
    CGFloat maxOffset = ZFPlayer_ScreenHeight * 0.5;
    CGFloat y = [gesture locationInView:[UIApplication sharedApplication].keyWindow].y;
    // 移动的距离
    CGFloat distance = y - self.startOffsetY;
    distance = MIN(maxOffset, distance);
    double degree = (distance / maxOffset) * M_PI_2;
    // 为增量实现一个曲线变化的效果
    double x = 1 - (sin(degree));
    // 计算增量
    CGFloat delta = distance - self.lastOffsetY;
    self.lastOffsetY = self.lastOffsetY + x * delta;
    self.lastOffsetY = MAX(self.lastOffsetY, 0);
    CGFloat percent = self.lastOffsetY / maxOffset;
    return percent;
}


- (void)panAction: (UIPanGestureRecognizer *)gestureRecognizer
{
    switch (gestureRecognizer.state){
        case UIGestureRecognizerStateBegan:
        {
            self.startOffsetY = [gestureRecognizer locationInView:[UIApplication sharedApplication].keyWindow].y;
            [self.navigationController popViewControllerAnimated:YES];
            break;
        }
        case UIGestureRecognizerStateChanged:
            // 调用updateInteractiveTransition来更新动画进度
            // 里面嵌套定义 percentForGesture 方法计算动画进度
            [self.interactiveGes updateInteractiveTransition:[self percentForGesture:gestureRecognizer]];
            break;
        case UIGestureRecognizerStateEnded:
            //判断手势位置,要大于一般,就完成这个转场,要小于一半就取消
            if ([self percentForGesture:gestureRecognizer] >= 0.4) {
                self.transition.isComplete = YES;
                // 完成交互转场
                [self.interactiveGes finishInteractiveTransition];
            }else {
                // 取消交互转场
                [self.interactiveGes cancelInteractiveTransition];
            }
            break;
        default:
            [self.interactiveGes cancelInteractiveTransition];
            break;
    }
}

要注意的是,在pan手势触发的时候,需要先调用[self.navigationController popViewControllerAnimated:YES];,告诉导航控制器,我要执行pop操作

3、手势退出和点击back退出的处理

我们可以仔细观察一下今日头条的Gif,不难发现他点击返回键退出,以及手势退出时,转场动画时不一样的。

  • 点击返回键退出时:直接中间一个大的圆形头像,回到上个列表头像位置
  • 手势退出时:整个页面下滑,背景透明度改变,松开时,再进入点击返回键退出时的动画效果

因为这里产生了两种动画执行的方式,我这里声明了一个属性,继续用户是点击退出,然后手势退出的

@property (nonatomic, assign) BOOL isInteracting;

那么在点击退出时,设置为NO,请他情况皆为YES,然后在对应的地方做处理即可

/// 若不是手势退出,直接返回nil则不会调用手势操作的相关方法
- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
{
    return self.isInteracting ? self.interactiveGes : nil;
}

同时,在转场动画也要做相应的处理,转场动画需要标记手势是否完成,然后再去做对应的动画

/// 关注的用户动态转场
@interface MPUserDynamicTransition : NSObject<UIViewControllerAnimatedTransitioning, CAAnimationDelegate>
/// 是否手势退出
@property (nonatomic, assign) BOOL isInteracting;
/// 是否手势完成
@property (nonatomic, assign) BOOL isComplete;

pop动画的核心动画代码

- (void)startPopAnimation: (nonnull id<UIViewControllerContextTransitioning>)transitionContext
{
    UIView *contentView = [transitionContext containerView];
    // 获取 fromView 和 toView
    UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView *whiteCoverView = [[UIView alloc] init];
    whiteCoverView.backgroundColor = [UIColor blackColor];
    whiteCoverView.frame = CGRectMake(0, 0, ZFPlayer_ScreenWidth, ZFPlayer_ScreenHeight);
    whiteCoverView.alpha = 0;
    [contentView addSubview:toView];
    [contentView addSubview:whiteCoverView];
    [contentView addSubview:fromView];
    
    UIImageView *imageView = [[UIImageView alloc] init];
    imageView.contentMode = UIViewContentModeScaleAspectFill;
    imageView.clipsToBounds = YES;
    imageView.image = self.startImage;
    imageView.layer.cornerRadius = ZFPlayer_ScreenWidth * 0.5;
    CGFloat top = (ZFPlayer_ScreenHeight - ZFPlayer_ScreenWidth) * 0.5;
    CGRect winFrame = CGRectMake(0, top, ZFPlayer_ScreenWidth, ZFPlayer_ScreenWidth);
    imageView.frame = winFrame;
    imageView.hidden = YES;
    [contentView addSubview:imageView];
    
    CGFloat targetCorner = 0;
    CGRect targetFrame = CGRectZero;
    if (self.startView) {
        targetFrame = [self.startView convertRect:self.startView.bounds toView:nil];
        targetFrame = CGRectMake(self.endX, targetFrame.origin.y, targetFrame.size.width, targetFrame.size.height);
        targetCorner = self.startView.bounds.size.width * 0.5;
    }
    dispatch_block_t block = dispatch_block_create(0, ^{
        imageView.hidden = NO;
        toView.alpha = 1.0f;
        fromView.transform = CGAffineTransformIdentity;
        fromView.alpha = 0.0f;
        whiteCoverView.alpha = 0.4;
        [UIView animateWithDuration:self.duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
            whiteCoverView.alpha = 0;
            imageView.frame = targetFrame;
            imageView.layer.cornerRadius = targetCorner;
        } completion:^(BOOL finished) {
            [imageView removeFromSuperview];
            [whiteCoverView removeFromSuperview];
            // 结束动画
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];
    });
    if (self.isInteracting) {
        whiteCoverView.alpha = 1;
        [UIView animateWithDuration:self.duration animations:^{
            whiteCoverView.alpha = 0.4;
            fromView.transform = CGAffineTransformScale(fromView.transform, 0.9, 0.9);
            fromView.transform = CGAffineTransformTranslate(fromView.transform, 0, ZFPlayer_ScreenHeight * 0.5);
        } completion:^(BOOL finished) {
            if (self.isComplete) {
                block();
            }else {
                [imageView removeFromSuperview];
                [whiteCoverView removeFromSuperview];
                // 结束动画
                [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
            }
        }];
    }else {
        block();
    }
}

注意self.isInteractingself.isComplete这两个Bool控制显示的动画即可

4、完成的效果如下

手势退出转场演示.gif

5、总结

这个Demo只是在演示如何用一个Transition,处理点击退出和手势退出时,执行不一样的转场效果。这里还需要完善的地方有

  • 用户详情页做成头条的列表页面时,退出pan的手势和tableView的触发时机
  • 侧滑处理,这个红色页面是不能侧滑退出的

关于转场动画的书写,可以看以下链接
https://blog.devtang.com/2016/03/13/iOS-transition-guide/

6、Demo地址

https://github.com/maple1994/MPPlayerDemo

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

推荐阅读更多精彩内容