天天看點

Swift -- 仿今日頭條轉場效果 (二)

上回說到直接的pop和push效果已經有了,沒有随手勢滑動pop時的過渡效果,這篇繼續記錄說明。

//pop手勢百分比
    func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        
        if self.transitionAnimation.animationType == .pop {
            let gestureMove = isInteractive == true ? self.transitionAnimation : nil
            return gestureMove
        }
        return nil
    }
           

還記得導航控制器的這個代理方法吧,它就是處理手勢的時候要用的。

首先添加手勢,

//添加側滑手勢
    private func addPanGestureAction() {
        let ges = UIPanGestureRecognizer(target: self, action: #selector(edgTapAction))
        self.view.addGestureRecognizer(ges)
    }
           

實作手勢事件:

//MARK: add tap action
extension BaseNaviViewController {
    //側滑事件
    @objc func edgTapAction(ges:UIPanGestureRecognizer) {
        //找到目前點
        let translation = ges.translation(in: ges.view)
        percentComplete = abs(translation.x/kWidth)
        //滑動比例
        percentComplete = min(max(percentComplete, 0.01), 0.99)
        if translation.x < 0 {  //手勢左滑的狀态相當于滑動比例為0,
            percentComplete = 0.0
        }
        
        switch ges.state {
        case .began:
            isInteractive = true
            let currentVCArray = self.viewControllers
            if (currentVCArray.count) > 1 {
                self.popViewController(animated: true)
            }
        case .changed:
            self.transitionAnimation.update(CGFloat(percentComplete))
        case .ended,.cancelled:
            isInteractive = false
            ges.isEnabled = false
            if percentComplete >= 0.5 {
                self.transitionAnimation.finish {
                    ges.isEnabled = true  //響應完成方可操作
                }
            } else {
                self.transitionAnimation.cancel {
                    ges.isEnabled = true //響應完成方可操作
                }
            }
        default:
            break
        }
    }
}
           

isInteractive作為一個标記,記錄手勢的開始和結束,并在導航欄代理裡一同作為是否進入自定義pop手勢的依據。

//pop手勢百分比
    func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        
        if self.transitionAnimation.animationType == .pop {
            let gestureMove = isInteractive == true ? self.transitionAnimation : nil
            return gestureMove
        }
        return nil
    }
           

transitionAnimation執行個體裡實作以下方法以響應手勢事件:

//檢測到右手勢操作就會走這個方法,直接push或pop則不會
//在此方法裡對fromVC和toVC做轉場動畫前的準備工作
override func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
        
        let containerView = transitionContext.containerView
        guard let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else { return }
        
        storedContext = transitionContext
        containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)
        
        fromVC = fromViewController
        toVC = toViewController
        
        self.toView = toViewController.view
        self.fromView = fromViewController.view
        self.toView.transform = CGAffineTransform(scaleX: default_scale, y: default_scale)
        
    }
           

 由前一篇的對UIPercentDrivenInteractiveTransition分析了解到,重寫進度更新方法

//更新進度的方法,根據目前進度百分比,對正在過渡的視圖進行處理
override func update(_ percentComplete: CGFloat) {
        if storedContext == nil {
            return
        }
        guard self.toView != nil,self.fromView != nil else {
            return
        }
        self.fromView?.frame = CGRect(x: kWidth * percentComplete, y: 0, width: kWidth, height: kHeight)
        let gap_scale = (1 - default_scale) * percentComplete
        let scale = default_scale + gap_scale
        self.toView.transform = CGAffineTransform(scaleX: scale, y: scale)
        storedContext?.updateInteractiveTransition(percentComplete)
    }
           
func cancel(completedClosure:@ escaping ()->()) {
        
        guard self.toView != nil,self.fromView != nil else {
            completedClosure()
            return
        }
        
        UIView.animate(withDuration: 0.2, animations: {
            self.fromView?.frame = CGRect(x: 0, y: 0, width: kWidth, height: kHeight)
            self.toView.transform = CGAffineTransform(scaleX: default_scale, y: default_scale)
        }) { (completed) in
            self.storedContext?.cancelInteractiveTransition()
            self.storedContext?.completeTransition(false)
            self.storedContext = nil
            self.toView = nil
            self.fromView = nil
            self.toVC.view.transform = .identity
            completedClosure()
        }
    }
    
    func finish(completedClosure:@ escaping ()->()) {
        
        guard self.toView != nil,self.fromView != nil else {
            completedClosure()
            return
        }
        
        UIView.animate(withDuration: 0.2, animations: {
            self.fromView?.frame = CGRect(x: kWidth, y: 0, width: kWidth, height: kHeight)
            self.toView.transform = CGAffineTransform.init(scaleX: 1.0, y: 1.0)
        }) { (completed) in
            self.storedContext?.finishInteractiveTransition()
            self.storedContext?.completeTransition(true)
            self.storedContext = nil
            self.toView = nil
            self.fromView = nil
            completedClosure()
        }
    }
           

這裡的進度取消和完成我沒有進行重寫而是自己寫了個方法,原因是在手勢滑動過程中,很可能出現UIView動畫還未完成,滑動手勢還在觸發update更新方法,導緻toVC.view一直在後面躲來躲去,是以這裡寫了一個閉包在取消/完成操作結束後回調給手勢,手勢isEnabled屬性解鎖為true。

其中self.storedContext?.cancelInteractiveTransition()  self.storedContext?.finishInteractiveTransition()作用相當于調用了取消和完成操作,不用重寫父類方法也可以。

到這裡仿今日頭條轉場效果就結束了。水準有限,如果你看完我寫的文章有不同意見或建議,歡迎留言一起讨論。

Github位址