自定义转场动画实现导航栏渐变效果

前言

使用系统的转场动画,不能控制动画整个动画过程,没法针对系统导航栏进行颜色过渡效果,故此篇文章采取自定义转场动画的方式,把控整个动画过程,实现导航栏颜色平滑的过渡效果,实际效果如下所示。

正文

接下来开始进入正题,本篇文章主要涉及到自定义转场动画的实现和通过runtime给分类添加属性。

首先我们要自己实现push和pop时候的转场动画,实现转场动画主要是实现UINavigationController的代理方法


主要实现红色框中的两个代理方法,其中下面的代理方面是定义push和pop时的动画效果(没有手势交互),上面哪一个代理方法是左划屏幕pop时的动画交互(push不存在手势滑动,故push不需要)。

下面的代理方法返回了一个遵循UIViewControllerAnimatedTransitioning协议的对象,新建一个类KJPushAnimator并遵循该协议,该协议中有两个required方法

1、 - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
2、 - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

第一个方法表示动画的持续时间,第二方法就是实现我们转场时候的动画了。
代码如下:

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

动画时间定义为0.25秒,然后另一个协议方法的实现如下

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {

    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    
    
    UIView *containerView = [transitionContext containerView];
    UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    toView.kj_x = [UIScreen mainScreen].bounds.size.width;
    
    [containerView addSubview:fromView];
    [containerView addSubview:toView];
    
    UINavigationBar *navigationBar = toVC.navigationController.navigationBar;
    
    [navigationBar kj_setBackgroundColor:fromVC.kj_navigationBarTintColor];
    [navigationBar kj_setNavigationBarAlpha:fromVC.kj_navigationBarAlpha];
    [navigationBar kj_setNavigationTitleColor:fromVC.kj_navigationTitleColor];
    
    [UIView animateWithDuration:kTransitionDuration delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
        
        fromView.kj_x = -100;
        toView.kj_x = 0;
        
        [navigationBar kj_setBackgroundColor:toVC.kj_navigationBarTintColor];
        [navigationBar kj_setNavigationBarAlpha:toVC.kj_navigationBarAlpha];
        [navigationBar kj_setNavigationTitleColor:toVC.kj_navigationTitleColor];
        
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        [navigationBar kj_setBackgroundColor:toVC.kj_navigationBarTintColor];
        [navigationBar kj_setNavigationBarAlpha:toVC.kj_navigationBarAlpha];
        [navigationBar kj_setNavigationTitleColor:toVC.kj_navigationTitleColor];
    }];
}

首先拿到两个转场相关的两个viewController(fromVC和toVC)以及它们的view(fromView和toView),这里containerView是用来承载fromView和toView的容器父视图,然后将fromView和toView add到containerView上,注意添加的顺序,注意到系统的push动画是从右往左出现要push的vc,所以toView初始的x坐标是屏幕宽度。然后是动画开始前设置navigationBar的相关属性,因为是从fromVC push 到toVC,故navigationBar的设置先跟fromVC关联,然后简单的使用UIView动画模仿系统的push动画,并设置navigationBar的颜色和透明度等。

实现完push转场,在来看pop转场实现:
新建一个类KJPopAnimator遵循UIViewControllerAnimatedTransitioning
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.25;
}
同样动画时间定义为0.25秒,另一个协议方法如下:

  • (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {

      UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
      UIViewController  *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
      
      
      UIView *containerView = [transitionContext containerView];
      UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
      toView.kj_x = -100;
      
      UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
      fromView.layer.shadowRadius = 8;
      fromView.layer.shadowColor = [UIColor blackColor].CGColor;
      fromView.layer.shadowOpacity = 0.5;
      
      UIView *maskView = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
      maskView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.15];
      
      
      [containerView addSubview:toView];
      [containerView addSubview:maskView];
      [containerView addSubview:fromView];
      
      CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
      animation.duration = kTransitionDuration;
      animation.removedOnCompletion = YES;
      animation.toValue = @0;
      animation.delegate = self;
      [fromView.layer addAnimation:animation forKey:nil];
      
      UINavigationBar *navigationBar = toVC.navigationController.navigationBar;
      [navigationBar kj_setBackgroundColor:fromVC.kj_navigationBarTintColor];
      [navigationBar kj_setNavigationBarAlpha:fromVC.kj_navigationBarAlpha];
      [navigationBar kj_setNavigationTitleColor:fromVC.kj_navigationTitleColor];
    
      
      [UIView animateWithDuration:kTransitionDuration delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
          toView.kj_x = 0;
          fromView.kj_x = [UIScreen mainScreen].bounds.size.width;
          maskView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0];
          
          [navigationBar kj_setBackgroundColor:toVC.kj_navigationBarTintColor];
          [navigationBar kj_setNavigationBarAlpha:toVC.kj_navigationBarAlpha];
          [navigationBar kj_setNavigationTitleColor:toVC.kj_navigationTitleColor];
          
      } completion:^(BOOL finished) {
          [maskView removeFromSuperview];
          fromView.layer.shadowOpacity = 0;
          [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
      }];
    

    }

实现上大致和push差不多,这里为了和系统的pop动画保持一致,还加了pop时的视图阴影以及一个黑色透明的遮罩层(读者可以自己打开一个app观察系统的pop动画,建议手势左划观察),另外需要注意的一点是,pop是从fromVC到toVC的过程,所以这里视图添加的顺序和push不一样,containerView先add toView然后在add fromView,确保fromView在最上面。

实现完push和pop动画后就要运用到开始说的UINavigationController的代理方法了,创建一个继承自UINavigationController的子类KJNavigationController,在viewDidLoad方法中设置self.delegate = self;然后实现代理方法

- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                            animationControllerForOperation:(UINavigationControllerOperation)operation
                                                         fromViewController:(UIViewController *)fromVC
                                                           toViewController:(UIViewController *)toVC {
    if (operation == UINavigationControllerOperationPush) {
        
        return [KJPushAnimator new];
    } else if (operation == UINavigationControllerOperationPop) {
        return [KJPopAnimatior new];
    }
    
    return nil;
}

通过operation判断是push行为还是pop,传入对应的动画驱动,此时我们就自己模仿了系统的转场动画,但是系统默认还有手势左划pop回上一个页面,这就需要实现UINavigationController的里一个代理方法- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController

该代理返回的是一个实现了UIViewControllerInteractiveTransitioning的对象,系统有一个已经实现好的类UIPercentDrivenInteractiveTransition,直接使用就



然后就是滑动手势的实现,使用UIScreenEdgePanGestureRecognizer,顾名思义,手势的触发是从屏幕的边缘(上下左右四个边缘)

- (void)setupGesture { 
    UIScreenEdgePanGestureRecognizer *edgePan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(edgePanGesture:)];
    edgePan.edges = UIRectEdgeLeft;
[self.view addGestureRecognizer:edgePan];
}

设置从屏幕左边缘触发,然后看edpePanGesuture方法

- (void)edgePanGesture:(UIScreenEdgePanGestureRecognizer *)sender {
    UIGestureRecognizerState state = sender.state;
    
    CGFloat offsetX = MAX(0, [sender translationInView:sender.view].x);
    CGFloat width = [UIScreen mainScreen].bounds.size.width;
    CGFloat percent = offsetX / width;
    switch (state) {
        case UIGestureRecognizerStateBegan:
        {
            self.interactive  = [[UIPercentDrivenInteractiveTransition alloc] init];
            self.interactive.completionCurve = UIViewAnimationCurveLinear;
            [self popViewControllerAnimated:YES];
        }
            break;
        case UIGestureRecognizerStateChanged:
        {
            
            [self.interactive updateInteractiveTransition:percent];
            
        }
            break;
        case UIGestureRecognizerStateEnded:
        {
            if (percent > 0.5) {
                [self.interactive finishInteractiveTransition];
            } else {
                [self.interactive cancelInteractiveTransition];
            }
            
            self.interactive = nil;
        }
            break;
        default:
        {
            [self.interactive cancelInteractiveTransition];
            self.interactive = nil;
        }
            break;
    }
}

首先拿到手指滑动的偏移量offsetX,计算手指滑动距离与屏幕尺寸的百分比,然后判断手势的状态,在手势began时,调用popViewControllerAnimated方法(这一步很重要),在changed时,告诉UIPercentDrivenInteractiveTransition对象pop完成的百分比,在手势end时,判断手指滑动是否超过屏幕一般,超过一般则pop完成,调用finishInteractiveTransition,没有则表示取消这次pop,调用cancelInteractiveTransition,并且注意将self.interactive置为nil。

以上实现这些都是为了能够得到push和pop时的转场进行的百分比(进度),接来下就是对navigationBar的一些配置。

给navigationBar创建一个分类,设置navigationBar的颜色、透明度以及标题颜色


这里不是直接对navigationBar进行这些颜色设置,而是在navigationBar上插入一个子视图,通过设置这个子视图的颜色来显示navigationBar的颜色

- (UIView *)backgroundView {
    UIView *backgroundView = objc_getAssociatedObject(self, _cmd);
    if (!backgroundView) {
        [self setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
        backgroundView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([UIScreen mainScreen].bounds), CGRectGetHeight(self.bounds) + 20)];
        backgroundView.userInteractionEnabled = NO;
        backgroundView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin|UIViewAutoresizingFlexibleTopMargin|UIViewAutoresizingFlexibleBottomMargin;
        [self.subviews.firstObject insertSubview:backgroundView atIndex:0];
        [self setBackgroundView:backgroundView];
    }
    self.backgroundColor = [UIColor clearColor];
    return backgroundView;
}
- (void)setBackgroundView:(UIView *)maskLayer {
    objc_setAssociatedObject(self, @selector(backgroundView), maskLayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

上图中将_UIBarBackground的color设置为clearColor,后面的navigationBar的颜色实际上就是插入的backgroundView的颜色

最后再给UIViewController添加一个分类,方便直接设置navigationBar的相关属性




就是一些setter和getter方法,主要是用户没有手动设置时给定一个默认置,代码比较简单就不做详细解释了,读者此时可以回到KJPushAnimator和KJPopAnimator中看看navigationBar的相关设置,注意动画前后navigationBar的设置。

然后实际项目中的设置navigationBar的颜色,和标题颜色就非常简单了,#improt "UIViewController+KJNavigationBar.h"然后在viewDidLoad中

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.kj_navigationBarTintColor = [UIColor cyanColor];
    self.kj_navigationTitleColor = [UIColor whiteColor];
}

实际效果就如同文章开始的gif一样

本文完整Demo请点击这里

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

推荐阅读更多精彩内容