iOS自定义过渡动画

历时5天从各种英文教程中学习到的过渡动画,是一个很难忘的探索经历

比较好的参考文章自定义UIViewController过渡入门动画入门

转场方式

首先让我们来了解iOS转场的方式:

  • UINavigationController push/pop UIViewController导航栏的转场
  • UITabBarController 切换Tab的转场
  • present/dismiss 模态的方式转场

这是iOS提供的3种基本转场方式,默认的转场方式转场风格有限。例如模态转场中,尽管有modalPresentationStylemodalTransitionStyle关于展现风格和过渡风格的设置,但是转场仍是死板从底部弹出。这并不能满足我们在软件开发的需求,侧边栏、顶部栏的动画效果都无法很好地实现。在iOS 7.0之后Apple提供一套完整的自定义过渡动画的API,为各种转场动画的实现带了无限可能。

这里主要介绍模态转场的自定义动画。
顶部栏的动画效果:

SlideMune 的源码

Modal 转场

模态转场分为非交互式转场交互式转场
非交互式转场也就是普通转场,转场的动画无法交互,不能在动画的过程中终止转场。
交互式转场能通过手指触摸屏幕通过滑动体验过渡动画的进行,并能终止动画过程。

在这里插入图片描述

过渡动画API

我们定义:如果视图控制器Apresent之后展示视图控制器B。在后文中,源视图控制器fromVC目标视图控制器toVC

状态 视图控制器A 视图控制器B
Present 源视图控制器 目标视图控制器
Dissmiss 目标视图控制器 源视图控制器

在这里插入图片描述

transitioningDelegate 过渡代理

每个视图控制器UIViewController都有一个transitioningDelegate属性,该代理需遵循UIViewControllerTransitioningDelegate协议,提供相关动画控制器。

每当您显示或关闭视图控制器时,UIKit都会要求其过渡代理使用动画控制器。要将默认动画替换为您自己的自定义动画,必须实现过渡代理,并使其返回适当的动画控制器。

AnimationController 动画控制器

过渡代理在present/dismiss时返回相应的动画控制器。动画控制器是过渡动画的核心。它完成了动画过渡的“繁重工作”。

TransitioningContext 过渡语境

过渡语境在过渡过程中实现并起着至关重要的作用:它封装了有关过渡中涉及的视图和视图控制器的信息。过渡语境辅助动画控制器实现动画。
从图中可以看出,自己并没有实现此协议。UIKit会为您创建和配置过渡上下文,并在每次发生过渡时将其传递给动画控制器。

非交互式过渡动画的过程

以present过渡动画为例:

  1. 通过代码或StoryBoard segue触发模态视图present过程。
  2. UIKit向toVC(目标视图控制器)请求其过渡代理。如果没有,UIKIt将使用标准的内置过渡。
  3. 然后,UIKit通过来向过渡代理请求动画控制器animationController(forPresented:presenting:source:)。如果返回nil,则过渡将使用默认动画。
  4. UIKit构造过渡语境。UIKit通过调用向动画控制器询问动画的持续时间transitionDuration(using:)。UIKit animateTransition(using:)在动画控制器上调用以执行过渡的动画。
  5. 最后,动画控制器调用completeTransition(_:)过渡上下文以指示动画已完成。

dimiss过渡动画的步骤几乎相同。
在这种情况下,UIKit向fromVC视图控制器(正在关闭的视图控制器)请求其过渡代理,要求提供动画控制器animationController(forDismissed:)

非交互式过渡动画需要提供的条件

  1. 设置过渡动画代理。设置(目标视图控制器)的transitioningDelegate属性,即设置过渡动画代理对象,该代理对象遵循UIViewControllerTransitioningDelegate协议,实现forPresentedforDismissed两个方法,分别提供present和dismiss的视图控制器实例。
  2. 创建动画控制器。创建present和dismiss的动画控制器,实现动画持续时间和构造动画方法。

这里我们完成一个简单的从左到右的过渡动画。

1. 构建present/dimiss场景

这里不再讲述构建过程,无论是stroyboard还是纯代码都是很好完成的。这里为了方便,使用storyboard完成。
左视图控制器为:ViewController
右视图控制器为:LeftViewController

2. 创建Animation Controller

Animation Controller是自定义过渡动画的核心对象
AnimationControlle继承至NSObject,遵循UIViewControllerAnimatedTransitioning协议,实现两个required方法。

class AnimationController: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        //要求提供动画师对象的动画持续时间属性
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        //过渡动画的实现效果,也是自定义过渡动画的核心方法,需要构建动画的实现。
    }
}

这里贴出Present状态下的AnimationController的代码,Dismiss状态与其类似(动画过程执行反过程)。

import UIKit

class PresentAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.6
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        // 1 
        guard let _ = transitionContext.viewController(forKey: .from),
            let toVC = transitionContext.viewController(forKey: .to) else {
                return
        }
        
        // 2
        let containerView = transitionContext.containerView
        containerView.addSubview(toVC.view)
        let duration = transitionDuration(using: transitionContext)
        toVC.view.frame = CGRect(x: -ScreenWidth, y: 0, width: ScreenWidth, height: ScrennHeight)
        
        // 3
        UIView.animate(withDuration: duration, animations: {
            toVC.view.frame = CGRect(x: 0, y: 0, width: ScreenWidth, height: ScrennHeight)
        }) { (_) in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }
        
    }
    
}

在第一个transitionDuration(using:)方法中设置动画的持续时间。

在第二个transitionDuration(using:)方法中构建动画。

  1. 获取过渡动画所需的视图控制器及snapshot。从过渡语境中transitionContext.viewController我们可以获取源视图控制器fromVC目标视图控制器toVC,过渡语境封装了设计的视图控制器的信息,极大地帮助我们处理视图控制器的动画转化。还可以获取fromVC和toVC的snapshot(屏幕快照),来构造更加复杂和优秀的动画。
  2. 管理过渡语境的容器视图 — containerView和视图动画的位置初始化。UIKit将整个过渡封装在容器视图中,以简化视图层次结构和动画的管理,容器视图负责管理fromVC.viewtoVC.view。由UIKit创建的容器视图仅包含fromVC视图。您必须添加任何其他将参与过渡的视图。

addSubview(_:)将新视图置于视图层次结构中的所有其他视图之前,因此添加子视图的顺序很重要。

  1. 设置动画效果。动画有两种实现方法,一种是基础动画animate,另一种是关键帧动画animateKeyframes。这里只是简单地将fromVC.view从屏幕的左边界外移动到屏幕中。注意:在动画完成后需要调用completeTransition(_:)通知UIKit动画已完成。这将确保最终状态是一致的。

3.设置过渡动画代理

设置(目标视图控制器)destinationVC的modalPresentationStyle为枚举属性custom,过渡动画的代理为self即(源视图控制器)ViewController。

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let destinationVC = segue.destination as? LeftViewController {
            destinationVC.modalPresentationStyle = .custom
            destinationVC.transitioningDelegate = self
        }
    }

然后在(源视图控制器)添加扩展,遵循UIViewControllerTransitioningDelegate协议,实现forPresentedforDismissed方法。

extension ViewController: UIViewControllerTransitioningDelegate {
    
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        //目标VC是presented,源VC是presenting
        guard let _ = presented as? LeftViewController, let _ = presenting as? ViewController else {
            return nil
        }
        return PresentAnimationController()
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        guard let _ = dismissed as? LeftViewController else {
            return nil
        }
        return DismissAnimationController()
    }
}

这样就构建好了一个简单、基础的自定义转场过渡动画。

交互式过渡动画

交互式动画会使使用者的动画体验更自然舒适而不显得尖锐,给控制动画过程留下了余地。iOS原生APP设置中便有这样的交互式动画的例子。

过渡动画的进度跟随手指的滑动来启动/终止转场动画,这样可以带来良好的用户交互体验。

交互式控制器的工作方式

交互控制器通过加快,减慢甚至反转过渡的过程来响应触摸事件或编程输入。为了启用交互式转换,转换代理必须提供一个交互控制器。您已经制作了过渡动画。在过渡动画的基础上,Apple将交互式动画封装成交互控制器将响应手势来管理此动画,而不是让其像视频一样播放。Apple提供了现成的UIPercentDrivenInteractiveTransition类,通过继承该类来创建自己的交互式控制器。

1. 创建交互式控制器

我们在之前过渡动画的基础上构建交互式过渡动画,我们首先需要创建交互式控制器。这里贴出显示交互式控制器PresentInteractionController的代码,继承封装好的UIPercentDrivenInteractiveTransition类。

class PresentInteractionController: UIPercentDrivenInteractiveTransition {
    var interactionInProgress = false
    private var shouldCompleteTrantision = false
    private weak var viewController: UIViewController!
    private weak var toViewController: UIViewController!
    
    init(viewController: UIViewController, toViewController: UIViewController) {
        super.init()
        self.viewController = viewController
        self.toViewController = toViewController
        prepareGestureRecognizer(in: self.viewController.view)
    }
    
    private func prepareGestureRecognizer(in view: UIView) {
        let gesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleGsture(_:)))
        gesture.edges = .left
        view.addGestureRecognizer(gesture)
    }
    
    @objc func handleGsture(_ gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
        let translation = gestureRecognizer.translation(in: gestureRecognizer.view?.superview)
        var progress = translation.x / 200
        progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))
        
        switch gestureRecognizer.state {
        case .began:
            interactionInProgress = true
            viewController.present(toViewController, animated: true, completion: nil)
        case .changed:
            shouldCompleteTrantision = progress > 0.5
            update(progress)
        case .cancelled:
            interactionInProgress = false
            cancel()
        case .ended:
            interactionInProgress = false
            if shouldCompleteTrantision {
                finish()
            } else {
                cancel()
            }
        default:
            break
        }
    }
}
  • interactionInProgressBool属性,表示交互式场景是否在发生。
  • shouldCompleteTrantisionBool属性,表示是否应该终止过渡动画。用于内部管理过渡。
  • viewControllertoViewController,获取源视图控制器和目标视图控制器的引用,用于管理过渡某状态present视图控制器,达到交互式控制器与动画控制器相联系的作用。
  • prepareGestureRecognizer(in:)为源视图添加屏幕手势的方法,这里的交互式动画为从左往右present出VC,所以为源视图添加屏幕相应在.left的手势。
  • handleGsture(_:)为相应手势变化从而改变过渡动画状态的方法。通过声明局部变量以跟踪滑动进度translation,根据translation在视图中获取并计算过渡进度progress。手势开始时,您将设置interactionInProgresstrue并触发prsent视图控制器。手势移动时,调用update(_:)更新过渡进度。这是一种根据UIPercentDrivenInteractiveTransition您传入的百分比移动过渡的方法。如果取消手势,则更新interactionInProgress并取消过渡。一旦动作已经结束,您使用的过渡进度来决定cancel()finish()

2. 控制器联系

(源)视图控制器与交互控制器相联

viewController中添加以下属性:

var presentInteractionController: PresentInteractionController?

并在viewDidLoad()方法中初始化属性:

presentInteractionController = PresentInteractionController(viewController: self, toViewController: leftViewController)

交互控制器与动画控制器相联

PresentAnimationController中添加以下属性:

let interactionController: PresentInteractionController?

并添加init方法:

init(interactionController: PresentInteractionController?) {
    self.interactionController = interactionController
}

3. 实现过渡代理协议方法

动画控制器forPresented方法中修改PresentAnimationController对象的创建。

func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        //展现VC是presented,源VC是presenting
        guard let _ = presented as? LeftViewController, let fromVC = presenting as? ViewController else {
            return nil
        }
        return PresentAnimationController(interactionController: fromVC.presentInteractionController)
    }

添加以下方法:

func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        guard let animator = animator as? PresentAnimationController,
            let interactionController = animator.interactionController,
            interactionController.interactionInProgress else {
                return nil
        }
        return interactionController
    }

这首先检查所涉及的动画控制器是否为PresentAnimationController。如果是这样,它将获取关联的交互控制器的引用,并验证用户交互正在进行中。如果不满足这些条件中的任何一个,它将返回nil以便转换将继续进行而不会发生交互。否则,它将交互控制器交还给UIKit,以便它可以管理过渡。

dismiss状态的交互式控制器的方法也是相近的,最终实现效果如下:

源码下载

以上便是自定义过渡动画的全部内容,读者学习完后可以学一些进阶动画,参考一些优秀App的转场动画,进而创建自己过渡动画,从而获得良好的用户体验。
发布了54 篇原创文章 · 获赞 25 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/WxqHUT/article/details/104147918
今日推荐