iOS View 编程指导(四)-动画(Animation)

动画指一个用户界面从一个状态动态的地过渡到另外一个状态. 在iOS应用中,动画被广泛的使用,从view位置的变换,大小的改变,隐藏,改变的view的层级树; 你可以使用动画来作为对用户的反馈或者实现其他视觉效果.

在iOS中,实现精美的动画比较简单,只需要几行代码就可以搞定. 这就需要感谢iOS中内置的强大框架Core Animation, 该框架帮你实现动画的每一帧的绘制操作, 开发者要做的就是触发动画就可以了, 这就是几行代码的事.

UIView的哪些更新可以做动画

UIKit和Core Animation都支持动画, 但是两者支持的层次不一样. UIKit动画使用UIView来展示动画, view支持一套比较基础的动画, 这样足够你完成80%的工作了, 比如你可以使用view做属性更新的动画以及view的切换.

下表展示了UIView的哪些属性支持动画, 这些属性改变必须发生在UIView的Animation-block中,反正无法展示动画.

属性 更新
frame 改变这个属性,可以更新view的位置和大小(如果view的transform的值不为identity transform,此时使用bounds或者center来更新view的位置或者大小)
bounds 更新view的大小
center 更新view的位置
transform 放射变换,能根据view的center来对view做旋转(rotate),缩放(scale),位移(translate)等2D变换; 如果你想做对View做3D变换,那么请使用Core Animation对view的layer层加个相应的动画
alpha 更新view的透明度
backgroundColor 更新view的背景色

除了使用viewController对界面的进行切换,还是可以使用view的transition动画. 虽然你应该使用viewController来管理简单的view的层级树,但有时候你需要替换部分或者全部的层级树; 那么在这种情况下,你就需要使用view的transition动画来进行views的删除和添加.

如果你想对动画的掌控度更高,或者UIView提供的动画无法满足需求时,你就应该使用Core Animation了. 使用CoreAnimation你可以进行下面类型的layer动画:

  • 改变layer的size和position
  • 当进行了变换时(transformations),你可以使用center来改变位置
  • 对layer或者它sublayers做3D变换(transformations)
  • 对layer的层级树(类似view的层级树)进行添加/删除layer
  • layer在z-轴上次序
  • layer的阴影(shadow)
  • layer的边(包括layer是否为圆角)
  • layer的透明度(opacity)
  • layer是否对sublayer进行裁剪
  • layer的contents
  • layer的rasterization行为

注意:如果你的view中包含一个自定义layer对象(没有和view关联起来的layer),你必须使用CoreAnimation来给它做动画.

本文只讲解Core Animation少数几个初始化CoreAnimation的行为,如果想完全了解CoreAnimation或者想知道CoreAnimation是如何对layer进行动画展示的知识, 你可以参考Apple文档Core Animation Programming Guide和Core Animation Cookbook

如何对view进行属性更新的动画

想要对view做动画,你需要在Animation-block中写入关于属性变动的代码,在iOS4之前Animation-block指beging-end,iOS4之后指一个block类型. 苹果官方推荐以后尽量使用block, 具体如何操作请看下文的代码展示即可.

使用基于block的方法开启动画

iOS4之后,才能使用block方法开启动画. 下面展示了几个方法,不同的方法提供了不同的参数配置:

  • animationWithDuration:animations:
  • animationWithDuration:animations:completion:
  • animationWithDuration:delay:options:animations:completion:

因为这些都是类方法,不会和一个特定的view对象绑定,所以你可以在Animation-block中同时对多个view进行更新. 如下列代码所展示,在一秒内,一个view进行渐变隐藏,另一个view渐变显示. 当block中代码执行后,会立即开一个线程进行动画,这样不会阻塞主线程

[UIView animateWithDuration:1.0 animations:^{
        firstView.alpha = 0.0;
        secondView.alpha = 1.0;
}];

上面代码以一种慢进慢出(ease-in,ease-out)地方式进行动画,如果你想改变这种方式,你就必须使用animationWithDuration:delay:options:animations:completion:方法. 这个方法可以让你进行如下的配置:

  • 延迟多长时间才开始动画
  • 以何种方式进行动画(比如先快后慢UIViewAnimationOptionCurveEaseOut,慢进慢出等等UIViewAnimationOptionCurveEaseInOut,这个在iOS中叫时间曲线,也叫动画曲线)
  • 动画重复多少次(UIViewAnimationOptionRepeat)
  • 动画结束后是否要反向重复动画(UIViewAnimationOptionAutoreverse)
  • 当进行动画时是否要响应用户事件(UIViewAnimationOptionAllowUserInteraction),默认是不响应
  • 新开的动画是否应该等待之前的动画完成之后才开始还是中断之前的动画

animationWithDuration:animations:completion:animationWithDuration:delay:options:animations:completion:这两个方法都支持动画完成回调(completion-handle-blcok). 在completion-block中你可以告诉该动画结束了,在completion-block中还可以将两个独立的动画联系起来.

下面代码展示了completion-block的使用:首先启动一个动画,当第一个动画结束后,在它的completion-block中开启第二个动画,并延迟执行. completion-block通常用来将多个动画联系起来.

- (IBAction)showHideView:(id)sender {
    // 立刻渐变显示一个view
    [UIView animateWithDuration:1.0
        delay: 0.0
        options: UIViewAnimationOptionCurveEaseIn
        animations:^{
             thirdView.alpha = 0.0;
        }
        completion:^(BOOL finished){
            // 等待一秒后渐变隐藏同一个view
            [UIView animateWithDuration:1.0
                 delay: 1.0
                 options:UIViewAnimationOptionCurveEaseOut
                 animations:^{
                    thirdView.alpha = 1.0;
                 }
                 completion:nil];
        }];
}

注意:在属性改变动画过程中,如果你再次改变同一属性,当前的动画不会停止并且动画更新到你新设置属性的值.

使用Begin/Commit方法开启动画

如果你应用在iOS3.2或者iOS3.2之前的版本上跑,那么你必须使用beginAnimations:context:commitAnimations这些类方法来创建animation. 你的Animation-block代码必须写在哪两个方法之间, 同样也会开起新的线程去跑你的动画代码.

下面示例代码展示使用begin/commit方法来实现上文例子同样效果:

 [UIView beginAnimations:@"ToggleViews" context:nil];
    [UIView setAnimationDuration:1.0];
 
    // Make the animatable changes.
    firstView.alpha = 0.0;
    secondView.alpha = 1.0;
 
    // Commit the changes and perform the animation.
[UIView commitAnimations];

写在Animation-block中的属性更新代码,默认都是做动画的,但也可以通过setAnimationsEnabled:方法来禁止动画. 想开启动画用同样的方法进行设置即可,你可以调用areAnimationsEnabled方法来判断当前是否禁止动画.

注意:在属性改变动画过程中,如果你再次改变同一属性,当前的动画不会停止并且动画更新到你新设置属性的值.

如何配置Begin/Commit类型的动画

UIView提供许多类方法用来对Begin/Commit动画的配置,大部分方法应该只能在Begin/Commit中的Animation-block中调用,但也有部分方法也能在基于block动画的Animation-block中调用,如果没有调用相应的配置方法,系统会使用一个默认值(具体请看UIView的api). 下表列举了这些类方法和用法:

方法 用法
setAnimationStartDate:和setAnimationDelay: 这两个方法都是用来设置何时开始执行动画,如果设置的日期是过去的(或者设置delay为0)那么动画立即执行
setAnimationDuration: 动画执行多长时间
setAnimationCurve: 设置动画曲线,这个用来控制动画是线性执行还是变速执行
setAnimationRepeatCount:和setAnimationRepeatAutoreverses: 使用这两个方法来设置动画重复次数和动画是否自动反向执行
setAnimationDelegate:和setAnimationWillStartSelector:和setAnimationDidStopSelector: 使用这三个方法设置动画执行前/后调用的回调
setAnimationBeginsFromCurrentState: 调用这个方法会立即停止之前所有的动画,然后开启一个新的动画从当前状态开始执行,如果你传的是NO,新的动画不会立即执行,直到之前的动画执行完才会开始.

下面代码展示使用上面方法对动画进行配置:

// 开启第一个动画
- (IBAction)showHideView:(id)sender
{
    [UIView beginAnimations:@"ShowHideView" context:nil];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
    [UIView setAnimationDuration:1.0];
    [UIView setAnimationDelegate:self];
    [UIView setAnimationDidStopSelector:@selector(showHideDidStop:finished:context:)];
 
    // 动画属性更新
    thirdView.alpha = 0.0;
 
    // 提交Animation-block,开始执行动画
    [UIView commitAnimations];
}
 
// 第一个动画完成后执行
- (void)showHideDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
{
    [UIView beginAnimations:@"ShowHideView2" context:nil];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
    [UIView setAnimationDuration:1.0];
    [UIView setAnimationDelay:1.0];
 
    thirdView.alpha = 1.0;
 
    [UIView commitAnimations];
}

给你的动画设置一个delegate

你若想在动画执行的前/后执行一些代码,那么就必须给Begin/Commit动画用setAnimationDelegate:设置一个delegate,然后用setAnimationWillStartSelector:setAnimationDidStopSelector:设置start-selector和end-selector. 设置完这些后,动画系统会何时的时机自动调用你设置的selector.

你的提供selector的方法签名应该按照下面的格式提供:

- (void)animationWillStart:(NSString *)animationID context:(void *)context;
- (void)animationDidStop:(NSString *)animationID finished:(BOOL)finished context:(void *)context;

上面的animationID是你调用beginAnimations:context:时传入,用于表示某个动画. context用于给动画传入额外的信息,没有的话传nil即可.
setAnimationDidStopSelector:的selector需要一个额外的参数finished,YES标记动画执行完毕,NO标记该动画被cancel了或者被其他动画给终结了.

注意:尽管也可以给block动画设置一个delegate,但完全没必要啊,因为在Animation-block中开始地方可以执行任何动画之前的代码,在completion-block中执行动画结束的代码.

block动画的嵌套

动画嵌套是指在一个动画的Animation-block中开启一个新的动画. 嵌套动画同父动画同时启动,但根据自己配置的参数执行(大多数情况是这样). 默认情况下,嵌套的动画继承父动画的duration和animation-curve,虽然这些参数可以根据需求被重写. 使用嵌套可以对部分动画的配置(比如timing)进行修改

在下面的代码示例中,展示了使用动画嵌套来修改动画整体的部分动画的timing,duration等以及其他一些行为. 这个例子中,将两个view进行隐藏动画,然后anotherView会重复动画几次.
UIViewAnimationOptionOverrideInheritedCurveUIViewAnimationOptionOverrideInheritedDuration这两个key用来说明嵌套的动画可以修改curve和duration的配置,如果没有这两个key的话,只能继承外层动画的.

[UIView animateWithDuration:1.0
        delay: 1.0
        options:UIViewAnimationOptionCurveEaseOut
        animations:^{
            aView.alpha = 0.0;
 
            // Create a nested animation that has a different
            // duration, timing curve, and configuration.
            [UIView animateWithDuration:0.2
                 delay:0.0
                 options: UIViewAnimationOptionOverrideInheritedCurve |
                          UIViewAnimationOptionCurveLinear |
                          UIViewAnimationOptionOverrideInheritedDuration |
                          UIViewAnimationOptionRepeat |
                          UIViewAnimationOptionAutoreverse
                 animations:^{
                      [UIView setAnimationRepeatCount:2.5];
                      anotherView.alpha = 0.0;
                 }
                 completion:nil];
 
        }
        completion:nil];

如果是嵌套Begin/Commit动画,工作原理和block动画一样. 使用beginAnimations:context:开启一个动画,再一调用该方法就可以嵌套一个动画了,同理也是使用commitAnimations提交嵌套动画.

关于动画反转

结合repeat count可以创建一个反转动画. 对于repeatCount为整数的反转动画,动画会从original value->new value再从new value->original value为一次动画,动画执行整数次后,会立即从original->new,这可能不是你想要的结果,所以,你可以给整数repeatCount多加0.5次,那么最后0.5次动画是original-> new,这可能符合你的要求.

view的过渡动画(View Transitions)

view的过渡动画可以使view切换(添加/删除,隐藏/显示)过渡完成,而不至于那么突兀. 使用view的过渡可以做如下实现:

  • 改变view中的subview的隐藏状态,通常用来做局部更新.
  • 使用其他view替换当前整个view,通常用来做整屏更新.

注意:不要将view的过渡和view controller中的modal,show,push等视图控制器的切换弄混淆. view的过渡只作用于view的层级树,而视图控制器的切换在切换view的同时也会切换当前活动的视图控制. 所以view的过渡不会影响视图控制器的活动状态,当前视图控制器依然是活动状态. 关于更多视图控制切换的知识请看View Controller Programming Guide for iOS

改变view中的subview

  • 通过对view中的subview进行add/remove操作动画来切换view的状态可以温和地更新view的内容,从而不那么突兀. 当动画完成后,view还是原来那个,但内容变了.
  • 在iOS4及以后,你可以使用transitionWithview:duration:options:animations:completion:方法来对view做过渡动画. 在Animation-block中,你的修改应该是只是对subview的add/remove/show/hide操作. 该动画原理是默认给view的创建改变前后的两个截图,然后使用两个图片做动画,这样做更加高效. 如果不想这样,可以添加一个UIViewAnimationOptionAllowAnimatedContent选项,这样可以防止创建两个截图,而是动画展示所有改变.
  • 下面代码展示使用view的过渡动画显示一个添加一张空白页的效果. 在这个列子中,main view包含两个text view,一个隐藏一个显示, 当用户点击按钮创建一个新的页面时,代码切换两个text view的可见状态. 结果是显示了一个空白页,当过渡动画结束后,保存先前textview中的内容,然后清空内容等待下一次添加新页操作.
- (IBAction)displayNewPage:(id)sender {
    [UIView transitionWithView:self.view
        duration:1.0
        options:UIViewAnimationOptionTransitionCurlUp
        animations:^{
            currentTextView.hidden = YES;
            swapTextView.hidden = NO;
        }
        completion:^(BOOL finished){
            // Save the old text and then swap the views.
            [self saveNotes:temp];
 
            UIView*    temp = currentTextView;
            currentTextView = swapTextView;
            swapTextView = temp;
        }];
}

如果是iOS3.2及以前,你可以使用setAnimationTransition:forView:cache:来进行view的过渡. 下面代码展示该方法的简单使用,就是在执行过渡动画后需要执行stop-selector,你可以给动画设置delegate来完成:

[UIView beginAnimations:@"ToggleSiblings" context:nil];
    [UIView setAnimationTransition:UIViewAnimationTransitionCurlUp forView:self.view cache:YES];
    [UIView setAnimationDuration:1.0];
 
    // Make your changes
 
[UIView commitAnimations];

view的替换

  • 当你要更新整个界面时,可以直接把一个view替换掉. 因为和view Controller的切换很像,但不涉及controller的切换,所以在使用替换整个view的技术时,要合理的设计你的controller. 通常使用该技术来做view的简单切换.
  • iOS4及以后,你可以使用transitionFromView:toView:duration:options:completion:方法来做两个view间的切换动画.该方法会将fromView从层级树中移除,然后插入一个toView, 所以你应该保存一下fromView的引用. 如果你只想隐藏fromView而不是remove的话,你可以在选项中传入UIViewAnimationOptionShowHideTransitionViewkey.
  • 下面代码展示一个控制器中的两个view间的切换. 在该列子中,控制器的rootview包含两个subview(primaryViewsecondaryView),controller使用displayingPrimary跟踪当前显示的是那个subview.
- (IBAction)toggleMainViews:(id)sender {
    [UIView transitionFromView:(displayingPrimary ? primaryView : secondaryView)
        toView:(displayingPrimary ? secondaryView : primaryView)
        duration:1.0
        options:(displayingPrimary ? UIViewAnimationOptionTransitionFlipFromRight :
                    UIViewAnimationOptionTransitionFlipFromLeft)
        completion:^(BOOL finished) {
            if (finished) {
                displayingPrimary = !displayingPrimary;
            }
    }];
}

注意:在切换view的动画过程中,view所在的控制需要负责加载或者卸载primary view或者secondary view. 对于如何加载或卸载view的知识,本文不做讲解,读者可以自行阅读苹果文档View Controller Programming Guide for iOS

将多个动画连接起来

UIView的动画,可以将多个动画连接起来顺序执行.具体操作分为两种情况:

  • 如果是用block动画,使用animationanimateWithDuration:animations:completion:animateWithDuration:delay:options:animations:completion:方法提供的completion-block来添加接下来的动画
  • 如果是Begin/Commit动画,使用delegate提供一个did-stop selector,在selector代码中添加接下来的动画.

另外还可以使用动画的嵌套来链接两个动画,你需要设置后嵌套的动画的delay参数.

同时进行view动画和layer动画

  • 应用程序可以根据需要自由地混合基于视图和基于层的动画代码,但是配置动画参数的过程取决于layer。更改view的layer与改变view本身相同,并且任何应用于该layer属性的动画都遵循当前基于view的Animation-block中的动画参数。对于你自定义layer来说,却是不一样的。自定义layer对象忽略基于view的动画参数,而使用默认的核心动画参数。
  • 如果你想对layer动画的动画参数进行配置,那么你就必须使用Core Animation. 通常创建layer动画需要创建CABasicAnimation对象或者类CAAnimation的具体子类的对象. 配置该对象后,将其加入到相应的layer中,这一操作既可以在block动画中的内容进行也可以在外面进行.
  • 下面代码展示了同时进行view和layer的动画操作.在该例子中,在view的中心包含一个自定义的layer. 该动画逆时针旋转view同是顺时针旋转layer.因为旋转方向相反,所以layer相对于屏幕保持其原始方向,并且看起来不显著旋转。然而,该layer下方的view旋转360度,并返回到原来的方向。这个例子主要是演示如何混合view和layer动画。这种类型的混合不应该在需要精确定时的情况下使用。
[UIView animateWithDuration:1.0
    delay:0.0
    options: UIViewAnimationOptionCurveLinear
    animations:^{
        // Animate the first half of the view rotation.
        CGAffineTransform  xform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(-180));
        backingView.transform = xform;
 
        // Rotate the embedded CALayer in the opposite direction.
        CABasicAnimation*    layerAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
        layerAnimation.duration = 2.0;
        layerAnimation.beginTime = 0; //CACurrentMediaTime() + 1;
        layerAnimation.valueFunction = [CAValueFunction functionWithName:kCAValueFunctionRotateZ];
        layerAnimation.timingFunction = [CAMediaTimingFunction
                        functionWithName:kCAMediaTimingFunctionLinear];
        layerAnimation.fromValue = [NSNumber numberWithFloat:0.0];
        layerAnimation.toValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(360.0)];
        layerAnimation.byValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(180.0)];
        [manLayer addAnimation:layerAnimation forKey:@"layerAnimation"];
    }
    completion:^(BOOL finished){
        // Now do the second half of the view rotation.
        [UIView animateWithDuration:1.0
             delay: 0.0
             options: UIViewAnimationOptionCurveLinear
             animations:^{
                 CGAffineTransform  xform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(-359));
                 backingView.transform = xform;
             }
             completion:^(BOOL finished){
                 backingView.transform = CGAffineTransformIdentity;
         }];
}];

注意,上例中的CABasicAnimation对象的创建和添加也可以发生在Animation-block的外面,结果是一样的. 因为动画最终还是靠Core Animation来实现,所以只要保证在同一时间提交,那么两个动画肯定同时执行.

你可能感兴趣的:(iOS View 编程指导(四)-动画(Animation))