这两天想大致学习下animation的使用方法。看了文顶顶的ios开发UI篇专题,写的很好,学习了很多。再摘录部分,以作备忘。
CALayer简介
简介
UIView之所以能显示在屏幕上,因为内部的layer。创建UIView的时候自动创建CALayer对象。UIView本身不具备显示的功能,拥有显示功能的是它内部的图层。
基本属性
通过操作这个CALayer对象,可以很方便地调整UIView的一些界面属性,比如:阴影、圆角大小、边框宽度和颜色等。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//设置边框的宽度为20
self.customView.layer.borderWidth=20;
//设置边框的颜色(borderColor是CGColor类型)
self.customView.layer.borderColor=[UIColor greenColor].CGColor;
//设置layer的圆角
self.customView.layer.cornerRadius=20;
//设置超过子图层的部分裁减掉
self.iconView.layer.masksToBounds=YES;
//在view的图层上添加一个image,contents表示接受内容
self.customView.layer.contents=(id)[UIImage imageNamed:@"cat"].CGImage;
//设置阴影的颜色
self.customView.layer.shadowColor=[UIColor blackColor].CGColor;
//设置阴影的偏移量,如果为正数,则代表为往右边偏移
self.customView.layer.shadowOffset=CGSizeMake(15, 5);
//设置阴影的透明度(0~1之间,0表示完全透明)
self.customView.layer.shadowOpacity=0.6;
//通过uiview设置(2D效果)
self.customView.transform=CGAffineTransformMakeTranslation(0, -100);
//通过layer来设置(3D效果,x,y,z三个方向)
self.iconView.layer.transform=CATransform3DMakeTranslation(100, 20, 0);
//旋转
self.iconView.layer.transform=CATransform3DMakeRotation(M_PI_4, 1, 1, 0.5);
注意,这里的颜色不是 UIColor 类型,而是 CGColor 类型。需要使用 [UIColor redColor].CGColor 的方式获取,而不是使用强制转型把 UIColor 转为 CGColor。
position,anchorPoint,bounds 以及 frame 的概念
- frame: 用来描述自己在父视图的位置,即 x y 是相对于父视图的起点的。
- bounds: 描述当前视图的左上角的相对子视图的坐标,以及视图大小的。一般
bounds.origin
为 (0, 0) ,如果把它改为 (-50, 0),就表示左上角的坐标为 (-50, 0),现在子视图相对的原点坐标就在左上角右边 50 的地方。所以相当于把所有子视图向右移动 50. - position:设置CALayer在父层中的位置,这个位置会和锚点重合。
- anchorPoint:决定着CALayer身上的哪个点会在position属性所指的位置。以自己的左上角为原点(0, 0),它的x、y取值范围都是0~1,默认值为(0.5, 0.5)。layer 的缩放和旋转,都是以 anchorPoint 为原点的。
position,anchorpoint 和 bounds 共同决定了视图的 frame。一般情况下,frame 的大小等于 bounds 的大小,但是如果视图旋转了,frame 会变为旋转视图的外界矩形的大小。
如果我们想移动一个视图的所有子视图,可以修改
bonuds.orgin
。不过我们也可以在视图上添加一个contentView
,然后移动它的 frame。
隐式动画
每个View内部都关联着一个Root Layer。所有非rootlayer都存在着隐式动画。隐式动画就是对于layer的部分属性进行修改时,默认会产生的一些动画。
常见的动画属性:
- bounds:用于设置CALayer的宽度和高度。修改这个属性会产生缩放动画
- backgroundColor:用于设置CALayer的背景色。修改这个属性会产生背景色的渐变动画
- position:用于设置CALayer的位置。修改这个属性会产生平移动画
创建图层
1 | - (void)viewDidLoad |
总结
对比CALayer,UIView多了一个事件处理的功能。也就是说,CALayer不能处理用户的触摸事件,而UIView可以。
如果显示出来的东西需要跟用户进行交互的话,用UIView;如果不需要跟用户进行交互,用UIView或者CALayer都可以
UIView的transform属性
transform是view的一个重要属性,它在矩阵层面上改变view的显⽰状态,能实现view的缩放、旋转、平移等功能。transform是CGAffineTransform
类型的。
transform结构
transform是一个CGAffineTransform
类型,结构如下:
1 | struct CGAffineTransform { |
CGAffineTransform实际上是一个矩阵
1 | | a, b, 0 | |
由于transform只有两维,需要一个3阶矩阵来表示其缩放以及平移的变化。
坐标变换过程:
1 | | a, b, 0 | |
这个矩阵的第三列是固定的,所以每次变换时,只需传入前两列的六个参数[a,b,c,d,tx,ty]即可。
transform方法
在CGAffineTransform
的生成函数中,大多是两两对应的,一个带
make字样,一个不带。带make字样的是直接生成一个新的CGAffineTransform
,不带make字样的则是在一个CGAffineTransform
的基础上生成新的。函数返回值均是CGAffineTransform
类型。
多个CGAffineTransform
对象赋给view,最终只执行最后一个动画,多个动画需要组合在一起。
scale
实现的是放大和缩小:
1 | CGAffineTransformScale(CGAffineTransform t, |
生成新的transform相当于将t' = [sx ,0 ,0,sy ,0, 0]
这六个参数代入矩阵中,即改变a和d。
rotate
实现的是旋转:
1 | CGAffineTransformRotate(CGAffineTransform t, |
angle为角度,angle=π则旋转180度。矩阵的六个参数为t' = [ cos(angle),sin(angle),-sin(angle),cos(angle) 0,0];
translate
实现的是平移:
1 | CGAffineTransformTranslate(CGAffineTransform t, |
矩阵的六个参数为t' = [1,0,0,1,tx,ty] ;
代入公式,xn=x+tx,yn=y+ty
复原
1 | view.transform=CGAffineTransformIdentity; |
上述的各种动画的变化都是以原始图像为基准的,而不是在变化后继续变化。CGAffineTransformIdentity
将view从当前状态复原回view最初始的状态。
一个技巧。如果使用 AutoLayout 的视图要添加动画,一般我们会把约束拖到代码中,然后在动画前修改约束,在动画的 block 中调用其父控件的
layoutIfNeeded
。还有一种更好的办法,在动画的 block 中直接用 view 的 transform 属性。
设置 transform 进行动画和 设置 frame 动画的区别
视图的 frame 其实是由视图的 bounds,position,anchorpoint 以及 transform 共同决定的。修改 frame 其实就是修改 bounds 和 position 的值。
一般而言,最好使用 transform 进行动画,因为有一种情况是,如果该视图具有子视图,并且要进行缩放动画。那么执行动画的时候,动画虽然有一个过程,但其实从动画一开始,frame就已经修改了。如果直接设置frame,那么开始的时候,子视图就会按变化后的frame来重新布局,而不是跟随父视图一起慢慢变化。
而 transform 动画的话,不会调用控件的 layoutsubview
方法,整个过程更像是把整个视图按比例缩放,子视图会跟着父视图一起缩放。
CALayer的transform属性
transform结构
CALayer的transform是一个CATransform3D
结构:
1 | struct CATransform3D |
有别于CGAffineTransform
,CATransform3D
是一个三维变化,需要一个4阶矩阵表示。其他类似,再次不表。
transform方法
CALayer的transform方法和View的transform基本一致。举几点不同:
- CALayer由于有z轴,因此对不同图层使用
CATransform3DMakeTranslation (CGFloat tx, CGFloat ty, CGFloat tz)
方法的时候可以通过改变tz的值,来实现图层的覆盖。对于tz来说,值越大,那么图层就越往外(接近屏幕),值越小,图层越往里(屏幕里)。 - 由于图像是从正面投影,直接绕着x或y轴旋转达不到透视的效果,如果要达到透视效果,需要改变
m34
(其实改变m14,m24也能达到效果,可以自行通过行列式推导。),再对图层进行旋转。m34
的值可以根据需要实现的效果推导得到,不直接的方法还是直接试。 - 如果想要直接改变矩阵里的值,可以先使用
CATransform3DIdentity
的方式,初始化一个CATransform3D
实例,然后再赋值。
CAShapeLayer
CAShapeLayer
继承自CALayer。CAShapeLayer
是在坐标系内绘制贝塞尔曲线的,通过绘制贝塞尔曲线,设置shape(形状)的path(路径),从而绘制各种各样的图形以及不规则图形。因此,使用CAShapeLayer
需要与UIBezierPath
一起使用。
创建
1 | CAShapeLayer *layer = [CAShapeLayer layer]; |
属性
1 | // 要呈现的路径 |
创建贝塞尔曲线
这里只是给了几个图形的基本画法,还有更多图形的画法可以稍后查看
1 | //绘制矩形 |
曲线动画
曲线动画的主要思想是对 layer 的 strokeEnd
属性做 CABasicAnimation
动画:
1 | UIBezierPath *path = [UIBezierPath bezierPath]; |
Mask属性
基本使用
layer的大小和形状是受到mask遮罩层的影响的,可以通过赋给mask层一个新layer,来实现改变layer形状的效果。mask图层的 Color 属性是无关紧要的(mask不是透明的部分,layer能显示出原来的颜色),真正重要的是图层的轮廓。
下面的例子中,为一个图片设置了圆形的蒙版。蒙版外的部分是透明的,该部分图片不予显示。
1 | - (CALayer *)maskRadiusCorner:(UIImageView *)imageView{ |
效果图如下:
再举一个例子,观察下图的实现方式:
原理是将彩色的图片添加在灰色的图片上,对彩色的图片添加一个圆形的蒙版。圆形蒙版外的部分由于是透明的所以就不予显示,也就是下面的灰色图片。圆形蒙版内的部分由于设置了颜色,就能够显示彩色图片。
1 | /** |
绘制只有两个圆角的视图
有些情况下,一个 button 或者 label,只要右边的两个角圆角,或者只要一个圆角。该怎么办呢?
1 | CGRect rect = CGRectMake(0, 0, 100, 50); |
关于离屏渲染
渲染机制
CPU 计算内容交由 GPU 渲染,GPU 渲染完成后放入帧缓冲区。随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。
GPU 渲染方式
- On-Screen Rendering:意为当前屏幕渲染。GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。
- Off-Screen Rendering:意为离屏渲染。GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
其实非当前屏幕缓冲区的渲染都叫做离屏渲染,CPU 渲染得到 bitmap 然后交由 GPU 显示也是一种特殊的离屏渲染,此举会消耗一定的 CPU 资源。但是如果 GPU 资源紧张,同时 CPU 空闲,可以考虑如此优化,以消除 GPU 不能及时渲染导致的丢帧的影响。
所以离屏渲染不一定就是影响性能的。只是渲染方式的一种。
离屏渲染的触发
- shouldRasterize(光栅化)
- masks(遮罩)
- shadows(阴影)
- edge antialiasing(抗锯齿)
- group opacity(不透明)
其中,光栅化是把GPU的操作转到CPU上,严格意义上是”软件渲染”,将图转化为一个个栅格组成的图象。并且缓存起来,减少渲染的频度。对于基本不会变化的视图,开启光栅化有助于性能优化,但是如果内容经常变化,那么就会造成性能浪费。
为什么会产生离屏渲染
当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制。
这是因为 GPU 无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分。因为在这一层之前的若干层layer像素数据,已经在渲染中被永久覆盖了。这就意味着,对于每一层layer,要么能找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个临时中转区域来完成一些更复杂的、多次的修改/剪裁操作。
视图圆角
设置视图的 cornerRadius
本身并不会触发离屏渲染,需要和 maskToBounds
一起设置才会产生。
一般情况下,直接设置圆角即可。
1 | UIView *view = [[UIView alloc] init]; |
但是 UILabel 例外,UILabel 如果有背景色需要设置 layer 的背景色:
1 | UILabel *label = [[UILabel alloc] init]; |
UIImageView 无法做到直接隐藏图片圆角。所以需要自己绘制出一个带圆角的图片:
1 | extension UIImage { |
这种方式如果不做缓存,每次都会创建新的 UIImage,加重 CPU 的负担。
另外,还有一种使用贝塞尔曲线,利用CALayer层绘制指定圆角样式的mask遮盖View 的方式达到圆角效果的。不过这种方式也是操作 mask,会产生离屏渲染,但是效果会比直接设置圆角要好很多。
这样是把 GPU 的任务转给了 CPU 去完成。那么如果还是掉帧怎么办?
- 直接让 UI 将图片切为圆角
- 在原来的视图上添加一个四个角有颜色中间透明的图片,遮盖到原来图片上。这样是通过混合图层的方式实现。损耗的性能会好很多。
- 将上面绘制圆角图片的过程放到子线程中去,绘制完成后回到主线程中。
针对上面第二点:添加一个四个角有颜色中间透明的图片。相关代码如下:
1 | @implementation UIImage (XWAddForRoundedCorner) |
添加shadow的离屏渲染
我们直接设置 shadow 相关属性会产生离屏渲染:
1 | let layer = view.layer |
只要你提前告诉CoreAnimation你要渲染的View的形状Shape,就会减少离屏渲染计算。因此,我们需要加上设置 shadowPath
的一行:
1 | let layer = view.layer |