iOS仿微信的懸浮窗實(shí)現(xiàn)真慢,自定義轉(zhuǎn)場(chǎng)動(dòng)畫(huà),使用超級(jí)簡(jiǎn)單

先看看效果圖

screenshot1.gif
screenshot2.gif

demo在這里边涕。

代碼結(jié)構(gòu)

代碼結(jié)構(gòu).jpg
  • HXSuspendViewManager是一個(gè)單例,負(fù)責(zé)主要的邏輯褂微,控制懸浮窗和扇形view的生命周期功蜓、展示和隱藏。
  • HXSuspendViewController是一個(gè)協(xié)議宠蚂,只要你的控制器遵守了這個(gè)協(xié)議式撼,你的控制器就可以添加到懸浮窗中。
  • UINavigationController+HXSuspend是UINavigationController的分類(lèi)求厕,懸浮窗相關(guān)的處理邏輯都在這里著隆。
  • HXCircleTransition是自定義轉(zhuǎn)場(chǎng)動(dòng)畫(huà)類(lèi)
  • HXSuspendWindow懸浮窗的視圖,繼承自UIWindow
  • HXCircularSectorView右下角的扇形view

實(shí)現(xiàn)原理

  • 攔截UINavigationController的右滑返回手勢(shì)呀癣,判斷是否顯示右下角的扇形view美浦,主要包括三個(gè)方法,這三個(gè)方法都通過(guò)runtime交換了方法實(shí)現(xiàn)
    open override func viewDidLoad() {
        super.viewDidLoad()
        UINavigationController.initializeSuspendOnce()
        interactivePopGestureRecognizer?.delegate = self
        delegate = self
    }
    
    private static let onceToken = UUID().uuidString
    private static func initializeSuspendOnce() {
        guard self == UINavigationController.self else { return }
        DispatchQueue.hx_once(onceToken) {
            let needSwizzleSelectorArr = [
                NSSelectorFromString("_updateInteractiveTransition:"),
                NSSelectorFromString("_finishInteractiveTransition:transitionContext:"),
                NSSelectorFromString("_cancelInteractiveTransition:transitionContext:"),
                NSSelectorFromString("popViewControllerAnimated:"),
                NSSelectorFromString("popToRootViewControllerAnimated:"),
                NSSelectorFromString("popToViewController:animated:")
            ]
            for selector in needSwizzleSelectorArr {
                let newSelector = ("hx_" + selector.description).replacingOccurrences(of: "__", with: "_")
                let originalMethod = class_getInstanceMethod(self, selector)
                let swizzledMethod = class_getInstanceMethod(self, Selector(newSelector))
                if originalMethod != nil && swizzledMethod != nil {
                    method_exchangeImplementations(originalMethod!, swizzledMethod!)
                }
            }
        }
    }

滑動(dòng)中:hx_updateInteractiveTransition:
滑動(dòng)結(jié)束项栏,并完成pop:hx_finishInteractiveTransition:transitionContext:
滑動(dòng)結(jié)束浦辨,取消了pop:hx_cancelInteractiveTransition:transitionContext:
具體實(shí)現(xiàn)如下:

     @objc func hx_updateInteractiveTransition(_ percentComplete: CGFloat) {
        hx_updateInteractiveTransition(percentComplete)
        guard let poppingVC = hx_poppingVC as? HXSuspendViewController,
            let keyWindow = UIApplication.shared.keyWindow,
            let point = interactivePopGestureRecognizer?.location(in: keyWindow) else { return }
        /// 添加右下角扇形view
        if HXSuspendViewManager.shared.circularSectorView.superview == nil {
            keyWindow.addSubview(HXSuspendViewManager.shared.circularSectorView)
        }
        /// 如果是新的控制器,顯示扇形沼沈,否則顯示懸浮窗
        if poppingVC.suspendIdentifier != HXSuspendViewManager.shared.suspendWindow?.viewContoller?.suspendIdentifier {
            HXSuspendViewManager.shared.circularSectorView.type = .add
            HXSuspendViewManager.shared.circularSectorView.show(percent: percentComplete)
            HXSuspendViewManager.shared.circularSectorView.move(point: point)
        } else {
            HXSuspendViewManager.shared.changeSuspendViewAlpha(percentComplete, animated: false)
        }
    }
    
    @objc func hx_finishInteractiveTransition(_ percentComplete: CGFloat, transitionContext: UIViewControllerContextTransitioning)  {
        hx_finishInteractiveTransition(percentComplete, transitionContext: transitionContext)
        /// 保證最后一定調(diào)用隱藏扇形view
        defer {
            HXSuspendViewManager.shared.circularSectorView.hide()
        }
        guard let poppingVC = hx_poppingVC as? HXSuspendViewController,
            let keyWindow = UIApplication.shared.keyWindow,
            let point = interactivePopGestureRecognizer?.location(in: keyWindow) else { return }
        if poppingVC.suspendIdentifier != HXSuspendViewManager.shared.suspendWindow?.viewContoller?.suspendIdentifier {
            /// 添加新的懸浮窗
            if HXSuspendViewManager.shared.circularSectorView.isPointInView(point: point) {
                HXSuspendViewManager.shared.addSuspendView(viewController: poppingVC, percent: percentComplete)
            }
        } else {
            // 播放一個(gè)假的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)
            HXSuspendViewManager.shared.fakeTransitionAnimation(percentComplete)
        }
    }
    
    @objc func hx_cancelInteractiveTransition(_ percentComplete: CGFloat, transitionContext: UIViewControllerContextTransitioning) {
        hx_cancelInteractiveTransition(percentComplete, transitionContext: transitionContext)
        defer {
            HXSuspendViewManager.shared.circularSectorView.hide()
        }
        guard let poppingVC = hx_poppingVC as? HXSuspendViewController else { return }
        if poppingVC.suspendIdentifier == HXSuspendViewManager.shared.suspendWindow?.viewContoller?.suspendIdentifier  {
            HXSuspendViewManager.shared.changeSuspendViewAlpha(0, animated: false)
        } else {
            HXSuspendViewManager.shared.changeSuspendViewAlpha(1, animated: false)
        }
    }
  • 實(shí)現(xiàn)自定義的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)流酬,通過(guò)UINavigationControllerDelegate代理實(shí)現(xiàn)
// MARK: -  UINavigationControllerDelegate
extension UINavigationController: UINavigationControllerDelegate {
    
    public func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        hx_poppingVC = nil
    }
    
    public func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        guard let suspendWindow = HXSuspendViewManager.shared.suspendWindow,
            let currentSuspendVC = suspendWindow.viewContoller else { return nil }
        switch operation {
        case .push:
            // 保證是suspendWindow所持有的viewController
            guard let suspendToVC = toVC as? HXSuspendViewController,
                suspendToVC.suspendIdentifier == currentSuspendVC.suspendIdentifier else { return nil }
            return HXCircleTransition(operationType: .push, originPoint: suspendWindow.center)
        case .pop:
            guard let suspendFromVC = fromVC as? HXSuspendViewController,
                suspendFromVC.suspendIdentifier == currentSuspendVC.suspendIdentifier else { return nil }
            return HXCircleTransition(operationType: .pop, originPoint: suspendWindow.center)
        default:
            return nil
        }
    }
    
}

其他細(xì)節(jié)

  • 懸浮窗的拖動(dòng)處理
     @objc private func didPan(gesture: UIPanGestureRecognizer) {
        let point = gesture.location(in: UIApplication.shared.keyWindow)
        switch gesture.state {
        case .began:
            panStartPoint = point
            panStartCenter = center
            HXSuspendViewManager.shared.circularSectorView.type = .delete
            HXSuspendViewManager.shared.circularSectorView.show()
        case .changed:
            let panDeltaX = point.x - panStartPoint.x
            let panDeltaY = point.y - panStartPoint.y
            let centerX = min(max(panStartCenter.x + panDeltaX, bounds.width / 2), hx_screenWidth - bounds.width / 2)
            let centerY = min(max(panStartCenter.y + panDeltaY, bounds.height / 2), hx_screenHeight - bounds.height / 2 )
            center = CGPoint(x: centerX, y: centerY)
            HXSuspendViewManager.shared.circularSectorView.move(point: center)
        default:
            if HXSuspendViewManager.shared.circularSectorView.isPointInView(point: center) {
                HXSuspendViewManager.shared.removeSuspendView()
            } else {
                // 保證懸浮窗在安全范圍之內(nèi)
                let centerX = min(max(center.x, bounds.width / 2 + 10), hx_screenWidth - bounds.width / 2 - 10)
                let centerY = min(max(center.y, bounds.height / 2 + hx_statusBarHeight), hx_screenHeight - bounds.height / 2 - hx_safeBottomHeight)
                UIView.animate(withDuration: 0.2) {
                    self.center = CGPoint(x: centerX, y: centerY)
                }
            }
            HXSuspendViewManager.shared.circularSectorView.hide()
        }
    }
  • 轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的具體實(shí)現(xiàn),pop的實(shí)現(xiàn)同理
    private func pushAnimation(transitionContext: UIViewControllerContextTransitioning) {
        guard let fromVC = transitionContext.viewController(forKey: .from),
            let toVC = transitionContext.viewController(forKey: .to) else {
                completeTransition(transitionContext: transitionContext)
                HXSuspendViewManager.shared.changeSuspendViewAlpha(0, animated: false)
                return
        }
        // 添加到containerView中
        let containerView = transitionContext.containerView
        containerView.addSubview(fromVC.view)
        containerView.addSubview(toVC.view)
        // 計(jì)算path
        let originSize = HXSuspendViewConfig.suspendViewSize
        let originFrame = CGRect(x: originPoint.x - originSize.width / 2, y: originPoint.y - originSize.height / 2, width: originSize.width, height: originSize.height)
        let beginPath = UIBezierPath(ovalIn: originFrame)
        let finalRadius = HXCircleTransition.radius(with: originPoint)
        let finalPath = UIBezierPath(ovalIn: originFrame.insetBy(dx: -finalRadius, dy: -finalRadius))
        let maskLayer = CAShapeLayer()
        maskLayer.path = finalPath.cgPath
        toVC.view.layer.mask = maskLayer
        // 開(kāi)始動(dòng)畫(huà)
        let animation = CABasicAnimation(keyPath: "path")
        animation.fromValue = beginPath.cgPath
        animation.toValue = finalPath.cgPath
        animation.duration = transitionDuration(using: transitionContext)
        animation.delegate = self
        maskLayer.add(animation, forKey: "path")
        // 改變懸浮窗alpha
        HXSuspendViewManager.shared.changeSuspendViewAlpha(0, animated: true)
    }

使用方法

超級(jí)簡(jiǎn)單的使用方法列另,完全無(wú)侵入芽腾。


class VipcnViewController: UIViewController, HXSuspendViewController {
    
    // MARK: -  HXSuspendViewController
    var suspendIdentifier: Int {
        // 保證suspendIdentifier唯一
        return hashValue
    }
    
    var suspendIcon: UIImage? {
        return UIImage(named: "2")
    }

}

就是這么簡(jiǎn)單,只需要讓你的控制器遵守HXSuspendViewController協(xié)議就行了页衙。

總結(jié)

iOS仿微信的懸浮窗摊滔,自定義轉(zhuǎn)場(chǎng)動(dòng)畫(huà),集成超級(jí)簡(jiǎn)單。
如果覺(jué)得對(duì)你有幫助惭载,請(qǐng)給個(gè)star旱函,demo在這里。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末描滔,一起剝皮案震驚了整個(gè)濱河市棒妨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌含长,老刑警劉巖券腔,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異拘泞,居然都是意外死亡纷纫,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)陪腌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)辱魁,“玉大人,你說(shuō)我怎么就攤上這事诗鸭∪敬兀” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵强岸,是天一觀的道長(zhǎng)锻弓。 經(jīng)常有香客問(wèn)我,道長(zhǎng)蝌箍,這世上最難降的妖魔是什么青灼? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮妓盲,結(jié)果婚禮上杂拨,老公的妹妹穿的比我還像新娘。我一直安慰自己悯衬,他們只是感情好扳躬,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著甚亭,像睡著了一般贷币。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上亏狰,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天役纹,我揣著相機(jī)與錄音,去河邊找鬼暇唾。 笑死促脉,一個(gè)胖子當(dāng)著我的面吹牛辰斋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播瘸味,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼宫仗,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了旁仿?” 一聲冷哼從身側(cè)響起藕夫,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎枯冈,沒(méi)想到半個(gè)月后毅贮,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡尘奏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年滩褥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炫加。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瑰煎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出俗孝,到底是詐尸還是另有隱情酒甸,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布驹针,位于F島的核電站烘挫,受9級(jí)特大地震影響诀艰,放射性物質(zhì)發(fā)生泄漏柬甥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一其垄、第九天 我趴在偏房一處隱蔽的房頂上張望苛蒲。 院中可真熱鬧,春花似錦绿满、人聲如沸臂外。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)漏健。三九已至,卻和暖如春橘霎,著一層夾襖步出監(jiān)牢的瞬間蔫浆,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工姐叁, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瓦盛,地道東北人洗显。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像原环,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子嘱吗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容