iOS UIViewControllerTransitioning 自定义转场框架的使用
iOS UIViewControllerTransitioning 自定义转场框架的使用 1
1 简介 1
2 转场协议 2
2.1转场代理(Transition Delegate): 2
2.2.动画控制器(Animation Controller): 2
2.3.交互控制器(Interaction Controller): 3
2.4.转场环境(Transition Context): 3
2.5.转场协调器(Transition Coordinator): 3
3 非交互式转场 3
3.1 动画控制器协议 3
3.2 动画控制器实现 5
3.3 特殊的 Modal 转场 6
4 交互式转场 10
Transition Coordinator 13
交互转场的限制 14
5 UICollectionViewController 布局转场 15
1 简介
在 iOS 7 之前,我们只能使用系统提供的转场效果,大部分时候够用,但仅仅是够用而已,总归会有各种不如意的小地方,但我们却无力改变;iOS 7 开放了相关 API 允许我们对转场效果进行全面定制,目前为止,官方支持以下几种方式的自定义转场:
(1)、在 UINavigationController 中 push 和 pop;
(2)、在 UITabBarController 中切换 Tab;
(3)、Modal 转场:presentation 和 dismiss,俗称视图控制器的模态显示和消失,仅限于modalPresentationStyle属性为 UIModalPresentationFullScreen 或 UIModalPresentationCustom 这两种模式;
(4)、UICollectionViewController 的布局转场;
2 转场协议
iOS 7 以协议的方式开放了自定义转场的 API,协议的好处是不再拘泥于具体的某个类,只要是遵守该协议的对象都能参与转场,非常灵活。转场协议由5种协议组成,在实际中只需要我们提供其中的两个或三个便能实现绝大部分的转场动画
2.1转场代理(Transition Delegate):
自定义转场的第一步便是提供转场代理,告诉系统使用我们提供的代理而不是系统的默认代理来执行转场。有如下三种转场代理,对应上面三种类型的转场:
//1. UINavigationController 的 delegate 遵循的协议
UINavigationControllerDelegate。
//2. UITabBarController 的 delegate 遵循的协议。
UITabBarControllerDelegate
// 3.UIViewController 的 transitioningDelegate 遵循的协议。
UIViewControllerTransitioningDelegate
这里除了<UIViewControllerTransitioningDelegate>是 iOS 7 新增的协议,其他两种在 iOS 2 里就存在了,在 iOS 7 时扩充了这种协议来支持自定义转场。
转场发生时,UIKit 将要求转场代理将提供转场动画的核心构件:动画控制器和交互控制器(可选的)由我们实现。
2.2.动画控制器(Animation Controller):
最重要的部分,负责添加视图以及执行动画;遵守<UIViewControllerAnimatedTransitioning>协议;由我们实现。
2.3.交互控制器(Interaction Controller):
通过交互手段,通常是手势来驱动动画控制器实现的动画,使得用户能够控制整个过程;遵守<UIViewControllerInteractiveTransitioning>协议;系统已经打包好现成的类供我们使用。
2.4.转场环境(Transition Context):
提供转场中需要的数据;遵守<UIViewControllerContextTransitioning>协议;由 UIKit 在转场开始前生成并提供给我们提交的动画控制器和交互控制器使用。
2.5.转场协调器(Transition Coordinator):
可在转场动画发生的同时并行执行其他的动画,其作用与其说协调不如说辅助,主要在 Modal 转场和交互转场取消时使用,其他时候很少用到;遵守<UIViewControllerTransitionCoordinator>协议;由 UIKit 在转场时生成,UIViewController 在 iOS 7 中新增了方法transitionCoordinator()返回一个遵守该协议的对象,且该方法只在该控制器处于转场过程中才返回一个此类对象,不参与转场时返回 nil。
3 非交互式转场
这个阶段要做两件事,提供转场代理并由代理提供动画控制器。在转场代理协议里动画控制器和交互控制器都是可选实现的,没有实现或者返回 nil 的话则使用默认的转场效果。
3.1 动画控制器协议
动画控制器负责添加视图以及执行动画,遵守UIViewControllerAnimatedTransitioning协议,该协议要求实现以下方法:
//执行动画的地方,最核心的方法。
(Required)func animateTransition(_ transitionContext: UIViewControllerContextTransitioning)
//返回动画时间,"return 0.5" 已足够,非常简单
(Required)func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval
//如果实现了,会在转场动画结束后调用,可以执行一些收尾工作。
(Optional)func animationEnded(_ transitionCompleted: Bool)
最重要的是第一个方法,该方法接受一个遵守<UIViewControllerContextTransitioning>协议的转场环境对象,上一节的 API 解释里提到这个协议,它提供了转场所需要的重要数据:参与转场的视图控制器和转场过程的状态信息。
UIKit 在转场开始前生成遵守转场环境协议<UIViewControllerContextTransitioning>的对象 transitionContext,它有以下几个方法来提供动画控制器需要的信息:
//返回容器视图,转场动画发生的地方。 func containerView() -> UIView?
//获取参与转场的视图控制器,有 UITransitionContextFromViewControllerKey 和 UITransitionContextToViewControllerKey 两个 Key。
func viewControllerForKey(_ key: String) -> UIViewController? //iOS 8新增 API 用于方便获取参与参与转场的视图,有 UITransitionContextFromViewKey 和 UITransitionContextToViewKey 两个 Key。
func viewForKey(_ key: String) -> UIView? AVAILABLE_IOS(8_0)
通过viewForKey:获取的视图是viewControllerForKey:返回的控制器的根视图,或者 nil。viewForKey:方法返回 nil 只有一种情况: UIModalPresentationCustom 模式下的 Modal 转场 ,通过此方法获取 presentingView 时得到的将是 nil,在后面的 Modal 转场里会详细解释。
前面提到转场的本质是下一个场景的视图替换当前场景的视图,从当前场景过渡下一个场景。下面称即将消失的场景的视图为 fromView,对应的视图控制器为 fromVC,即将出现的视图为 toView,对应的视图控制器称之为 toVC。几种转场方式的转场操作都是可逆的,一种操作里的 fromView 和 toView 在逆向操作里的角色互换成对方,fromVC 和 toVC 也是如此。 在动画控制器里,参与转场的视图只有 fromView 和 toView 之分,与转场方式无关。转场动画的最终效果只限制于你的想象力。 这也是动画控制器在封装后可以被第三方使用的重要原因。
在 iOS 8 中可通过以下方法来获取参与转场的三个重要视图,在 iOS 7 中则需要通过对应的视图控制器来获取,为避免 API 差异导致代码过长,示例代码中直接使用下面的视图变量:
let containerView = transitionContext.containerView()
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
3.2 动画控制器实现
转场 API 是协议的好处是不限制具体的类,只要对象实现该协议便能参与转场过程,这也带来另外一个好处:封装便于复用,尽管三大转场代理协议的方法不尽相同,但它们返回的动画控制器遵守的是同一个协议,因此可以将动画控制器封装作为第三方动画控制器在其他控制器的转场过程中使用。
func animateTransition(transitionContext: UIViewControllerContextTransitioning) { ...
//1
containerView.addSubview(toView)
//计算位移 transform,NavigationVC 和 TabBarVC 在水平方向进行动画,Modal 转场在竖直方向进行动画。
var toViewTransform = ...
var fromViewTransform = ...
toView.transform = toViewTransform
//根据协议中的方法获取动画的时间。
let duration = self.transitionDuration(transitionContext)
UIView.animateWithDuration(duration, animations: {
fromView.transform = fromViewTransform
toView.transform = CGAffineTransformIdentity
}, completion: { _ in
//考虑到转场中途可能取消的情况,转场结束后,恢复视图状态。
fromView.transform = CGAffineTransformIdentity
toView.transform = CGAffineTransformIdentity
//2
let isCancelled = transitionContext.transitionWasCancelled()
transitionContext.completeTransition(!isCancelled)
})
}
注意上面的代码有2处标记,是动画控制器必须完成的:
- 将 toView 添加到容器视图中,使得 toView 在屏幕上显示( Modal 转场中此点稍有不同,下一节细述);
- 正确地结束转场过程。转场的结果有两种:完成或取消。非交互转场的结果只有完成一种情况,不过交互式转场需要考虑取消的情况。如何结束取决于转场的进度,通过transitionWasCancelled()方法来获取转场的状态,使用completeTransition:来完成或取消转场。
3.3 特殊的 Modal 转场
UINavigationController 和 UITabBarController 这两个容器 VC 的根视图在屏幕上是不可见的(或者说是透明的),可见的只是内嵌在这两者中的子 VC 中的视图,转场是从子 VC 的视图转换到另外一个子 VC 的视图,其根视图并未参与转场;而 Modal 转场,以 presentation 为例,是从 presentingView 转换到 presentedView,根视图 presentingView 也就是 fromView 参与了转场。而且 NavigationController 和 TabBarController 转场中的 containerView 也并非这两者的根视图。
[图片上传失败...(image-d2e88f-1616030414999)]
Modal 转场与两种容器 VC 的转场的另外一个不同是:Modal 转场结束后 presentingView 可能依然可见,UIModalPresentationPageSheet 模式就是这样。这种不同导致了 Modal 转场和容器 VC 的转场对 fromView 的处理差异:容器 VC 的转场结束后 fromView 会被主动移出视图结构,这是可预见的结果,我们也可以在转场结束前手动移除;而 Modal 转场中,presentation 结束后 presentingView(fromView) 并未主动被从视图结构中移除。准确来说,是 UIModalPresentationCustom 这种模式下的 Modal 转场结束时 fromView 并未从视图结构中移除;UIModalPresentationFullScreen 模式的 Modal 转场结束后 fromView 依然主动被从视图结构中移除了。这种差异导致在处理 dismissal 转场的时候很容易出现问题,没有意识到这个不同点的话出错时就会毫无头绪。下面来看看 dismissal 转场时的场景。
ContainerView 在转场期间作为 fromView 和 toView 的父视图。三种转场过程中的 containerView 是 UIView 的私有子类,不过我们并不需要关心 containerView 具体是什么。在 dismissal 转场中:
- UIModalPresentationFullScreen 模式:presentation 后,presentingView 被主动移出视图结构,在 dismissal 中 presentingView 是 toView 的角色,其将会重新加入 containerView 中,实际上,我们不主动将其加入,UIKit 也会这么做,前面的两种容器控制器的转场里不是这样处理的,不过这个差异基本没什么影响。
- UIModalPresentationCustom 模式:转场时 containerView 并不担任 presentingView 的父视图,后者由 UIKit 另行管理。在 presentation 后,fromView(presentingView) 未被移出视图结构,在 dismissal 中,注意不要像其他转场中那样将 toView(presentingView) 加入 containerView 中,否则本来可见的 presentingView 将会被移除出自身所处的视图结构消失不见。如果你在使用 Custom 模式时没有注意到这点,就很容易掉进这个陷阱而很难察觉问题所在
对于 Custom 模式,我们可以参照其他转场里的处理规则来打理:presentation 转场结束后主动将 fromView(presentingView) 移出它的视图结构,并用一个变量来维护 presentingView 的父视图,以便在 dismissal 转场中恢复;在 dismissal 转场中,presentingView 的角色由原来的 fromView 切换成了 toView,我们再将其重新恢复它原来的视图结构中。测试表明这样做是可行的。但是这样一来,在实现上,需要在转场代理中维护一个动画控制器并且这个动画控制器要维护 presentingView 的父视图,第三方的动画控制器必须为此改造。显然,这样的代价是无法接受的。
小结 :经过上面的尝试,建议是,不要干涉官方对 Modal 转场的处理,我们去适应它。在 Custom 模式下,由于 presentingView 不受 containerView 管理,在 dismissal 转场中不要像其他的转场那样将 toView(presentingView) 加入 containerView,否则 presentingView 将消失不见,而应用则也很可能假死;而在 presentation 转场中,切记不要手动将 fromView(presentingView) 移出其父视图。
iOS 8 为<UIViewControllerContextTransitioning>协议添加了viewForKey:方法以方便获取 fromView 和 toView,但是在 Modal 转场里要注意,从上面可以知道,Custom 模式下,presentingView 并不受 containerView 管理,这时通过viewForKey:方法来获取 presentingView 得到的是 nil,必须通过viewControllerForKey:得到 presentingVC 后来获取。因此在 Modal 转场中,较稳妥的方法是从 fromVC 和 toVC 中获取 fromView 和 toView。
顺带一提,前面提到的UIView的类方法transitionFromView:toView:duration:options:completion:能在 Custom 模式下工作,却与 FullScreen 模式有点不兼容。
Model 转场实现:
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
...
//不像容器 VC 转场里需要额外的变量来标记操作类型,UIViewController 自身就有方法跟踪 Modal 状态。
//处理 Presentation 转场:
if toVC.isBeingPresented(){
//1
containerView.addSubview(toView)
//在 presentedView 后面添加暗背景视图 dimmingView,注意两者在 containerView 中的位置。
let dimmingView = UIView()
containerView.insertSubview(dimmingView, belowSubview: toView)
//设置 presentedView 和 暗背景视图 dimmingView 的初始位置和尺寸。
let toViewWidth = containerView.frame.width * 2 / 3
let toViewHeight = containerView.frame.height * 2 / 3
toView.center = containerView.center
toView.bounds = CGRect(x: 0, y: 0, width: 1, height: toViewHeight)
dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
dimmingView.center = containerView.center
dimmingView.bounds = CGRect(x: 0, y: 0, width: toViewWidth, height: toViewHeight)
//实现出现时的尺寸变化的动画:
UIView.animateWithDuration(duration, delay: 0, options: .CurveEaseInOut, animations: {
toView.bounds = CGRect(x: 0, y: 0, width: toViewWidth, height: toViewHeight)
dimmingView.bounds = containerView.bounds
}, completion: {_ in
//2
let isCancelled = transitionContext.transitionWasCancelled()
transitionContext.completeTransition(!isCancelled)
})
}
//处理 Dismissal 转场,按照上一小节的结论,.Custom 模式下不要将 toView 添加到 containerView,省去了上面标记1处的操作。
if fromVC.isBeingDismissed(){
let fromViewHeight = fromView.frame.height
UIView.animateWithDuration(duration, animations:
{
fromView.bounds = CGRect(x: 0, y: 0, width: 1, height: fromViewHeight)
}, completion: { _ in
//2
let isCancelled = transitionContext.transitionWasCancelled()
transitionContext.completeTransition(!isCancelled)
})
} }
4 交互式转场
在非交互转场的基础上将之交互化需要两个条件:
- 由转场代理提供交互控制器,这是一个遵守<UIViewControllerInteractiveTransitioning>协议的对象,不过系统已经打包好了现成的类UIPercentDrivenInteractiveTransition供我们使用。我们不需要做任何配置,仅仅在转场代理的相应方法中提供一个该类实例便能工作。另外交互控制器必须有动画控制器才能工作。
- 交互控制器还需要交互手段的配合,最常见的是使用手势,或是其他事件,来驱动整个转场进程。
满足以上两个条件很简单,但是很容易犯错误。
正确地提供交互控制器 :
如果在转场代理中提供了交互控制器,而转场发生时并没有方法来驱动转场进程(比如手势),转场过程将一直处于开始阶段无法结束,应用界面也会失去响应:在 NavigationController 中点击 NavigationBar 也能实现 pop 返回操作,但此时没有了交互手段的支持,转场过程卡壳;在 TabBarController 的代理里提供交互控制器存在同样的问题,点击 TabBar 切换页面时也没有实现交互控制。因此仅在确实处于交互状态时才提供交互控制器,可以使用一个变量来标记交互状态,该变量由交互手势来更新状态。
以为 NavigationController 提供交互控制器为例:
class SDENavigationDelegate: NSObject, UINavigationControllerDelegate { var interactive = false let interactionController = UIPercentDrivenInteractiveTransition() ... func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return interactive ? self.interactionController : nil } }
TabBarController 的实现类似,Modal 转场代理分别为 presentation 和 dismissal 提供了各自的交互控制器,也需要注意上面的问题。
问题的根源是交互控制的工作机制导致的,交互过程实际上是由转场环境对象<UIViewControllerContextTransitioning>来管理的,它提供了如下几个方法来控制转场的进度:
func updateInteractiveTransition(_ percentComplete: CGFloat)//更新转场进度,进度数值范围为0.0~1.0。 func cancelInteractiveTransition()//取消转场,转场动画从当前状态返回至转场发生前的状态。 func finishInteractiveTransition()//完成转场,转场动画从当前状态继续直至结束。
交互控制协议<UIViewControllerInteractiveTransitioning>只有一个必须实现的方法:
func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning)
在转场代理里提供了交互控制器后,转场开始时,该方法自动被 UIKit 调用对转场环境进行配置。
系统打包好的UIPercentDrivenInteractiveTransition中的控制转场进度的方法与转场环境对象提供的三个方法同名,实际上只是前者调用了后者的方法而已。系统以一种解耦的方式使得动画控制器,交互控制器,转场环境对象互相协作,我们只需要使用UIPercentDrivenInteractiveTransition的三个同名方法来控制进度就够了。如果你要实现自己的交互控制器,而不是UIPercentDrivenInteractiveTransition的子类,就需要调用转场环境的三个方法来控制进度,压轴环节我们将示范如何做。
交互控制器控制转场的过程就像将动画控制器实现的动画制作成一部视频,我们使用手势或是其他方法来控制转场动画的播放,可以前进,后退,继续或者停止。finishInteractiveTransition()方法被调用后,转场动画从当前的状态将继续进行直到动画结束,转场完成;cancelInteractiveTransition()被调用后,转场动画从当前的状态回拨到初始状态,转场取消。
在 NavigationController 中点击 NavigationBar 的 backBarButtomItem 执行 pop 操作时,由于我们无法介入 backBarButtomItem 的内部流程,就失去控制进度的手段,于是转场过程只有一个开始,永远不会结束。其实我们只需要有能够执行上述几个方法的手段就可以对转场动画进行控制,用户与屏幕的交互手段里,手势是实现这个控制过程的天然手段,我猜这是其被称为交互控制器的原因。
交互手段的配合 :
下面使用演示如何利用屏幕边缘滑动手势UIScreenEdgePanGestureRecognizer在 NavigationController 中控制 Slide 动画控制器提供的动画来实现右滑返回的效果,该手势绑定的动作方法如下:
func handleEdgePanGesture(gesture: UIScreenEdgePanGestureRecognizer){ //根据移动距离计算交互过程的进度。 let percent = ... switch gesture.state{ case .Began: //转场开始前获取代理,一旦转场开始,VC 将脱离控制器栈,此后 self.navigationController 返回的是 nil。 self.navigationDelegate = self.navigationController?.delegate as? SDENavigationDelegate //更新交互状态 self.navigationDelegate?.interactive = true //1.交互控制器没有 start 之类的方法,当下面这行代码执行后,转场开始; //如果转场代理提供了交互控制器,它将从这时候开始接管转场过程。 self.navigationController?.popViewControllerAnimated(true) case .Changed: //2.更新进度: self.navigationDelegate?.interactionController.updateInteractiveTransition(percent) case .Cancelled, .Ended: //3.结束转场: if percent > 0.5{ //完成转场。 self.navigationDelegate?.interactionController.finishInteractiveTransition() }else{ //或者,取消转场。 self.navigationDelegate?.interactionController.cancelInteractiveTransition() } //无论转场的结果如何,恢复为非交互状态。 self.navigationDelegate?.interactive = false default: self.navigationDelegate?.interactive = false } }
交互转场的流程就是三处数字标记的代码。不管是什么交互方式,使用什么转场方式,都是在使用这三个方法控制转场的进度。 对于交互式转场,交互手段只是表现形式,本质是驱动转场进程。 很希望能够看到更新颖的交互手法,比如通过点击页面不同区域来控制一套复杂的流程动画。TabBarController 的 Demo 中也实现了滑动切换 Tab 页面,代码是类似的,就不占篇幅了;示范的 Modal 转场我没有为之实现交互控制,原因也提到过了,没有比较合乎操作直觉的交互手段,不过真要为其添加交互控制,代码和上面是类似的。
转场交互化后结果有两种:完成和取消。取消后动画将会原路返回到初始状态,但已经变化了的数据怎么恢复?
一种情况是,控制器的系统属性,比如,在 TabBarController 里使用上面的方法实现滑动切换 Tab 页面,中途取消的话,已经变化的selectedIndex属性该怎么恢复为原值;上面的代码里,取消转场的代码执行后,self.navigationController返回的依然还是是 nil,怎么让控制器回到 NavigationController 的控制器栈顶。对于这种情况,UIKit 自动替我们恢复了,不需要我们操心(可能你都没有意识到这回事);
另外一种就是,转场发生的过程中,你可能想实现某些效果,一般是在下面的事件中执行,转场中途取消的话可能需要取消这些效果。
func viewWillAppear(_ animated: Bool) func viewDidAppear(_ animated: Bool) func viewWillDisappear(_ animated: Bool) func viewDidDisappear(_ animated: Bool)
交互转场介入后,视图在这些状态间的转换变得复杂,WWDC 上苹果的工程师还表示转场过程中 view 的Will系方法和Did系方法的执行顺序并不能得到保证,虽然几率很小,但如果你依赖于这些方法执行的顺序的话就可能需要注意这点。而且,Did系方法调用时并不意味着转场过程真的结束了。另外,fromView 和 toView 之间的这几种方法的相对顺序更加混乱,具体的案例可以参考这里:The Inconsistent Order of View Transition Events。
如何在转场过程中的任意阶段中断时取消不需要的效果?这时候该转场协调器(Transition Coordinator)再次出场了。
Transition Coordinator
转场协调器(Transition Coordinator)的出场机会不多,但却是关键先生。Modal
转场中,UIPresentationController类只能通过转场协调器来与动画控制器同步,并行执行其他动画;这里它可以在交互式转场结束时执行一个闭包:
func notifyWhenInteractionEndsUsingBlock(_ handler: (UIViewControllerTransitionCoordinatorContext) -> Void)
当转场由交互状态转变为非交互状态(在手势交互过程中则为手势结束时),无论转场的结果是完成还是被取消,该方法都会被调用;得益于闭包,转场协调器可以在转场过程中的任意阶段搜集动作并在交互中止后执行。闭包中的参数是一个遵守<UIViewControllerTransitionCoordinatorContext>协议的对象,该对象由 UIKit 提供,和前面的转场环境对象<UIViewControllerContextTransitioning>作用类似,它提供了交互转场的状态信息。
override func viewWillAppear(animated: Bool) { super.viewWillDisappear(animated) self.doSomeSideEffectsAssumingViewDidAppearIsGoingToBeCalled() //只在处于交互转场过程中才可能取消效果。 if let coordinator = self.transitionCoordinator() where coordinator.initiallyInteractive() == true{ coordinator.notifyWhenInteractionEndsUsingBlock({ interactionContext in if interactionContext.isCancelled(){ self.undoSideEffects() } }) } }
不过交互状态结束时并非转场过程的终点(此后动画控制器提供的转场动画根据交互结束时的状态继续或是返回到初始状态),而是由动画控制器来结束这一切:
optional func animationEnded(_ transitionCompleted: Bool)
如果实现了该方法,将在转场动画结束后调用。
UIViewController 可以通过transitionCoordinator()获取转场协调器,该方法的文档中说只有在 Modal 转场过程中,该方法才返回一个与当前转场相关的有效对象。实际上,NavigationController 的转场中 fromVC 和 toVC 也能返回一个有效对象,TabBarController 有点特殊,fromVC 和 toVC 在转场中返回的是 nil,但是作为容器的 TabBarController 可以使用该方法返回一个有效对象。
转场协调器除了上面的两种关键作用外,也在 iOS 8 中的适应性布局中担任重要角色,可以查看<UIContentContainer>协议中的方法,其中响应尺寸和屏幕旋转事件的方法都包含一个转场协调器对象,视图的这种变化也被系统视为广义上的 transition,参数中的转场协调器也由 UIKit 提供。这个话题有点超出本文的范围,就不深入了,有需要的话可以查看文档和相关 session。
交互转场的限制
如果希望转场中的动画能完美地被交互控制,必须满足2个隐性条件:
- 使用 UIView 动画的 API。你当然也可以使用 Core Animation 来实现动画,甚至,这种动画可以被交互控制,但是当交互中止时,会出现一些意外情况:如果你正确地用 Core Animation 的方式复现了 UIView 动画的效果(不仅仅是动画,还包括动画结束后的处理),那么手势结束后,动画将直接跳转到最终状态;而更多的一种状况是,你并没有正确地复现 UIView 动画的效果,手势结束后动画会停留在手势中止时的状态,界面失去响应。所以,如果你需要完美的交互转场动画,必须使用 UIView 动画。
- 在动画控制器的animateTransition:中提交动画。问题和第1点类似,在viewWillDisappear:这样的方法中提交的动画也能被交互控制,但交互停止时,立即跳转到最终状态。
5 UICollectionViewController
布局转场
布局转场只针对 CollectionViewController 搭配 NavigationController 的组合,且是作用于布局,而非视图。采用这种布局转场时,NavigationController 将会用布局变化的动画来替代 push 和 pop 的默认动画。苹果自家的照片应用中的「照片」Tab 页面使用了这个技术:在「年度-精选-时刻」几个时间模式间切换时,CollectionViewController 在 push 或 pop 时尽力维持在同一个元素的位置同时进行布局转换。
布局转场的实现比三大主流转场要简单得多,只需要满足四个条件:NavigationController + CollectionViewController, 且要求后者都拥有相同数据源, 并且开启useLayoutToLayoutNavigationTransitions属性为真。
let cvc0 = UICollectionViewController(collectionViewLayout: layout0) //作为 root VC 的 cvc0 的该属性必须为 false,该属性默认为 false。 cvc0.useLayoutToLayoutNavigationTransitions = false let nav = UINavigationController(rootViewController: cvc0) //cvc0, cvc1, cvc2 必须具有相同的数据,如果在某个时刻修改了其中的一个数据源,其他的数据源必须同步,不然会出错。
let cvc1 = UICollectionViewController(collectionViewLayout: layout1) cvc1.useLayoutToLayoutNavigationTransitions = true
nav.pushViewController(cvc1, animated: true)
let cvc2 = UICollectionViewController(collectionViewLayout: layout2) cvc2.useLayoutToLayoutNavigationTransitions = true
nav.pushViewController(cvc2, animated: true)
nav.popViewControllerAnimated(true)
Push 进入控制器栈后,不能更改useLayoutToLayoutNavigationTransitions的值,否则应用会崩溃。当 CollectionView 的数据源(section 和 cell 的数量)不完全一致时,push 和 pop 时依然会有布局转场动画,但是当 pop 回到 rootVC 时,应用会崩溃。可否共享数据源保持同步来克服这个缺点?测试表明,这样做可能会造成画面上的残缺,以及不稳定,建议不要这么做。
此外,iOS 7 支持 UICollectionView 布局的交互转换(Layout Interactive Transition),过程与控制器的交互转场(ViewController Interactive Transition)类似,这个功能和布局转场(CollectionViewController Layout Transition)容易混淆,前者是在自身布局转换的基础上实现了交互控制,后者是 CollectionViewController 与 NavigationController 结合后在转场的同时进行布局转换。