Farlanki

iOS转场:神奇效果

字数统计: 2.3k阅读时长: 9 min
2017/04/17 Share

#前言
iOS中可以实现自定义效果的转场动画,iOS 7为我们带来了新的转场动画API。下面将来我们看看具体怎么使用这些新的API实现自定义转场效果,最后本文章会介绍一种类似keynote的神奇效果的转场动画的实现方法。
可以用到转场动画的地方一般有以下这几个:

  • 模态view的展示与消失时:
  • 1
    2
    open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Swift.Void)? = nil)
    open func dismiss(animated flag: Bool, completion: (() -> Swift.Void)? = nil)
  • 在UINavigationViewController中view的呈现和返回时:

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    open func pushViewController(_ viewController: UIViewController, animated: Bool)
    open func popViewController(animated: Bool) -> UIViewController?
    ```
    在调用这些方法后,如果相应的ViewController的相应delegate被设置了,那么系统就会调用相应的方法执行我们提供的自定义转场动画。

    具体来说,对于模态展示的viewController,设置其`transitioningDelegate`,对于UINavigationViewController,设置其`delegate`,并提供相应的返回`interactionControllerFor`或者`UIViewControllerAnimatedTransitioning`的方法,就可以让系统执行自定义转场动画。

    # UIViewControllerAnimatedTransitioning
    若要实现非交互式的转场动画,就需要用到`UIViewControllerAnimatedTransitioning`这个协议。这个协议负责提供转场动画的持续时间和具体的动画:
    ```swift
    public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval

    public func animateTransition(using transitionContext: UIViewControllerContextTransitioning)

我们需要实现的动画的核心代码需要在animateTransition:这个方法中实现。
注意到这两个方法的参数都是一个遵循了UIViewControllerContextTransitioning的变量。转场相关的信息就由这个变量负责提供。所以animateTransition的一般流程是:

  1. 利用context对象取得动画相关参数。
  2. 使用Core Animation或者UIView。 animation实现动画
  3. 清理并且完成转场。
    下面来介绍一下UIViewControllerContextTransitioning。

UIViewControllerContextTransitioning

transitionContext是遵循UIViewControllerContextTransitioning的系统传入的作为转场上下文的变量,transitionContext负责提供与转场相关的信息。其中比较重要的有:

1
2
3
4
5
6
7
8
9
10
11
12
13
public var containerView: UIView { get }//返回动画发生的view的superview

public var isInteractive: Bool { get }//返回此转场是否是交互性的

public func viewController(forKey key: UITransitionContextViewControllerKey) -> UIViewController?
//返回与此次转场相关的from和to view controller.

public func view(forKey key: UITransitionContextViewKey) -> UIView?
//返回与此次转场相关的from和to view.
public func initialFrame(for vc: UIViewController) -> CGRect
//返回与此次转场相关的view controller 的 初始frame
public func finalFrame(for vc: UIViewController) -> CGRect
//返回与此次转场相关的view controller 的 最终的frame

对于func view(forKey key: UITransitionContextViewKey) -> UIView?public func viewController(forKey key: UITransitionContextViewControllerKey) -> UIViewController?这两个方法,传入的参数可以为UITransitionContextViewKey.from或者UITransitionContextViewKey.to。from表示返回的view或者view controller 为转场发生时已经被展示的view或者相应的view controller, to 表示即将要展示的view。

  • 对于一个展示的动画,需要将toview手动加入container view。
  • 对于一个消失的动画,需要将fromview手动从container view中移除。
  • 完成动画后,需要调用func completeTransition(_ didComplete: Bool)方法。对应上面的第三步。

UIViewControllerTransitioningDelegate

UIViewControllerTransitioningDelegate的主要工作是返回遵循了UIViewControllerAnimatedTransitioning或者UIViewControllerInteractiveTransitioning的对象。上面已经介绍了前者,后者将在稍后介绍。
UIViewControllerTransitioningDelegate的主要方法如下:

1
2
3
4
5
6
7
8
9
10
11
optional public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?


@available(iOS 2.0, *)
optional public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?


optional public func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?


optional public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?

前两个方法负责返回动画控制器,后两个方法负责返回交互控制器。
当一个已经展示的view controller的transitioningDelegate已被设置,系统会依照以下流程处理:

  1. 调用animationControllerForPresentedController:presentingController:sourceController:或者 animationControllerForDismissedController:以取得animation controller。
  2. 调用interactionControllerForPresentation: 或者interactionControllerForDismissal:来确定本次转场动画是否为交互式的。如果上述两个方法返回的为nil,则本次转场动画不是为交互式的。
  3. 调用animation controller的` transitionDuration:方法取得动画持续的时间。
  4. 对于不可交互转场,调用animation controller的animateTransition:,对于可交互的转场,调用interactive animation object的startInteractiveTransition方法。
  5. 系统等待我们实现的代码调用context对象的completeTransition:方法。

非交互式转场

非交互式转场比较简单,只要对上面提及的几个协议有所了解就可以实现非交互式转场。

  1. 实现UIViewControllerAnimatedTransitioning的transitionDuration:和animateTransition:方法
  2. 实现UIViewControllerTransitioningDelegate的返回animation controller的方法
  3. 调用view controller切换的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class DelegateObject: NSObject , UIViewControllerTransitioningDelegate , UIViewControllerAnimatedTransitioning {

//MARK: - UIViewControllerAnimatedTransitioning

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


func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

let container = transitionContext.containerView
let fromView = transitionContext.view(forKey: .from)!
let toView = transitionContext.view(forKey: .to)!

let fromVC = transitionContext.viewController(forKey: .from)
let toVC = transitionContext.viewController(forKey: .to)

let offScreenUp = CGAffineTransform(translationX: 0, y: -container.frame.height)
let offScreenDown = CGAffineTransform(translationX: 0, y: container.frame.height)

toView.transform = offScreenUp
container.addSubview(toView)

transitionContext.finalFrame(for: fromVC!)

let duration = self.transitionDuration(using: transitionContext)

UIView.animate(withDuration: duration, animations: {
fromView.transform = offScreenDown
toView.transform = CGAffineTransform.identity
}) { (finish) in
fromView.removeFromSuperview()
transitionContext.completeTransition(true)
}
}
//MARK: - UIViewControllerTransitioningDelegate

func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}

以上代码实现了dismiss一个view controller时的自定义转场,效果是当dismiss的时候,from view和to view都从上向下滑动,from view从屏幕外滑到屏幕内,to view从屏幕内滑出屏幕。因为协议具有组装的特性,我们可以创建一个符合两个协议的对象来实现转场动画,上述代码就是实现了DelegateObject作为该对象。
之后,我们要将DelegateObject赋值给view controller的transitionDelegate属性。注意,由于transitionDelegate是weak属性,所以需要先将DelegateObject赋值给一个strong的变量,再将该变量赋值给transitionDelegate,否则系统会析构DelegateObject对象。

交互式转场

将非交互式转场升级成交互式转场,需要实现interactionControllerForPresentation:或者
interactionControllerForDismissal:方法,该方法返回一个遵循UIViewControllerInteractiveTransitioning协议的对象。系统已经为我们提供了UIPercentDrivenInteractiveTransition类,使用该类时,只要根据用户手势更新调用该类的对象的updaate:方法,并且在转场完成或者取消后调用finish()或者cancel()方法。之后,实现UIViewControllerTransitioningDelegate的interactionControllerForPresentation:或者interactionControllerForDismissal:方法,返回一个UIPercentDrivenInteractiveTransition对象即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var interactionController : UIPercentDrivenInteractiveTransition?
//DelegateObject增加的属性
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self.interactionController
}
//增加的interactionControllerForDismissal方法

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//...
UIView.animate(withDuration: duration, animations: {
fromView.transform = offScreenDown
toView.transform = CGAffineTransform.identity
}) { (finish) in
fromView.removeFromSuperview()
self.interactionController = nil
//在动画介绍后移除interactionController
transitionContext.completeTransition(true)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//view controller
var finishTransition = false
var begin = CGPoint()
var interactionController : UIPercentDrivenInteractiveTransition = UIPercentDrivenInteractiveTransition()


func panGestureHandle(sender : UIPanGestureRecognizer){
switch sender.state {
case .began:
self.delegateObject?.interactionController = self.interactionController
self.navigationController?.popViewController(animated: true)
self.begin = sender.translation(in: self.view)
case .changed:
var transition = sender.translation(in: self.view)
var fraction = (transition.x - self.begin.x) / (self.view.frame.width)
if(fraction > 0.5){
self.finishTransition = true
}
else{
self.finishTransition = false
}
self.delegateObject?.interactionController?.update(fraction)
case .ended:
if(self.finishTransition == true){
self.delegateObject?.interactionController?.finish()
}else{
self.delegateObject?.interactionController?.cancel()
}
case .cancelled:
self.delegateObject?.interactionController?.cancel()
default :
break
}
}

在view controller 的手势处理函数中调用dismiss或者pop函数,系统会自动开始处理转场动画的相关流程。系统判断一个转场动画是否交互式的方法是判断UIViewControllerTransitioningDelegate的interactionControllerForPresentation:或者interactionControllerForDismissal:是否返回了UIViewControllerInteractiveTransitioning。因为转场由手势处理函数发起,我们可以肯定该转场是属于交互式转场,所以需要设置delegateObject的interactionController属性。对于按按钮等方式发起的非交互式转场动画,我们不需要传递UIViewControllerInteractiveTransitioning,否则会导致转场不成功。

神奇效果

神奇效果指的是转场前后一个页面上相应的元素动到下一个页面上的效果。实现这个效果的思路是:

  1. 对from view的相应元素截图,生成一个替代视图
  2. 将替代视图添加到container view上,位置,大小与from view中的相应视图一样
  3. 将from view上的相应视图设置为不可见
  4. 在container view上对替代视图作动画处理,覆盖to view上相应视图的位置。同时隐去from view,呈现to view
    5.动画完成,在container view中隐藏替代视图 ,执行清理工作

一下代码显示的是一个神奇效果的动画实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

let container = transitionContext.containerView
let fromView = transitionContext.view(forKey: .from)!
let toView = transitionContext.view(forKey: .to)!

let fromVC = transitionContext.viewController(forKey: .from)
let toVC = transitionContext.viewController(forKey: .to)

if(fromVC is TableTableViewController && toVC is DetailViewController){
let index = (fromVC as! TableTableViewController).tableView.indexPathForSelectedRow
var magicView = (fromVC as! TableTableViewController).tableView.cellForRow(at:index!)?.imageView
let snapShotView = magicView?.snapshotView(afterScreenUpdates: false)
var print = magicView?.superview?.convert((magicView?.frame)!, to: container)
snapShotView?.frame = (magicView?.superview?.convert((magicView?.frame)!, to: container))!
(toVC as! DetailViewController).imageView.alpha = 0
toVC?.view.alpha = 0
magicView?.alpha = 0
container.addSubview(toView)
container.addSubview(snapShotView!)
let finalRect = (toVC as! DetailViewController).imageView.frame
let finalFrame = toView.convert(finalRect, to: container)

UIView.animate(withDuration: self.transitionDuration(using: transitionContext), animations: {
snapShotView?.frame = finalFrame
toView.alpha = 1;
}, completion: { (finish) in
magicView?.alpha = 1
(toVC as! DetailViewController).imageView.alpha = 1
transitionContext.completeTransition(true)
})
}
}

总结

iOS中实现转场动画的方法大致如上。我觉得转场动画的关键是container view,它可以在转场的时候作为from view和to view之间的一个桥梁,让我们在其中实现转场动画。其次,苹果利用各种协议搭建了转场动画的api框架,让我们在书写相关代码的时候让代码得到重用。

CATALOG
  1. 1. UIViewControllerContextTransitioning
  2. 2. UIViewControllerTransitioningDelegate
  3. 3. 非交互式转场
  4. 4. 交互式转场
  5. 5. 神奇效果
  6. 6. 总结