美文网首页常用技术收集iOSios
导航栏的平滑显示和隐藏 - 个人页的自我修养(1)

导航栏的平滑显示和隐藏 - 个人页的自我修养(1)

作者: 日光镇 | 来源:发表于2017-03-07 23:43 被阅读12719次

    本文是《个人页的自我修养》系列文章的一篇,全部:

    • 导航栏的平滑显示和隐藏 - 个人页的自我修养(1) (本篇)
    • 多个UITableView共用一个tableHeader的效果实现 - 个人页的自我修养(2)(待补坑)
    • 处理Pan手势和ScrollView滚动冲突 - 个人页的自我修养(3)(待补坑)

    关于“个人页”

    带有社交属性的APP一般都会有一个“个人页”,用以展示某个用户的个人信息和发布的内容。以下是几个例子:

    个人页例子.png

    以上页面的共同特征是:
    1、透明的导航栏以更好的展示背景图
    2、可按标签切换到不同的内容页(这个特性看需求,不一定有)
    3、滚动时会停靠在页面顶部的SegmentView
    3、各个可滚动的内容页共用一个header

    最近刚好写到Rabo微博客户端的个人页的部分,发现踩到几个有意思的坑,解决下来决定写个系列文章把相关解决方法和代码分享一下。
    先看一下要实现的整体效果:

    overView.gif

    这篇文章先处理导航栏的平滑隐藏和显示。

    导航栏的平滑显示和隐藏

    1、现有解决方案

    先看一下手机QQ,是我目前能找到的处理得算比较好的导航栏返回效果。导航栏有跟随返回手势透明度渐变的动画。


    QQ返回.gif

    但导航栏的返回交互动画是自定义的,没有系统自带的视差效果和毛玻璃效果,而且中断返回操作的话导航栏会闪一下,影响观感。


    QQ取消返回.gif

    再看一下其他3家的处理方式,他们的处理方法基本一致,都是在进入个人页时隐藏了系统导航栏,然后添加一个自定义的导航栏,所以过度会比较生硬,与整体的返回效果有断层。

    微博.gif
    百度贴吧.gif
    Twitter.gif

    好,看完以上的例子,轮到我们来实现啦。我们今天的目标是不自定义导航栏,在系统自带导航栏的基础上进行非侵入(代码解耦)的实现。先看效果:

    navDemo.gif
    你可以在这里下载本篇文章的代码:https://github.com/EnderTan/ETNavBarTransparent
    2、记录某个VC的导航栏透明度

    对于同一个NavigationController上的ViewController,NavigationBar是全局的,并不能单独设置某个ViewController的导航栏样式和属性。所以我们先给ViewController用扩展添加一个记录导航栏透明度的属性:

    //ET_NavBarTransparent.swift
    
    extension UIViewController {
        
          fileprivate struct AssociatedKeys {
               static var navBarBgAlpha: CGFloat = 1.0
          }
        
          var navBarBgAlpha: CGFloat {
              get {
                  let alpha = objc_getAssociatedObject(self, &AssociatedKeys.navBarBgAlpha) as? CGFloat
                  if alpha == nil {
                    //默认透明度为1
                      return 1.0
                  }else{
                      return alpha!
                  }
                
              }
              set {
                  var alpha = newValue
                  if alpha > 1 {
                      alpha = 1
                  }
                  if alpha < 0 {
                      alpha = 0
                  }
                
                  objc_setAssociatedObject(self, &AssociatedKeys.navBarBgAlpha, alpha, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
                
                  //设置导航栏透明度
                  navigationController?.setNeedsNavigationBackground(alpha: alpha)
              }
          }
    }
    

    好的,现在可以根据需要随时记录下某个VC的导航栏透明度了,而不会因为push到下个页面而丢失了这个信息。

    3、设置导航栏背景的透明度

    要实现上面demo的效果,我们不能修改整个导航栏的透明度,因为导航栏上的NavigationBarItem是需要保留下来的,如果设置整个导航栏的透明度,左右的Item和标题栏都会跟着一起透明了。

    navItem.png

    然而,系统API并没有访问背景View的接口,只好动用下黑魔法了。先看一下导航栏的层级:

    navlevel.png

    首先想到调整第一层_barBackgroundView(_UIBarBackground)的透明度,但试了一下,调整这一层级会丢失毛玻璃效果,效果很突兀:

    bgAlphaErr.gif

    经过测试,调整_backgroundEffectView(-UIVisualEffectView)不会丢失毛玻璃效果:

    bgAlphaRight.gif

    下面是调整导航栏背景透明度的相关代码:

    //ET_NavBarTransparent.swift
    
    extension UINavigationController {
        //Some other code
        fileprivate func setNeedsNavigationBackground(alpha:CGFloat) {
            let barBackgroundView = navigationBar.value(forKey: "_barBackgroundView") as AnyObject
            let backgroundImageView = barBackgroundView.value(forKey: "_backgroundImageView") as? UIImageView
            if navigationBar.isTranslucent {
                if backgroundImageView != nil && backgroundImageView!.image != nil {
                    (barBackgroundView as! UIView).alpha = alpha
                }else{
                    if let backgroundEffectView = barBackgroundView.value(forKey: "_backgroundEffectView") as? UIView {
                        backgroundEffectView.alpha = alpha
                    }
                }
            }else{
                (barBackgroundView as! UIView).alpha = alpha
            }
            
            if let shadowView = barBackgroundView.value(forKey: "_shadowView") as? UIView {
                shadowView.alpha = alpha
            }
            
        }
    }
    

    到这里,我们只要给viewController的扩展属性navBarBgAlpha赋值,就可以随意设置导航栏的透明度了。

    4、监控返回手势的进度

    在手势返回的交互中,如果前后两个VC的导航栏透明度不一样,需要根据手势的进度实时调节透明度。
    这里method swizzling一下,用UINavigationController的"_updateInteractiveTransition:"方法监控返回交互动画的进度。

    //ET_NavBarTransparent.swift
    
    extension UINavigationController {
    
        //Some other code
        
        open override class func initialize(){
            
            if self == UINavigationController.self {
                let originalSelectorArr = ["_updateInteractiveTransition:"]
                //method swizzling
                for ori in originalSelectorArr {
                    let originalSelector = NSSelectorFromString(ori)
                    let swizzledSelector = NSSelectorFromString("et_\(ori)")
                    let originalMethod = class_getInstanceMethod(self.classForCoder(), originalSelector)
                    let swizzledMethod = class_getInstanceMethod(self.classForCoder(), swizzledSelector)
                    method_exchangeImplementations(originalMethod, swizzledMethod)
                }
                
            }
            
        }
        
    
        func et__updateInteractiveTransition(_ percentComplete: CGFloat) {
            et__updateInteractiveTransition(percentComplete)
            let topVC = self.topViewController
            if topVC != nil {
                //transitionCoordinator带有两个VC的转场上下文
                let coor = topVC?.transitionCoordinator
                if coor != nil {
                    //fromVC 的导航栏透明度
                    let fromAlpha = coor?.viewController(forKey: .from)?.navBarBgAlpha
                    //toVC 的导航栏透明度
                    let toAlpha = coor?.viewController(forKey: .to)?.navBarBgAlpha
                    //计算当前的导航栏透明度
                    let nowAlpha = fromAlpha! + (toAlpha!-fromAlpha!)*percentComplete
                    //设置导航栏透明度
                    self.setNeedsNavigationBackground(alpha: nowAlpha)
                }
            }
            
        }
    }
    

    看一下到这一步的效果:

    releaseFinger.gif

    在手势交互的过程中,透明度的变化跟预期一样跟随手势变化。但一旦松手,系统会自动完成或取消返回操作,在这一过程中,以上的方法并没有调用,而导致透明度停留在最后的那个状态。
    我们需要在UINavigationControllerDelegate中添加边缘返回手势松手时的监控,还有要处理一下直接点击返回按钮和正常Push到新界面时的情况:

    //ET_NavBarTransparent.swift
    
    extension UINavigationController:UINavigationControllerDelegate,UINavigationBarDelegate {
    
        public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
            let topVC = navigationController.topViewController
            if topVC != nil {
                let coor = topVC?.transitionCoordinator
                if coor != nil {
                    //添加对返回交互的监控
                    if #available(iOS 10.0, *) {
                        coor?.notifyWhenInteractionChanges({ (context) in
                        self.dealInteractionChanges(context)
                        })
                    } else {
                        coor?.notifyWhenInteractionEnds({ (context) in
                            self.dealInteractionChanges(context)
                        })
                        
                    } 
    
                }
                
            }
        }
        
        //处理返回手势中断对情况
            private func dealInteractionChanges(_ context:UIViewControllerTransitionCoordinatorContext) {
            if context.isCancelled {
                //自动取消了返回手势
                let cancellDuration:TimeInterval = context.transitionDuration * Double( context.percentComplete)
                UIView.animate(withDuration: cancellDuration, animations: {
                    
                    let nowAlpha = (context.viewController(forKey: .from)?.navBarBgAlpha)!
                    self.setNeedsNavigationBackground(alpha: nowAlpha)
                    
                    self.navigationBar.tintColor = context.viewController(forKey: .from)?.navBarTintColor
                })
            }else{
                //自动完成了返回手势
                let finishDuration:TimeInterval = context.transitionDuration * Double(1 - context.percentComplete)
                UIView.animate(withDuration: finishDuration, animations: {
                    let nowAlpha = (context.viewController(forKey: .to)?.navBarBgAlpha)!
                    self.setNeedsNavigationBackground(alpha: nowAlpha)
                    
                    self.navigationBar.tintColor = context.viewController(forKey: .to)?.navBarTintColor
                })
            }
        }
        
        public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
            if viewControllers.count >= (navigationBar.items?.count)! {
                //点击返回按钮
                let popToVC = viewControllers[viewControllers.count-2]
                setNeedsNavigationBackground(alpha: (popToVC.navBarBgAlpha))
                navigationBar.tintColor = popToVC.navBarTintColor
                
                _ = self.popViewController(animated: true)
            }
            
            return true
        }
        
        public func navigationBar(_ navigationBar: UINavigationBar, shouldPush item: UINavigationItem) -> Bool {
            //push到一个新界面
            setNeedsNavigationBackground(alpha: (topViewController?.navBarBgAlpha)!)
            navigationBar.tintColor = topViewController?.navBarTintColor
            return true
        }
        
    }
    

    好的,到这里,对返回和push操作的处理已经完成。

    releaseFingerRight.gif
    5、使用

    只需要在隐藏导航栏背景的viewController上把navBarBgAlpha设为0(或其他你需要的值)就可以了:

        override func viewDidLoad() {
            super.viewDidLoad()
            self.navBarBgAlpha = 0
            //other code
        }
    

    然后在比如tableView滚动到某个位置,需要显示导航栏时,把navBarBgAlpha设为1(或其他你需要的值)。

    6、其他

    要达到平滑的转场效果,还需要对navigationBar的tintColor进行类似的操作,这部分就留给大家自己看一下源码的相关部分啦。
    还有一些细节,比如状态栏颜色变化的时机,“preferredStatusBarStyle:”的调用链等,也交给大家去发现和思考了。

    相关文章

      网友评论

      • 阿飞_1217:核心方法是那几个呢
      • d82df298597e:我使用你的demo,在第一个控制器里设置了导航栏的背景图片,然后push到第二个控制器的时候背景图片还在,然后上下滑动背景图片才会消失
      • Go丶Pikachu:当设置导航栏为白色的时候在ios9下需要设置
        `if let adaptiveBackdrop = valueForKey("_adaptiveBackdrop") as? UIView , let backdropEffectView = adaptiveBackdrop.value(forKey: "_backdropEffectView") as? UIView {
        adaptiveBackdrop.alpha = alpha
        backdropEffectView.alpha = alpha
        return
        }`

        要把adaptiveBackdrop.alpha设置一下
      • Go丶Pikachu:问下大佬,怎么把导航栏变成白色的,不要苹果自带的那层模糊效果
      • SunshineBrother:亲,你的那个demo好像有一点问题。就是拖拽返回的时候,导航栏有问题,http://www.jianshu.com/p/ac6e6090ec9a
      • Pircate:如果present到一个带有导航栏的页面,并且navigationBar设置了backgroundImage,如果后面的页面设置navBarAlpha=0,导航栏就会变成黑色.
      • 布袋的世界:为何导航条有图标或者文字的时候,不能跟着 navBarBgAlpha=0 时一起透明呢?

        还有请问下楼主,当navBarBgAlpha为0时 如何一起改变状态栏的颜色
      • ChanJaWe:楼主,ios8.3好像无效,麻烦有空看下谢谢
      • cf1097cb1493:楼主你好,我使用cocoapods无法search你的ETNavBarTransparen
      • CnnJmh:楼主大大,什么时候能出oc版本的呢 ? 当开始滑动的时候会变成不透明的闪一下,这个是哪个步骤错了么 ? 万望解答。
        _悟了个空:@CnnJmh :stuck_out_tongue_winking_eye:
        CnnJmh:@菜大可 谢谢,已经点赞。:kissing_heart:
        _悟了个空:可以试一下它的OC版 https://github.com/90ck/CKNavSmoothDemo
      • 8b639bcd1bdc:请问怎么去掉毛玻璃效果,self.navigationController?.navigationBar.isTranslucent = false会变成黑溜溜
      • 41c3b9b74e46:好像无法更改其他颜色
        gary成:@日光镇 可是要是这样设置了,透明度的设置就会失效,怎么处理?
        日光镇:self.navigationController?.navigationBar.barTintColor = .red
      • deqiutseng:期待楼主大大OC版本
        _Erica:https://github.com/ac1217/KCNavigationTransition
      • 布袋的世界:强大的轮子
      • kelvin943:请问一下 楼主的 gif 图片怎么录制的
      • xxxixxxx:> 再看一下其他3家的处理方式,他们的处理方法基本一致,都是在进入个人页时隐藏了系统导航栏,然后添加一个自定义的导航栏,所以过度会比较生硬,与整体的返回效果有断层。

        我想知道他们是怎么做的。
        82295265a0fd:@拾荒少年v 非常感谢,但是我用的oc'的,还是不能用。
        xxxixxxx:@0号故事 这个第三方库可以 FDFullscreenPopGesture
        82295265a0fd:我也想知道,你有相相关代码或者博客吗
      • 前行哲:赞👍
      • Twenty_:我遇到一个非常奇怪的 问题 我把你的文件拖到我的一个旧项目中(OC +swift ),在Runtime 绑定 新属性的时候 会报一个 Extra argument in call的问题 导致,我不能编译, 但是我在我的新项目中把这个拖进去 会没有发生问题,我有点迷茫.
      • 谁动了我的梦:有oc 版本吗
        御雪飞斐:@会疼的白痴 仿的完全是两种,好不,跟楼主写的一点都不一样的
        会疼的白痴:这个OC版本还是会闪一下啊
        f0f92ae96e53:仿造大神写了个OC版的~ https://github.com/Cloudox/SmoothNavDemo
      • 我的月亮你的心: if viewControllers.count >= (navigationBar.items?.count)! {
        let popToVC = viewControllers[viewControllers.count-2]
        setNeedsNavigationBackground(alpha: (popToVC.navBarBgAlpha))
        navigationBar.tintColor = popToVC.navBarTintColor

        _ = self.popViewController(animated: true)
        }
        感觉有点莫名其妙,只能说你搞出来了,可以解释下 viewControllers.count >= (navigationBar.items?.count)!这个是为什么吗,不能说是试出来的吧!我觉得看你的文章不提几个疑问,有点对不起你啊!希望你能回答下
        日光镇:@我的月亮你的心 你运行你改后的代码,多push几层页面,再滑动返回,就知道什么意思的了。
        我的月亮你的心:@日光镇 我觉得你是为了判断数组是否越界,我改了下你的直接改成 viewControllers.count >= 2就行,感觉你的runtiom绑定的key有点怪。
        日光镇:这个判断是用来区分是点击返回还是滑动返回。涉及到NavigationController和NavigationBar的shouldPop方法的前后调用顺序,两种返回方式的调用顺序会不一样。
      • 我的月亮你的心:_updateInteractiveTransition:这个方法貌似系统没提供啊!在那找的
        我的月亮你的心:_shadowView这个从哪里找的,谢谢
      • 我的月亮你的心:_updateInteractiveTransition:
      • 施主小欣:期待楼主大大OC版本的
        御雪飞斐:@施主小欣 仿的完全是两种,好不,跟楼主写的一点都不一样的
        施主小欣:@Cloudox 万分感谢~回去研究~膜拜:+1:
        f0f92ae96e53:仿造大神写了个OC版的~ https://github.com/Cloudox/SmoothNavDemo
      • JohnnyHoo:泪求OC版本啊
        _悟了个空:按照大神的把它写成了OC版 https://github.com/90ck/CKNavSmoothDemo
        小星星吃KFC:@Cloudox 不可以啊,NextViewController 改成UITableViewController 添加一个 self.tableView.tableHeaderView 导航栏还是存在的~咋办?
        f0f92ae96e53:仿造大神写了个OC版的~ https://github.com/Cloudox/SmoothNavDemo
      • KennyMcCormick:6的飞起,大神求带
      • 张云龙:正在做的项目正好有这个需求,我找到了一种解决方案,但是会有些处女座受不了的小细节不完美。用你的试了试,当为navigationbar设置背景图片的时候,你这种方案就失效了。还有这种方案只支持navigationbar是半透明的,一旦设置为不透明的,你的方法同样失效。看看能否解决下。
        张云龙:@日光镇 我在MainViewController中设置“navigationController?.navigationBar.setBackgroundImage(UIImage.init(named: "navigationBarBackImage"), for: UIBarMetrics.default)”,push过去没有透明,滑动后会有半透明的效果,应该是以push过去就是全透明的吧?然后随着向上的滑动编程不是透明的。
        张云龙:@日光镇 好的,我试试。:blush:
        日光镇:下载最新的吧,已经兼容不透明的Bar和设成图片的Bar了。不过你要注意的是,前后两个VC的view都要填充整个屏幕,否则navigationBar后面是黑色的keyWindow,很难看。

        还有建议你可以把keyWindow设成白色:UIApplication.shared.keyWindow?.backgroundColor = .white
      • d9557f883fd8:为什么用 @selector()的方式去method swizzing。。。交换不了
      • 默默装作不知道:想问下 ~ 如果自定义返回按钮 不采用系统的按钮 设置leftBarButtonItem之后系统自带返回手势将会失效,有解决这个的问题的方法吗?
      • xiAo__Ju:翻译至美团的解决方案https://github.com/huangboju/KMNavigationBarTransition.swift
        日光镇:翻译何解?看了下代码,不是解决同一个问题
      • 杂雾无尘:期待第二篇的可以先参考下这篇
        http://www.jianshu.com/p/d21189d9224b
      • 張無忌:好像只能支持 iOS 10?跑在 iOS9 的 5s 上要报错。
        日光镇:已经添加对低版本的支持,之前没留意。
      • 5f58da0ef984:里面用到了iOS10的api 如果能标明一下就更好了
        日光镇:谢谢提醒,已经添加对低版本的支持了,之前没留意。
      • AppleTTT:多个UITableView共用一个tableHeader的效果实现 同期待这个,楼主大神

      本文标题:导航栏的平滑显示和隐藏 - 个人页的自我修养(1)

      本文链接:https://www.haomeiwen.com/subject/nqjcgttx.html