手機版 | wap版 | 網站主頁 | HOME | 3G網頁
<button id="fgwvn"><acronym id="fgwvn"></acronym></button>

<dd id="fgwvn"></dd>
<button id="fgwvn"></button>
      1. <progress id="fgwvn"></progress>
        <tbody id="fgwvn"><track id="fgwvn"></track></tbody>
        <em id="fgwvn"><tr id="fgwvn"></tr></em>

        使用Runtime優雅實現微信的手勢返回生成浮窗功能

        [複製鏈接]
        來自: Rogue24 分類: iOS精品源碼 上傳時間: 2020-3-8 11:40:15
        Tag:

        項目介紹:

        Demo地址
        最終效果圖

        微信的手勢返回生成浮窗的效果,我感覺是微信自定義的手勢返回動畫,畢竟跟系統自帶的有些許差別,我之前也使用了高仿系統返回的自定義動畫來實現,實現起來比較麻煩,這裡介紹另一種更簡潔更方便的方案 ---- Runtime。

        手勢返回生成浮窗最主要是要獲取手勢返回的進度,通過這個進度控制右下角那個半圓的顯示,接著判斷鬆手時的那個點有沒有觸碰到這個半圓,如果沒有就正常返回或取消,如果觸碰到了就將控制器的View去執行一個浮窗生成的動畫,那就OJBK了。

        Runtime

        系統返回的pop動畫是一個轉場動畫,但UINavigationController沒有公開這個動畫相關的API,現在想要獲取手勢返回的進度,通過Runtime來看看UINavigationController的私有方法有沒有:

        // 查看類的方法列表
        var count: UInt32 = 0
        let methodList = class_copyMethodList(UINavigationController.self, &count)
        for i in 0 ..< count {
            let method = methodList![Int(i)]
            let name = sel_getName(method_getName(method))
            print(String(cString: name))
        }
        free(methodList)

        列印了一大堆方法,手勢轉場,方法名應該是帶有Interactive這個詞的,通過篩選有以下這3個方法挺符合的:

        _updateInteractiveTransition:
        _finishInteractiveTransition:transitionContext:
        _cancelInteractiveTransition:transitionContext:

        很明顯這3個就是手勢控制返回動畫的私有API。
        OK,知道了這些方法的存在,下一步再使用Runtime交換一下實現:

        extension UINavigationController {
            private static func jp_swizzlingForClass(originalSelector: Selector, swizzledSelector: Selector) {
                let originalMethod = class_getInstanceMethod(self, originalSelector)
                let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
                guard originalMethod != nil, swizzledMethod != nil else {
                    return
                }
                method_exchangeImplementations(originalMethod!, swizzledMethod!)
            }
        
            // 要在AppDelegate裡面執行一下這個方法
            static func jp_takeOnceTimeFunc() {
                jp_takeOnceTime
            }
            private static let jp_takeOnceTime: Void = {
                jp_swizzlingForClass(originalSelector: Selector(("_updateInteractiveTransition:")), swizzledSelector: #selector(jp_updateInteractiveTransition(percent:)))
                jp_swizzlingForClass(originalSelector: Selector(("_finishInteractiveTransition:transitionContext:")), swizzledSelector: #selector(jp_finishInteractiveTransition(percent:transitionContext:)))
                jp_swizzlingForClass(originalSelector: Selector(("_cancelInteractiveTransition:transitionContext:")), swizzledSelector: #selector(jp_cancelInteractiveTransition(percent:transitionContext:)))
            }()
        
            // 手勢控制的過程,percent:動畫進度
            @objc fileprivate func jp_updateInteractiveTransition(percent: CGFloat) {
                // 先執行一下原本的方法
                jp_updateInteractiveTransition(percent: percent) 
               
            }
            
            // 手勢停止,確定完成動畫,動畫繼續直到結束后的狀態
            @objc fileprivate func jp_finishInteractiveTransition(percent: CGFloat, transitionContext: UIViewControllerContextTransitioning) {
                // 先執行一下原本的方法
                jp_finishInteractiveTransition(percent: percent, transitionContext: transitionContext)
               
            }
            
            // 手勢停止,確定取消動畫,動畫往返回到開始前的狀態
            @objc fileprivate func jp_cancelInteractiveTransition(percent: CGFloat, transitionContext: UIViewControllerContextTransitioning) {
                // 先執行一下原本的方法
                jp_cancelInteractiveTransition(percent: percent, transitionContext: transitionContext)
                
            }
        }

        接下來得創建一個單例,用來管理右下角的判定半圓和需要生成浮窗的控制器。

        我這裡寫了JPFwAnimator這麼一個單例,先簡單說明一下:

        • JPFwAnimator.decideView:右下角的判定半圓,內部封裝了相關實現,只需要傳入動畫進度(percent)來控制顯示進度(showPersent),和手指在屏幕上的點(touchPoint)來判定在手指離開屏幕的時候是否生成浮窗(isTouching)

        • JPFwAnimator.shrinkFwVCpopViewController返回的控制器,就是要生成浮窗的那個控制器

        首先判定半圓decideView得在updateInteractiveTransition之前就添加到navigationController.view上,並且確定是通過手勢觸發的pop動畫才添加,可以交換一下popViewController方法在其裡面進行判斷,並更新一下其他方法:

        @objc fileprivate func jp_popViewController(animated: Bool) -> UIViewController? {
            JPFwAnimator.shrinkFwVC = self.topViewController // 保存一下要生成浮窗的VC
        
            // 如果pop手勢狀態是begin,說明是手勢返回
            if interactivePopGestureRecognizer?.state == .began {
                // 把判定半圓加上去
                view.addSubview(JPFwAnimator.decideView)
            } else {
                // 否則,就是通過點擊返回的,這裡就可以直接執行浮窗動畫了
            }
            // 調用原本的方法,開始pop動畫
            return jp_popViewController(animated: animated)
        }
        
        @objc fileprivate func jp_updateInteractiveTransition(percent: CGFloat) {
            jp_updateInteractiveTransition(percent: percent)
        
            let animator = JPFwAnimator
            guard animator.shrinkFwVC != nil else {
                return
            }
        
            animator.decideView.showPersent = percent * 2 // * 2 是為了滑到一半就顯示完整
            animator.decideView.touchPoint = interactivePopGestureRecognizer!.location(in: view) // 獲取手指的點,在內部判定是否在半圓的範圍內
        }
        
        @objc fileprivate func jp_finishInteractiveTransition(percent: CGFloat, transitionContext: UIViewControllerContextTransitioning) {
            jp_finishInteractiveTransition(percent: percent, transitionContext: transitionContext)
               
            let animator = JPFwAnimator
            guard animator.shrinkFwVC != nil, animator.isPush == false else {
                return
            }
        
            // 如果是碰到了
            if decideView.isTouching {
                // 執行浮窗動畫
            }
        
            // 隱藏判定半圓並移除
            decideView.decideDoneAnimation()
        }
            
        @objc fileprivate func jp_cancelInteractiveTransition(percent: CGFloat, transitionContext: UIViewControllerContextTransitioning) {
            jp_cancelInteractiveTransition(percent: percent, transitionContext: transitionContext)
        
            // 隱藏判定半圓並移除
            decideView.decideDoneAnimation()
        }

        判定半圓的觸碰效果

        這裡有一個注意的點,有時候即便是碰到了判定半圓,系統還是會執行cancelInteractiveTransition,這是因為手勢被取消了,例如在全屏系列的iPhone上滑到了下巴的時候就會取消這個手勢,可是我看微信,只要是碰到了就肯定會生成浮窗,所以微信很大可能是自定義的,不過這裡也是可以通過Runtime來修改。
        過場動畫是需要使用UIPercentDrivenInteractiveTransition這個類來控制的,上面那3個方法就是由這個類來調用的,又或者通過打斷點來查看:
        列印bt查看函數調用棧
        那就好辦了,交換UIPercentDrivenInteractiveTransition的取消方法的實現即可:

        extension UIPercentDrivenInteractiveTransition {
            // 要在AppDelegate裡面執行一下這個方法
            static func jp_takeOnceTimeFunc() {
                jp_takeOnceTime
            }
            private static let jp_takeOnceTime: Void = {
                jp_swizzlingForClass(originalSelector: #selector(cancel), swizzledSelector: #selector(jp_cancel))
            }()
            
            // 有時候已經滑到判定區域裡面,但還是會取消pop,這是系統自身的判斷(例如手指滑到了iPhoneX的下巴),這裡hook來自己判斷
            @objc fileprivate func jp_cancel() {
                guard JPFwAnimator.shrinkFwVC != nil else {
                    jp_cancel()
                    return
                }
                if JPFwAnimator.decideView.isTouching == true {
                    // 只要碰到了,強行finish,接著就會調用finishInteractiveTransition方法
                    finish()
                } else {
                    jp_cancel()
                }
            }
        }

        浮窗動畫

        現在知道動畫的進度和結束了,就剩這個浮窗動畫了。
        這個動畫不難,用maskView進行收縮,再把center設置為目標的點過去就好了。
        關鍵是系統的這個動畫無法停止,也就是說不能停住這個控制器去執行自己的動畫。
        只能自己寫一個做浮窗動畫的View,放上一張對控制器的view調用snapshotView獲取的截圖,然後就是設置浮窗動畫的初始位置,現在有了動畫的進度就可以知道了,percent可以當做這個控制器的view的x在屏幕的比例,接著就是放在navigationController.view上面,記得在動畫開始前對控制器的view進行隱藏,執行動畫。

        // 大概就醬紫,具體可查看Demo
        
        // 動畫初始位置
        let frame = CGRect(x: percent * shrinkFwVC.view.frame.width, y: shrinkFwVC.view.frame.origin.y, width: shrinkFwVC.view.frame.width, height: shrinkFwVC.view.frame.height)
         // 根據poping控制器的view的位置,創建浮窗對象
        let floatingWindow = JPFloatingWindow(frame: frame, floatingVC: shrinkFwVC)
        // 添加浮窗到當前容器視圖內,蓋住poping控制器的view
        navCtr.view.insertSubview(floatingWindow, belowSubview: navCtr.navigationBar)
        // 隱藏poping控制器的view
        fwView.isHidden = true
                
        // 搞個隨機點
        let randomPoint = CGPoint(x: CGFloat(arc4random_uniform(UInt32(jp_portraitScreenWidth_))), y: CGFloat(arc4random_uniform(UInt32(jp_portraitScreenHeight_))))
        // 開始浮窗動畫
        floatingWindow.shrinkFloatingWindowAnimation(floatingPoint: randomPoint) { (kFloatingWindow) in
            kFloatingWindow.removeFromSuperview()
            transitionContext?.completeTransition(true)
            // JPFwManager是管理浮窗的單例
            JPFwManager.floatingWindows.insert(kFloatingWindow, at: 0)
            JPFwManager.floatingWindowsHasDidChanged?(true, 0)
        }

        打開動畫跟浮窗動畫差不多,就是反過來的過程。

        最後

        做到這裡就跟微信的幾乎差不多了,不過微信上在pop的過程中導航欄有一些地方會有所不同:
        正常情況可以生成浮窗的情況
        可以看得出,微信應該是自定義的動畫,而且還是自定義的導航欄背景 ---- 動畫開始前先把導航欄背景放在底層控制器的view上。
        這是對控制器的其他處理,我在Demo裡面公開了相應的API,也做了相應的處理,具體可以去Demo看看:
        導航欄效果
        最終效果圖
        最後剩下的就是一些業務邏輯的處理(例如多個浮窗的管理、哪些控制器可以浮窗哪些不可以等等),並且得設置相關協議,以後Demo會完善這些功能並整合到一個新的庫。

        好了,要去搬磚了,先醬紫,Thx~

        Demo地址
        順帶以前寫的高仿版:高仿微信初版的網頁懸浮小窗口的小框架

        相關源碼推薦:

        我來說兩句
        所有評論(0)
        131 0 0
        聯繫我們
        首頁/微信公眾賬號投稿

        帖子代碼編輯/版權問題

        QQ:435399051,742864542

        如何獲得代碼達人稱號?

        代碼貢獻英雄榜
        用戶名 下載數
        通過郵件訂閱最新 Code4App 信息
        上一條 /4 下一條

        廣告投放| 台湾互聯網違法和不良信息舉報中心|中國互聯網舉報中心|Github|申請友鏈|手機版|Code4App ( 粵ICP備15117877號-1 )