《iOS 三问》 -- iOS 动画之 View 动画

iOS 的动画实质上是在 CALayer 上实现的,但是在 UIView 上也提供了对底层动画的封装,使得我们可以很方便地直接对 UIView 进行一些简单的动画。使用 UIView 提供的动画机制,可以作用于这些属性上:alpha,backgroundColor,bounds,center,frame,transform。

1 Block 动画

block 动画是最简单的 View 动画,你只需要把要变化的属性在 UIView 提供的 animations:block 中即可,例如下面这段代码,将指定 view 动画为背景色渐变为红色,然后位置沿 x 轴右移 100 个象素点,并且在动画完成之后执行回调,将 view 的背景色修改为蓝色。

Block 动画
1
2
3
4
5
6
7
8
9
[UIView animateWithDuration:1 animations:^{
animView.backgroundColor = [UIColor redColor];
CGPoint p = animView.center;
p.x += 100;
animView.center = p;
} completion:^(BOOL finished) { //finish 表示动画是否真正完成了,因为有可能调回高时,动画被安排到下一个 runloop 执行
NSLog(@"completion finished? ==> %@",@(finished));
animView.backgroundColor = [UIColor blueColor];
}];

1.1 在 block 之外修改动画属性

iOS 动画概述 中我们分析过动画的实现细节,在重绘时刻 (redraw monment),动画才会被处理,而 View 本身的重绘也会发生在此刻,我们以下面几个实例也深入了解下。

下面的几个示例可以在我的 iOSOneDemo 中查看。

1.1.1 示例 1 - 在 block 中先移 100 再移 300

示例 1
1
2
3
4
5
6
7
8
9
[UIView animateWithDuration:1 animations:^{
CGPoint p = animView.center;
p.x = 100;
animView.center = p;

CGPoint p2 = animView.center;
p2.x = 300;
animView.center = p2;
}];

上面这段代码并不会有两个动画出现。在重绘时刻动画被处理时,同一个属性的修改会被覆盖,最终 animView 只会从当前位置移动到 x=300 的位置上去

1.1.2 示例 2 - 在动画 block 之前修改属性

示例 2
1
2
3
4
5
6
7
8
9
CGPoint p = animView.center;
p.x = 350;
animView.center = p;
[UIView animateWithDuration:1 animations:^{
animView.backgroundColor = [UIColor redColor];
CGPoint p = animView.center;
p.x = 200;
animView.center = p;
}];

最终看到的动画是,animView 会马上渲染在 x=350 的地方,然后再向左动画移至 200 处。因为在重绘阶段,发现 animView.center 被设到 350 了,所以会马上触发一次重绘更新 animView 在屏幕上的位置。然后执行动画 block,从当前位置也就是 x=350 的地方,移至 x=200 的地方。

1.1.3 示例 3 - 在动画 block 之后修改属性

示例 3
1
2
3
4
5
6
7
8
9
[UIView animateWithDuration:1 animations:^{
animView.backgroundColor = [UIColor redColor];
CGPoint p = animView.center;
p.x = 200;
animView.center = p;
}];
CGPoint p = animView.center;
p.x = 350;
animView.center = p;

这个最终看到的动画比较难想,animView 会马上移到 x=200 的位置,然后再动画右移至 x=350 的位置。为什么会这样呢?
我们可以想像一下在执行完这段代码后的动画处理,一开始的动画 block 是让 animView 从当前 position 移到 x=200 处,会设一个动画的起始值 (animView 的当前值) 和一个终止值 (x=200),但是 block 后又去 animView 的 center 属性进行了修改,导致动画的起始值变成当前值 (x=200), 终止值变成 (x=350)。所以在重绘阶段的动画就是从 x=200 处移动 x=350 处。

1.1.4 小结

鉴于这种在动画 block 前后修改同一属性的动画实际效果是啥比较烧脑,所以网上的议论都是禁止在动画 block 的前后去修改动画属性。如果非得修改属性的话,比如给要动画的 view 在动画之前设置一个初始值,可以使用 [UIView performWithoutAnimation:block]

performWithoutAnimation
1
2
3
4
5
6
7
8
[UIView animateWithDuration:1 animations:^{
animView.backgroundColor = [UIColor redColor];
[UIView performWithoutAnimation:^{
CGPoint p = animView.center;
p.x = 200;
animView.center = p;
}];
}];

这样,在 performWithoutAnimation:block 之中的属性修改将会独立于动画显示出来,比如上面的例子,view 会先放到 x=200 的地方,然后再做 backgroundColor 渐变的动画。

1.2 动画选项 options

block 动画可以使用 UIViewAnimationOptions 对动画进行配置,从而达到我们想要的效果。动画的设置主要是设置:

1
2
3
4
5
- duration  设置动画的持续时间
- delay 动画延迟开始的时间
- options UIViewAnimationOptions,动画播放选项(使用按位 (bitwise) 可以用 | 符叠加)
- animations 动画 block
- completion 动画完成后的回调

比如:

设置动画选项 options 开启一个无限循环往返的动画
1
2
3
4
5
6
7
8
9
10
11
NSUInteger op = UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat;
CGPoint originPoint = animView.center;
[UIView animateWithDuration:1 delay:0 options:op animations:^{
animView.backgroundColor = [UIColor redColor];
CGPoint p = animView.center;
p.x += 200;
animView.center = p;
} completion:^(BOOL finished) { // 使用 Autoreverse 的话要在 completion 里复原
animView.backgroundColor = [UIColor yellowColor];
animView.center = originPoint;
}];

1.2.1 小技巧 - 动画重复 N 次

动画重复 N 次
1
2
3
4
5
6
7
8
9
10
11
12
- (void)rotateView:(UIView *)view times:(int)times
{
times--;
[UIView animateWithDuration:1 animations:^{
CGAffineTransform t = view.transform;
view.transform = CGAffineTransformRotate(t, M_PI);
} completion:^(BOOL finished) {
if (times) {
[self rotateView:view times:(times)];
}
}];
}

1.3 block 动画嵌套

block 动画是可以嵌套的,考虑这种情形,同一个 View 有两个动画要同时开始,但是他们的配置不同(比如持续时长不同),这时就可以使用嵌套 block 动画。但是要使用上 UIViewAnimationOptionOverrideInheritedDuration 这个选项。

block 动画嵌套
1
2
3
4
5
6
7
8
9
10
[UIView animateWithDuration:2 animations:^{
CGPoint p = animView.center;
p.x += 100;
animView.center = p;
NSUInteger op = UIViewAnimationOptionOverrideInheritedDuration;

[UIView animateWithDuration:0.5 delay:0 options:op animations:^{
animView.backgroundColor = [UIColor blueColor];
} completion:nil];
}];

1.4 停止动画

这个很简单,使用 [animView.layer removeAllAnimations]; // 停止所有动画 即可。

2 弹性动画 (Spring Animation)

iOS7 中提供了强大的弹性动画选项,可以很方便地制作弹性动画,从而让你的交互更加生动。

主要用到 dampspringVelocity 两个属性,如下示例:

1
2
3
4
5
6
7
8
9
10
11
[UIView animateWithDuration:2
delay:0
usingSpringWithDamping:0.3 //damp 阻尼,小于 1 时才会有抖动动画。值越小抖动越大
initialSpringVelocity:20 //spring velocity: 抖动加速度
options:0
animations:^{
animView3.backgroundColor = [UIColor redColor];
CGPoint p = animView3.center;
p.x += 200;
animView3.center = p;
} completion:nil];

3 关键帧动画 (Keyframe Animation)

也是 iOS7 新增的动画方式。当你的动画是由多个动画串联的时候,你会怎样去组织他们?

在这之前一般是在一个动画的 complete block 中去开启下一个动画,这样会使得代码比较长而且不容易修改,比如中间要去掉一个动画你必须把一头一尾的 complete block 修改之。而关键帧动画就可以很好地解决这个问题 – 也就是说,关键帧动画主要负责多个动画的串联,如示例:

使用关键帧动画实现一个 View 左右移动的动画
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__block CGPoint currentPoint = animView.center;
// 整个动画的持续时间,由最外层的 duration 决定
[UIView animateKeyframesWithDuration:4 delay:0 options:0 animations:^{
[UIView addKeyframeWithRelativeStartTime:0 relativeDuration:0.25 animations:^{
currentPoint.x += 100;
animView.center = currentPoint;
}];

[UIView addKeyframeWithRelativeStartTime:0.25 relativeDuration:0.25 animations:^{
currentPoint.x -= 50;
animView.center = currentPoint;
}];

[UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.5 animations:^{
currentPoint.x += 200;
animView.center = currentPoint;
}];
} completion:nil];

其中主要两个方法

1
2
3
1. animateKeyframesWithDuration: 创建一个关键帧动画,创建整个关键帧动画的外壳,整体的持续时间与动画选项;

2. addKeyframeWithRelativeStartTime: 添加一个关键帧,其中的参数都是在整个关键帧动画中占的比例。

4 过渡动画 (Transition Animation)

就像电影画面从一个场景转到另一个场景,view 也可以设置这样的转场动画。具体的效果可以在我的 iOSOneDemo 中查看。

过渡动画比较简单,主要两个方法:

1
2
1. [UIView transitionWithView:] - 用于 view 自动的 content 变化过场动画
2. [UIView transitionFromView:] - 用于 view 到另一个 view 的过场动画

4.1 View 自身 Content 变化转场动画

1
2
3
4
5
6
animView.image = [UIImage imageNamed:@"sunny"];
[UIView transitionWithView:animView duration:1
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{
animView.image = [UIImage imageNamed:@"rain"];
} completion:nil];

上面的动画会导致当前阳光普照的图片向左翻转,转成一张下雨的图片,看起来就像是下雨的这张图片在它的背面一样。

4.2 从一个 view 到另一个 view 的过场动画

1
2
3
4
5
[UIView transitionFromView:view1
toView:view2
duration:2
options:UIViewAnimationOptionTransitionFlipFromLeft
completion:nil];

这样就完成了从 view1 变成 view2 的转场动画。值得注意的是,动画完成后,view1 已被 view2 替代,那么 view1 怎么处置呢?如果没有配置了 UIViewAnimationOptionShowHideTransitionViews 选项,那么 view1 会被 remove 掉,而 view2 是 add 进来。而如果使用了 UIViewAnimationOptionShowHideTransitionViews 选顶,view1 只是 hide,而 view2 只是 show(也就是说 view2 之前就要先被 add 进来)。

5 引用

【1】[Matt Neuburg - 《Programming iOS deep into views,view controllers and frameworks》]