歡迎光臨
我們一直在努力

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

容器云強勢上線!快速搭建集群,上萬Linux鏡像隨意使用

使用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)

評論 搶沙發

  • 昵稱 (必填)
  • 郵箱 (必填)
  • 網址
色情影院 日本电影