自定义交互过渡动画

Posted

技术标签:

【中文标题】自定义交互过渡动画【英文标题】:custom interactive transition animation 【发布时间】:2017-02-12 19:24:48 【问题描述】:

我想实现两个视图控制器之间的交互转换。我希望它是一个模态或现在的过渡。

我希望应用在第一个视图控制器上启动并允许用户向下滑动以引入第二个视图控制器 第二个视图控制器应该进来并覆盖当前(第一个视图控制器),而不是把它移开

我知道我需要使用以下内容。

过渡委托

animationController(forPresented:presenting:Source:)

interactionControllerForPresentation(使用:)

UIPercentDrivenInteractiveTransition

我无法弄清楚如何实现这一切。我似乎在 swift 3 中找不到任何有用或任何工作示例。现在我创建了一个简单的单视图应用程序,其中包含两个视图控制器 VC1(蓝色背景)和 VC2(黄色背景)来轻松测试任何可能的解决方案。

【问题讨论】:

【参考方案1】:

请参阅 WWDC 2013 视频 Custom Transitions Using View Controllers,了解有关过渡委托、动画控制器和交互控制器的讨论。观看 WWDC 2014 视频 View Controller Advancements in ios 8 和 A Look Inside Presentation Controllers 了解演示控制器(您也应该使用)。

基本思想是创建一个转换委托对象,该对象标识将用于自定义转换的动画控制器、交互控制器和演示控制器:

class TransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate 

    /// Interaction controller
    ///
    /// If gesture triggers transition, it will set will manage its own
    /// `UIPercentDrivenInteractiveTransition`, but it must set this
    /// reference to that interaction controller here, so that this
    /// knows whether it's interactive or not.

    weak var interactionController: UIPercentDrivenInteractiveTransition?

    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? 
        return PullDownAnimationController(transitionType: .presenting)
    

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? 
        return PullDownAnimationController(transitionType: .dismissing)
    

    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? 
        return PresentationController(presentedViewController: presented, presenting: presenting)
    

    func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? 
        return interactionController
    

    func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? 
        return interactionController
    


然后,您只需指定正在使用自定义转换以及应该使用什么转换委托。您可以在实例化目标视图控制器时执行此操作,也可以将其指定为目标视图控制器的 init 的一部分,例如:

class SecondViewController: UIViewController 

    let customTransitionDelegate = TransitioningDelegate()

    required init?(coder aDecoder: NSCoder) 
        super.init(coder: aDecoder)

        modalPresentationStyle = .custom
        transitioningDelegate = customTransitionDelegate
    

    ...

动画控制器指定动画的细节(如何制作动画、用于非交互式过渡的持续时间等):

class PullDownAnimationController: NSObject, UIViewControllerAnimatedTransitioning 

    enum TransitionType 
        case presenting
        case dismissing
    

    let transitionType: TransitionType

    init(transitionType: TransitionType) 
        self.transitionType = transitionType

        super.init()
    

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) 
        let inView   = transitionContext.containerView
        let toView   = transitionContext.view(forKey: .to)!
        let fromView = transitionContext.view(forKey: .from)!

        var frame = inView.bounds

        switch transitionType 
        case .presenting:
            frame.origin.y = -frame.size.height
            toView.frame = frame

            inView.addSubview(toView)
            UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: 
                toView.frame = inView.bounds
            , completion:  finished in
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            )
        case .dismissing:
            toView.frame = frame
            inView.insertSubview(toView, belowSubview: fromView)

            UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: 
                frame.origin.y = -frame.size.height
                fromView.frame = frame
            , completion:  finished in
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            )
        
    

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval 
        return 0.5
    

上面的动画控制器同时处理展示和关闭,但如果感觉太复杂,理论上你可以将它分成两个类,一个用于展示,另一个用于关闭。但我不喜欢两个不同的类如此紧密耦合,所以我将承担animateTransition 的轻微复杂性的成本,以确保它们都很好地封装在一个类中。

无论如何,我们想要的下一个对象是表示控制器。在这种情况下,表示控制器告诉我们从视图层次结构中删除原始视图控制器的视图。 (在这种情况下,我们这样做是因为您要过渡到的场景恰好占据了整个屏幕,因此无需将旧视图保留在视图层次结构中。)如果您要添加任何其他额外的 chrome(例如添加调暗/模糊视图等),属于演示控制器。

无论如何,在这种情况下,表示控制器非常简单:

class PresentationController: UIPresentationController 
    override var shouldRemovePresentersView: Bool  return true 

最后,您可能想要一个手势识别器:

实例化UIPercentDrivenInteractiveTransition; 自行启动转换; 随着手势的进行更新UIPercentDrivenInteractiveTransition; 手势完成时取消或完成交互过渡;和 完成后删除UIPercentDrivenInteractiveTransition(以确保它不会徘徊,因此它不会干扰您以后可能想要执行的任何非交互式转换......这是一个很容易忽略的微妙点)。

因此,“呈现”视图控制器可能有一个手势识别器,它可能会执行以下操作:

class ViewController: UIViewController 

    override func viewDidLoad() 
        super.viewDidLoad()

        let panDown = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
        view.addGestureRecognizer(panDown)
    

    var interactionController: UIPercentDrivenInteractiveTransition?

    // pan down transitions to next view controller

    func handleGesture(_ gesture: UIPanGestureRecognizer) 
        let translate = gesture.translation(in: gesture.view)
        let percent   = translate.y / gesture.view!.bounds.size.height

        if gesture.state == .began 
            let controller = storyboard!.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
            interactionController = UIPercentDrivenInteractiveTransition()
            controller.customTransitionDelegate.interactionController = interactionController

            show(controller, sender: self)
         else if gesture.state == .changed 
            interactionController?.update(percent)
         else if gesture.state == .ended || gesture.state == .cancelled 
            let velocity = gesture.velocity(in: gesture.view)
            if (percent > 0.5 && velocity.y == 0) || velocity.y > 0 
                interactionController?.finish()
             else 
                interactionController?.cancel()
            
            interactionController = nil
        
    


您可能还想更改此设置,使其仅识别向下的手势(而不是任何旧的平底锅),但希望这能说明这个想法。

您可能希望“呈现的”视图控制器具有用于关闭场景的手势识别器:

class SecondViewController: UIViewController 

    let customTransitionDelegate = TransitioningDelegate()

    required init?(coder aDecoder: NSCoder) 
        // as shown above
    

    override func viewDidLoad() 
        super.viewDidLoad()

        let panUp = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
        view.addGestureRecognizer(panUp)
    

    // pan up transitions back to the presenting view controller

    var interactionController: UIPercentDrivenInteractiveTransition?

    func handleGesture(_ gesture: UIPanGestureRecognizer) 
        let translate = gesture.translation(in: gesture.view)
        let percent   = -translate.y / gesture.view!.bounds.size.height

        if gesture.state == .began 
            interactionController = UIPercentDrivenInteractiveTransition()
            customTransitionDelegate.interactionController = interactionController

            dismiss(animated: true)
         else if gesture.state == .changed 
            interactionController?.update(percent)
         else if gesture.state == .ended 
            let velocity = gesture.velocity(in: gesture.view)
            if (percent > 0.5 && velocity.y == 0) || velocity.y < 0 
                interactionController?.finish()
             else 
                interactionController?.cancel()
            
            interactionController = nil
        

    

    @IBAction func didTapButton(_ sender: UIButton) 
        dismiss(animated: true)
    


有关上述代码的演示,请参阅https://github.com/robertmryan/SwiftCustomTransitions。

看起来像:

但是,归根结底,自定义过渡有点复杂,所以我再次向您推荐那些原始视频。在发布任何进一步的问题之前,请确保您仔细查看它们。这些视频可能会回答您的大部分问题。

【讨论】:

嘿,很好的答案,我在我的项目中实现了这个。几个问题 1. 如果我不使用自动布局,如何获得切换页面的按钮。 2. 有时如果我刷得太快,下面的屏幕会在一秒钟内显示为黑色 3. 有时如果我快速刷了多次,就会有不止一个视图控制器掉下来。 我建议您发布一个新问题,其中包含重现您描述的问题的最小示例。提供的代码很难诊断您的问题。 @Rob 我接着查看了您在 github 上创建的示例。它工作得很好,但是当我将 shouldRemovePresentersView 更改为 false 时会崩溃。我该如何解决这个问题。 参见noremove branch,我在其中添加了提交2e59d6d。但是,只有在呈现的视图没有完全遮挡呈现的视图时,您才会这样做。

以上是关于自定义交互过渡动画的主要内容,如果未能解决你的问题,请参考以下文章

iOS - 创建自定义过渡动画

iOS 7 仅在某些时候使用自定义交互过渡

popViewController Animated:自定义过渡动画?

UINavigationController 自定义过渡动画视图控制器 alpha 值

iOS:自定义后退按钮过渡动画

自定义 segue 过渡动画