導航欄的平滑顯示和隱藏 - 個人頁的自我修養(yǎng)(1)

本文是《個人頁的自我修養(yǎng)》系列文章的一篇,全部:

  • 導航欄的平滑顯示和隱藏 - 個人頁的自我修養(yǎng)(1) (本篇)
  • 多個UITableView共用一個tableHeader的效果實現(xiàn) - 個人頁的自我修養(yǎng)(2)(待補坑)
  • 處理Pan手勢和ScrollView滾動沖突 - 個人頁的自我修養(yǎng)(3)(待補坑)

關于“個人頁”

帶有社交屬性的APP一般都會有一個“個人頁”,用以展示某個用戶的個人信息和發(fā)布的內容。以下是幾個例子:

個人頁例子.png

以上頁面的共同特征是:
1、透明的導航欄以更好的展示背景圖
2、可按標簽切換到不同的內容頁(這個特性看需求,不一定有)
3、滾動時會停靠在頁面頂部的SegmentView
3、各個可滾動的內容頁共用一個header

最近剛好寫到Rabo微博客戶端的個人頁的部分,發(fā)現(xiàn)踩到幾個有意思的坑,解決下來決定寫個系列文章把相關解決方法和代碼分享一下。
先看一下要實現(xiàn)的整體效果:

overView.gif

這篇文章先處理導航欄的平滑隱藏和顯示。

導航欄的平滑顯示和隱藏

1、現(xiàn)有解決方案

先看一下手機QQ,是我目前能找到的處理得算比較好的導航欄返回效果。導航欄有跟隨返回手勢透明度漸變的動畫。


QQ返回.gif

但導航欄的返回交互動畫是自定義的,沒有系統(tǒng)自帶的視差效果和毛玻璃效果,而且中斷返回操作的話導航欄會閃一下,影響觀感。


QQ取消返回.gif

再看一下其他3家的處理方式,他們的處理方法基本一致,都是在進入個人頁時隱藏了系統(tǒng)導航欄,然后添加一個自定義的導航欄,所以過度會比較生硬,與整體的返回效果有斷層。

微博.gif

百度貼吧.gif

Twitter.gif

好,看完以上的例子,輪到我們來實現(xiàn)啦。我們今天的目標是不自定義導航欄,在系統(tǒng)自帶導航欄的基礎上進行非侵入(代碼解耦)的實現(xiàn)。先看效果:

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)
          }
      }
}

好的,現(xiàn)在可以根據(jù)需要隨時記錄下某個VC的導航欄透明度了,而不會因為push到下個頁面而丟失了這個信息。

3、設置導航欄背景的透明度

要實現(xiàn)上面demo的效果,我們不能修改整個導航欄的透明度,因為導航欄上的NavigationBarItem是需要保留下來的,如果設置整個導航欄的透明度,左右的Item和標題欄都會跟著一起透明了。

navItem.png

然而,系統(tǒng)API并沒有訪問背景View的接口,只好動用下黑魔法了。先看一下導航欄的層級:

navlevel.png

首先想到調整第一層_barBackgroundView(_UIBarBackground)的透明度,但試了一下,調整這一層級會丟失毛玻璃效果,效果很突兀:

bgAlphaErr.gif

經(jīng)過測試,調整_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、監(jiān)控返回手勢的進度

在手勢返回的交互中,如果前后兩個VC的導航欄透明度不一樣,需要根據(jù)手勢的進度實時調節(jié)透明度。
這里method swizzling一下,用UINavigationController的"_updateInteractiveTransition:"方法監(jiān)控返回交互動畫的進度。

//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

在手勢交互的過程中,透明度的變化跟預期一樣跟隨手勢變化。但一旦松手,系統(tǒng)會自動完成或取消返回操作,在這一過程中,以上的方法并沒有調用,而導致透明度停留在最后的那個狀態(tài)。
我們需要在UINavigationControllerDelegate中添加邊緣返回手勢松手時的監(jiān)控,還有要處理一下直接點擊返回按鈕和正常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 {
                //添加對返回交互的監(jiān)控
                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操作的處理已經(jīng)完成。

releaseFingerRight.gif
5、使用

只需要在隱藏導航欄背景的viewController上把navBarBgAlpha設為0(或其他你需要的值)就可以了:

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

然后在比如tableView滾動到某個位置,需要顯示導航欄時,把navBarBgAlpha設為1(或其他你需要的值)。

6、其他

要達到平滑的轉場效果,還需要對navigationBar的tintColor進行類似的操作,這部分就留給大家自己看一下源碼的相關部分啦。
還有一些細節(jié),比如狀態(tài)欄顏色變化的時機,“preferredStatusBarStyle:”的調用鏈等,也交給大家去發(fā)現(xiàn)和思考了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容