专栏首页MapleYe【iOS】今日头条的转场动画设置+手势控制

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

前言

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

今日头条效果

分析

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、完成的效果如下

手势退出转场演示

5、总结

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

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

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

6、Demo地址

https://github.com/maple1994/MPPlayerDemo

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【iOS】瀑布流的实现

    Simulator Screen Shot - iPhone 8 - 2020-01-16 at 17.32.16.png

    MapleYe
  • 【iOS】教你用ZFPlayer+KTVHTTPCache搭建缓存,预加载的播放器

    mgr实现ZFPlayerMediaPlayback协议,然后在初始化时,开启本地服务器

    MapleYe
  • 【iOS】基于Realm数据库的记账软件--记账模块(二)

    从记账的需求出发,该界面需要用户输入以下账单信息: (1)账单金额 (2)账单类型 (3)相关账户 (4)账单产生的日期 (5)备注 那么,结合一下...

    MapleYe
  • 强化学习第-1步

    function self = one_dimensional_env(len,fresh_time)

    万木逢春
  • 使用Python写Windows Ser

    如果你想用Python开发Windows程序,并让其开机启动等,就必须写成windows的服务程序Windows Service,用Python来做这个事情必...

    py3study
  • Quartz2D复习(三) --- 涂鸦

    和上一篇手势解锁不一样,手势解锁只画了一条路径,从触摸开始--》触摸移动--》触摸结束 ,然后路径完成了,渲染出来就是手势解锁了;

    tandaxia
  • Python使用tkinter打造自定义对话框完整代码

    问题来源:前一阵发过一个技术文章Python编写抽奖式随机提问程序,其中有个弹出式对话框,好像上海科技大学宋老师在群里当时问了一句对话框中中奖姓名是否能显示的大...

    Python小屋屋主
  • python---贪吃蛇

    因为是简单的做一个贪吃蛇,并没有做其他的分数显示界面,以及结果的显示,具体效果就是运行程序后,出现上面的界面,然后只有你一动wasd的其中一个键,贪吃蛇便开始运...

    sjw1998
  • 性能测试: Python3 利用asynico协程系统构建生产消费模型

    今天研究了下python3的新特性 asynico ,试了试 aiohttp 协程效果,单核QPS在500~600之间,性能还可以。

    机械视角
  • 高效处理流量加解密——Burpy

    先来地址:Github: https://github.com/mr-m0nst3r/Burpy

    用户2202688

扫码关注云+社区

领取腾讯云代金券