<strong id="eiuuk"></strong>
  • 歡迎光臨
    我們一直在努力

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

    建站超值云服務器,限時71元/月

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

    項目介紹:

    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)
    版權申明:本站文章部分自網絡,如有侵權,請聯系:west999com@outlook.com 特別注意:本站所有轉載文章言論不代表本站觀點! 本站所提供的圖片等素材,版權歸原作者所有,如需使用,請與原作者聯系。未經允許不得轉載:IDC資訊中心 » 使用Runtime優雅實現微信的手勢返回生成浮窗功能
    分享到: 更多 (0)

    評論 搶沙發

    • 昵稱 (必填)
    • 郵箱 (必填)
    • 網址
    青青草国产线观,在线播放费人成视频,色综合久久五月色婷婷,免费看成年人视频在线观看