收藏iosiOS精选

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

2020-03-19  本文已影响0人  mapleYe

前言

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


今日头条效果.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,处理点击退出和手势退出时,执行不一样的转场效果。这里还需要完善的地方有

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

6、Demo地址

https://github.com/maple1994/MPPlayerDemo

上一篇下一篇

猜你喜欢

热点阅读