iOS: 记一次导航栏平滑过渡的实现

随着技术的迭代,现在对App的效果要求越来越高,那么在这篇文章里面我们一起讨论一下如何在控制器做跳转的时候对导航栏做平滑过渡的转场

构思:

  • 首先要获取到导航栏里的子控件来设置其透明度(实现透明度变化)
  • 为所有控制器添加一个导航栏透明度属性,用于记录当前控制器的导航栏透明度(记录透明度值)
  • 通过监听手势滑动来获取源和目的控制器,计算从源到目的控制器的透明度变化,来改变导航栏的透明度(实现平滑过渡)
bardemo

1、实现透明度变化

要想实现透明度变化,得先获取到导航栏里的子控件,然后设置其alpha值。但是如何获取呢? 首先考虑使用KVC,通过导航栏的'valueForKey:'方法来获取子控件对象,但是在不同的系统上,导航栏里的子控件布局排布也是有所不同, 意味着key值并非固定,通过key值拿子控件对象的方法在不同的系统上就很容易抛异常。

左:iOS10 右:iOS9

考虑到这一点,我采用了最直接的方式:遍历导航栏的所有子控件,拿到首个子控件给其设置透明度。这样不但不需要再去考虑系统的问题了,同时也能满足带颜色的导航栏或者是带背景图的导航栏透明度的变化。

- (void)xa_changeNavBarAlpha:(CGFloat)navBarAlpha{
    NSMutableArray *barSubviews = [NSMutableArray array];
    //将导航栏的子控件添加到数组当中,取首个子控件设置透明度(防止导航栏上存在非导航栏自带的控件)
    for (UIView * view in self.navigationBar.subviews) {
        if(![view isMemberOfClass:[UIView class]]){
            [barSubviews addObject:view];
        }
    }
    UIView *barBackgroundView = [barSubviews firstObject];
    barBackgroundView.alpha   = navBarAlpha;
}

2、记录透明度

每个控制器都应该有自己的导航栏透明度且当透明度发生变化后我们都应该把值保存下来,以方便下次的使用,这里我们就给UIViewController添加一个分类并加一个navBarAlpha的属性,这样我们就可以直接通过控制器去设置导航栏的透明度啦~

- (CGFloat)xa_navBarAlpha{
    return [objc_getAssociatedObject(self, _cmd)floatValue] ;
}

- (void)setXa_navBarAlpha:(CGFloat)xa_navBarAlpha{
    if(xa_navBarAlpha > 1){
        xa_navBarAlpha = 1;
    }
    if(xa_navBarAlpha < 0){
        xa_navBarAlpha = 0;
    }
    objc_setAssociatedObject(self, @selector(xa_navBarAlpha), @(xa_navBarAlpha), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    [self.navigationController xa_changeNavBarAlpha:xa_navBarAlpha];
}

3、实现平滑过渡

现在存在的问题就是我在某个页面设置了导航栏的透明度,back回上一个界面,导航栏的透明度值仍然是上个界面的


navbug

这里做过渡有两种情况,一种是手势滑动back回上一个界面,还有一种情况是直接点击了back按钮回到上一个界面的。根据这两种情况我们分别做一下处理。

3.1、手势滑动back

这种情况我们需要去监听导航控制器的手势滑动,导航控制器有个方法'_updateInteractiveTransition:',该方法可以监听手势滑动以及当前转场的进度,我们可以通过swizzing来交换方法实现,来接手'_updateInteractiveTransition:'方法调用的监听

+ (void)load{
    //交换导航控制器的手势进度转场方法,来监听手势滑动的进度
    SEL originalSEL =  NSSelectorFromString(@"_updateInteractiveTransition:");
    SEL swizzledSEL =  NSSelectorFromString(@"xa_updateInteractiveTransition:");
    Method originalMethod = class_getInstanceMethod(self,  originalSEL);
    Method swizzledMethod = class_getInstanceMethod(self,  swizzledSEL);
    BOOL success = class_addMethod(self, originalSEL, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if(success){
        class_replaceMethod(self, swizzledSEL, method_getImplementation(originalMethod),  method_getTypeEncoding(originalMethod));
    }else{
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

然后通过转场的上下文信息,拿到源和目的的控制器navBarAlpha值,再根据percentComplete转场进度参数计算并设置导航栏透明度值,这样就完成了手势滑动的back

- (void)xa_updateInteractiveTransition:(CGFloat)percentComplete{
    [self xa_updateInteractiveTransition:percentComplete];
    UIViewController *topVC = self.topViewController;
    if(topVC){
        //通过transitionCoordinator拿到转场的两个控制器上下文信息
        id <UIViewControllerTransitionCoordinator> coordinator =  topVC.transitionCoordinator;
        if(coordinator != nil){
            //拿到源控制器和目的控制器的透明度(每个控制器都单独保存了一份)
            CGFloat fromVCAlpha  = [coordinator viewControllerForKey:UITransitionContextFromViewControllerKey].xa_navBarAlpha;
            CGFloat toVCAlpha    = [coordinator viewControllerForKey:UITransitionContextToViewControllerKey].xa_navBarAlpha;
            //再通过源,目的控制器的导航条透明度和转场的进度(percentComplete)计算转场时导航条的透明度
            CGFloat newAlpha     = fromVCAlpha + ((toVCAlpha - fromVCAlpha ) * percentComplete);
            //这里不要直接去修改控制器navBarAlpha属性,会影响目的控制器的navBarAlpha的数值
            [self xa_changeNavBarAlpha:newAlpha];
        }
    }
}

3.2、按钮点击back

当点击buttonItem back控制器的时候我们可以在'viewWillAppear:'的时候设置回当前控制器的透明度值,所以我们同样要交换'viewWillAppear:'方法的实现,那么每当控制器要显示的时候,我们总是要将它重置回当前控制器应有的透明度值。

这里另外还需要做两个逻辑判断:

  • 一个是判断手势是否正在滑动。如果是YES表示当前的状态是手势滑动back的状态则不需要处理。
  • 另外一个逻辑是判断当前控制器是否设置过navBarAlpha的属性值。如果有设置过,那么每次控制器要显示的时候都要将导航栏透明度设置成控制器储存的透明值。反之,我们给这个控制器设置一个默认的透明度值
- (void)xa_viewWillAppear:(BOOL)animated{
    [self xa_viewWillAppear:animated];
    
    //当前控制器父控制器是导航控制器并且不是通过手势滑动显示的
    if([self.parentViewController isKindOfClass:[UINavigationController class]] &&
       (!self.navigationController.xa_isGrTransitioning)){
        //如果在控制器初始化的时候用户设置过导航栏的值,那么我们直接设置该导航栏应有的透明度值,没有设置过的话默认透明度给1
        if(self.xa_didSetBarAlpha){
            [self.navigationController xa_changeNavBarAlpha:self.xa_navBarAlpha];
        }else{
            self.xa_navBarAlpha = 1;
        }
    }
}

4、细节优化与调整

在手势滑动的时候,如果滑动到了一半就松手了,那么导航栏就可能自动完成或者取消返回操作了,导致剩下的导航栏的透明度将无法计算,可以看到firstViewController的导航栏透明度并非是1

bug

对于这一点的话,我们可以添加手势滑动的交互的状态,如果当前的滑动的过程中中断,那么判断是取消操作还是完成操作,然后再完成剩余的动画效果。
首先我们要去监听导航控制器的'popViewControllerAnimated:'的方法调用

+ (void)load{
    //交换导航控制器的popViewControllerAnimated:方法,来监听什么时候当前控制被back
    SEL popOriginalSEL =  @selector(popViewControllerAnimated:);
    SEL popSwizzledSEL =  NSSelectorFromString(@"xa_popViewControllerAnimated:");
    Method popOriginalMethod = class_getInstanceMethod(self,  popOriginalSEL);
    Method popSwizzledMethod = class_getInstanceMethod(self,  popSwizzledSEL);
    BOOL popSuccess = class_addMethod(self, popOriginalSEL, method_getImplementation(popSwizzledMethod), method_getTypeEncoding(popSwizzledMethod));
    if(popSuccess){
        class_replaceMethod(self, popSwizzledSEL, method_getImplementation(popOriginalMethod),  method_getTypeEncoding(popOriginalMethod));
    }else{
        method_exchangeImplementations(popOriginalMethod, popSwizzledMethod);
    }
    
}

然后再通过转场协调器对象监听手势滑动交互的改变

- (UIViewController *)xa_popViewControllerAnimated:(BOOL)animated{
    UIViewController *popVc =  [self xa_popViewControllerAnimated:animated];
    if(self.viewControllers.count <= 0){
        return popVc;
    }
    UIViewController *topVC = [self.viewControllers lastObject];
    if (topVC != nil) {
        id<UIViewControllerTransitionCoordinator> coordinator = topVC.transitionCoordinator;
        
        //监听手势返回的交互改变,如手势滑动过程当中松手就会回调block
        if (coordinator != nil) {
            if([[UIDevice currentDevice].systemVersion intValue]  >= 10){//适配iOS10
                [coordinator notifyWhenInteractionChangesUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context){
                    [self dealNavBarChangeAction:context];
                }];
            }else{
                [coordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
                    [self dealNavBarChangeAction:context];
                }];
            }
        }
    }
    return popVc;
}

最后我们通过转场的上下文信息根据操作(自动完成还是返回操作)来获取剩余的动画时长,并完成剩余的动画

- (void)dealNavBarChangeAction:(id<UIViewControllerTransitionCoordinatorContext>)context {
    if ([context isCancelled]) {// 取消了(还在当前页面)
        //根据剩余的进度来计算动画时长xa_changeNavBarAlpha
        CGFloat animdDuration = [context transitionDuration] * [context percentComplete];
        CGFloat fromVCAlpha   = [context viewControllerForKey:UITransitionContextFromViewControllerKey].xa_navBarAlpha;
        [UIView animateWithDuration:animdDuration animations:^{
            [self xa_changeNavBarAlpha:fromVCAlpha];
        }];
        
    } else {// 自动完成(pop到上一个界面了)
        
        CGFloat animdDuration = [context transitionDuration] * (1 -  [context percentComplete]);
        CGFloat toVCAlpha     = [context viewControllerForKey:UITransitionContextToViewControllerKey].xa_navBarAlpha;
        [UIView animateWithDuration:animdDuration animations:^{
            [self xa_changeNavBarAlpha:toVCAlpha];
        }];
    };
}

最后:

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

推荐阅读更多精彩内容