美文网首页
使用Runtime优雅实现微信的手势返回生成浮窗功能

使用Runtime优雅实现微信的手势返回生成浮窗功能

作者: 健了个平_24 | 来源:发表于2020-03-06 10:59 被阅读0次

    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地址
    顺带以前写的高仿版:高仿微信初版的网页悬浮小窗口的小框架

    相关文章

      网友评论

          本文标题:使用Runtime优雅实现微信的手势返回生成浮窗功能

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