动画与转场,个人认为在概念上并不复杂,只是在代码的组织和形式上比较复杂,因此我尝试先讲讲概念,再讲讲实现,让思绪清晰一些。
什么是动画(Animation)?
所谓动画,就是在一段时间内,一些 view 的位置、颜色等属性会逐渐变化的一个现象。那么要完成一个动画,我们只需要确定三点:动画有多久、动画涉及到哪些 view 、这些 view 都有哪些属性改变了,说简单点儿就是时间、元素、变化形式。明确了这三点,各种 API 的变化只是在代码的简洁性和复用度上不停的做文章而已。
那,什么是转场(Transition)?
我们说到,动画的三个主要元素是时间、元素、变化形式,在元素这里动画并没有做过多的约束,而从概念上讲,转场就是一个动画的子集,其约束动画的元素必须为两个元素,并且一般都是两个 view controller 的主 view 进行的转换(所以说转场是针对两个 vc 的动画也没啥大毛病)。
iOS 中动画怎么做?
了解了动画的关键概念,我们来看看在 iOS 中,应该如何用代码去描述这三个概念。
第一种:使用UIView 的 begin/commit :
_demoView.frame = CGRectMake(0, SCREEN_HEIGHT/2-50, 50, 50);
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:1.0f];// 这里描述时间
_demoView.frame = CGRectMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-50, 50, 50);// 这里同时描述了元素和变化形式
[UIView commitAnimations];
第二种:直接通过 block 调用
_demoView.frame = CGRectMake(0, SCREEN_HEIGHT/2-50, 50, 50);
[UIView animateWithDuration:1.0f delay:1.0f // 这里是时间
options:UIViewAnimationOptionCurveEaseIn // 这里是一些封装的变化形式
animations:^{
_demoView.frame = CGRectMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-50, 50, 50);// 这里同时描述了元素和变化形式
} completion:nil];
第三种:将对属性的变化封装到 CoreAnimation 对象中,然后应用到某个 view 的 layer 上
CABasicAnimation *anima = [CABasicAnimation animationWithKeyPath:@"position"];
anima.fromValue = [NSValue valueWithCGPoint:CGPointMake(0, SCREEN_HEIGHT/2-75)];
anima.toValue = [NSValue valueWithCGPoint:CGPointMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-75)];// 这里描述了变化形式
anima.duration = 1.0f;// 这里描述了时间
[_demoView.layer addAnimation:anima forKey:@"positionAnimation"];// 这里则是描述了元素
这三种方式中,第一种是很久以前(iOS 4.0)使用的形式,无论是便捷度和复用度都不是很高。第二种是最方便的,但是缺点在于不好复用(除非把 block 保存起来,可以在一个 vc 中实现复用)。第三种是一种很容易复用的形式,将动画的三个元素中时间、变化形式单独抽离出来,使得其可以自由的应用在任意的元素上。(由此可以看出,如果想要代码的复用度更高,就需要不断的减少一段代码或者一个对象在概念上的职责)
iOS 中转场怎么做?
前面我们说过,转场是针对于两个特定的 view 的动画,所以我们需要先约定一下术语,假如我们有两个 VC A/B,我们要从 A 转换到 B,我们称呼 A 为 presentingViewController(或者 fromViewController),称呼 B 为 presentedViewController(或者 toViewController)。当从 B 结束转换回到 A 时,我们仍然称呼 A 为 presentingViewController,B 为 presentedViewController,但是我们会称呼 A 为 toViewController ,而 B 为 fromViewController。明白区别了么?from/to 是针对一次动画的,而 presented/presenting 是针对一次完整的转场的。
虽然从概念上来说,转场是一种特定的动画,但是实际上转场需要考虑的事情要比一般的动画要多(比如一般的动画可能不需要交互,但是转场可能需要),因此在代码的组织结构上,转场使用了更多的对象去更加细致的拆分概念上的职责。
最基本的一种实现转场的方式,非常类似于上面所说的第二种动画的表现形式:
[self transitionFromViewController:self.fromVC
toViewController:self.toVC // 元素
duration:5 // 时间
options:UIViewAnimationOptionCurveEaseInOut // 变化形式的封装
animations:^{
CGRect frame = self.thirdVC.view.frame;
frame.origin.y = 150;
self.thirdVC.view.frame = frame;
}
completion:nil];
这个转场一般在容器 VC 中使用。缺点其实是和最基本的动画调用方式一样,都是不容易复用,并且使用场景有限,只能用在容器 vc 中,不能用在两个平级的 vc 中。也就是说,为了从 A 转到 B,我们必须首先有一个 C ,然后让 A、B 作为 C 的 child vc ,显然很不方便啊,那么我们就需要考虑一种新的代码组织形式,将转场的职责进行拆分。
转场的职责划分
在一次自定义的转场中,我们会将指责进行如下形式的划分:
首先,我们需要有两个 vc(废话(╬▔皿▔)),然后设置 presentingVC 的 modalPresentationStyle
为 UIModalPresentationCustom
,接下来将 presentingVC 的 transitioningDelegate
属性指向一个实现了 UIViewControllerTransitioningDelegate
协议的对象上。这样就告诉 UIKit 任意一个 vc 用 prensentViewController:animated:completion
方法展示 presentingVC 时,presentingVC 的转场效果完全由 transitioningDelegate
属性所指向的对象来负责。
// PresentingVC
self.transitioningDelegate = [TransitionDelegate new];// 转场效果这一部分职责从 vc 中剥离了出去
TransitionDelegate
是一个实现了 UIViewControllerTransitioningDelegate
协议的对象,在这个协议中又将转场效果的职责分为三个对象去负责:一个负责转场动画效果的 Animator,一个负责转场过程中交互的 InteractiveAnimator,和一个则负责转场过程中 view 的层级关系以及在不同屏幕上的适配。这三个对象的职责,在代码上的表现形式就是将UIViewControllerTransitioningDelegate
的内容分为三组。我们来一个个了解一下。
TransitionAnimator
这个对象负责转场的动画效果,具体点儿来说,他决定了可见的视图从 PresentingViewController 的 view 到可见视图变为 PresentedViewController 的 view 的过程中,两个 view 应该如何去变化。在UIViewControllerTransitioningDelegate
协议中,该对象可以通过两个方法返回:
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
两个方法中,前者决定了 present 过程中的动画效果,后者则决定了 dismiss 过程中的动画效果。而具体 Animator 如何去控制转场过程中的动画,我们就需要看看 UIViewControllerAnimatedTransitioning
这个协议中的方法都有些什么:
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
第一个方法决定了转场的时间,第二个方法则是通过一个 transitionContext 对象传递给 Animator 对象转场过程中的 FromVC/ToVC,以及 containerView ,也就是转场过程中的元素,然后我们就可以通过 UIKit 的动画 API 决定转场的变化形式了。在这个方法中我们要做的就是:
- 得到 ToVC 的 view,设定其初始状态
- 将 ToVC 的 view 添加到 containerView 中
- 通过任意一种动画形式对 ToVC 的 view 做动画,然后在结束的时候调用
transitionContext
对象的completeTransition:
方法告知系统我们的动画做完了。
更具体的内容,可以参见如下的一段代码:
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
// 获取所有需要的 view 以及 vc
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = transitionContext.containerView;
// 设定初始状态
toVC.view.frame = CGRectMake(0, - CGRectGetHeight(fromVC.view.frame), CGRectGetWidth(fromVC.view.frame), CGRectGetHeight(fromVC.view.frame));
toVC.view.alpha = 0.0f;
// 一定要自己手动添加 subview, fromVC 的 view UIKit 会自动移除,但是 UIKit 不会自动添加 toVC 的 view
[containerView addSubview:toVC.view];
// 获取动画时间
NSTimeInterval duration = [self transitionDuration:transitionContext];
// 开始动画
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
toVC.view.alpha = 1.0f;
toVC.view.frame = fromVC.view.frame;
} completion:^(BOOL finished) {
if (finished) {
[transitionContext completeTransition:YES];
NSLog(@"finished");
}
}];
}
InteractiveAnimator
对于一般的转场来说,实现了基本的动画效果可能就够了,但是实际开发中,我们可能对于转场有更加深入的需求,比如希望转场能够带有用户交互,像系统的全局返回手势那样,这个时候,我们就需要额外返回一个 InteractiveAnimator
来告诉 UIKit 随着用户的手势变化,动画应该执行到百分之多少或者是否需要取消,这些操作我们都可以通过 context 对象中的方法来完成:
- (void)cancelInteractiveTransition;
- (void)finishInteractiveTransition;
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
因此,如果想实现一个交互式的转场,我们需要做如下几件事儿:
- 在 presentingVC 中添加一个 button 点击以外的『触发器』(一般来说,都是一个 Gesture Recognizer),比如添加一个边缘滑动的 Gesture Recognizer,当一个边缘滑动开始时,我们在对应的回调中 present PresentedVC。
- 在 presentedVC 的 transitionDelegate 中,返回一个 InteractiveAnimator。
- 在 Animator 中的
startInteractiveTransition:
方法中将 context 对象保存起来。 - 想办法将 Gesture Recognizer 传递给 InteractiveAnimator,使得在 Animator 中可以获取当前手势的信息,结合 context 对象中的 containerView 等信息,我们可以知道当前手势在 view 中更具体的信息。
- 根据预先设定好的规则,在 Gesture Recognizer 的回调中调用 context 对象的 cancel/finished/update 方法
比如,如果我们想实现一个边缘滑动的交互动画效果,我们可以这么来写代码:
- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
// 把 context 对象保存起来
self.transitionContext = transitionContext;
[super startInteractiveTransition:transitionContext];
}
// 根据手势的偏移来计算当前动画应该有的完成度
- (CGFloat)percentForGesture:(UIScreenEdgePanGestureRecognizer *)gesture
{
// 根据 container view 以及 gesture recognizer 计算偏移量
UIView *transitionContainerView = self.transitionContext.containerView;
CGPoint locationInSourceView = [gesture locationInView:transitionContainerView];
// 根据偏移量得出百分比
CGFloat width = CGRectGetWidth(transitionContainerView.bounds);
return (width - locationInSourceView.x) / width;
}
// gesture recognizer 的回调
- (IBAction)gestureRecognizeDidUpdate:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer
{
switch (gestureRecognizer.state)
{
case UIGestureRecognizerStateBegan:
break;
case UIGestureRecognizerStateChanged:
// 计算百分比,并返回
[self updateInteractiveTransition:[self percentForGesture:gestureRecognizer]];
break;
case UIGestureRecognizerStateEnded:
// 根据预先设定的阈值决定是结束还是取消,这里我们设定 view 中间是分界线
if ([self percentForGesture:gestureRecognizer] >= 0.5f)
[self finishInteractiveTransition];
else
[self cancelInteractiveTransition];
break;
default:
// 其他情况,取消转场
[self cancelInteractiveTransition];
break;
}
}
PresentationController
以上的两组接口,分别让我们自定义了转场过程中的动画、动画执行百分比,但是不管是哪个,都会在最后将 fromVC 的 view 从 containerView 上移除,并且整个转场过程中如果我们想添加一些额外的 view 也是无法做到的。如果想要实现这些功能,就需要我们创建一个 UIPresentationController
的子类,然后重载其 四个转场的生命周期方法:
- presentationTransitionWillBegin
- presentationTransitionDidEnd:
- dismissalTransitionWillBegin
- dismissalTransitionDidEnd:
在重载这些方法时,我们也可以使用其 presentingViewController 属性的 transitionCoordinator 来同步的为我们新添加的 view 执行动画(所谓同步就是和我们之前在 Animator 中写的动画同时执行)。
比如,我们可以为我们添加的一个 dimming view 的透明度设置一个动画:
id<UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator;
self.dimmingView.alpha = 0.f;
[transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
self.dimmingView.alpha = 0.5f;
} completion:NULL];
总结一下来说,如果我们想要使用 UIPresentationController ,我们需要:
- 设置 presentedVC 的 presentStyle 为
UIModalPresentationCustom
- 在 presentedVC 的 transitionDelegate 中返回我们创建的
UIPresentationController
的子类 - 在子类中重载转场生命周期的四个方法,添加我们所需要的自定义的view
网友评论