Core Animation
iOS系统的核心地位:应用内和应用间都会应用(多个程序间的手势切换)SpringBoard→BackBoard。
应用内的工作:Core Animation Pipeline管线(layout,display,preload,commit)。
应用外的工作:提交到Render Server服务,反序列化构建渲染树GPU的工作:提交到GPU进行合成和渲染,输出到缓冲区。
注1:代码只能控制CPU处理中的管线中的布局和显示两个阶段(通过setNeedLayout和setNeedDisplay)(setNeedDisplayInRect还能设置脏矩形来减少不必要的绘制)。
注2:CPU偏计算;而图像的处理和渲染,用硬件会更快,因为GPU对图像计算进行了优化。
隐式动画
概念:更改一个CALayer的可做动画属性,Core Animation会执行一个隐式动画,平滑过渡到新值。动画的执行时间取决于事务的设置,动画的类型取决于图层行为。
实现:layer属性更改时会调用actionForKey方法,然后执行actionForLayer:forKey:代理方法(UIView禁用隐式动画的原理),如果没有实现代理,就查找属性-名称的actions映射字典和style字典,最后直接调defaultActionForKey。查找完actionForKey方法会返回一个CAAction做动画或NULL不做动画。
时机:由事务的管理机制控制。
注:UIView更改默认没有隐式动画,因为UIKit禁用了:UIView实现了CALayerDelegate的-actionForLayer:forKey方法,当属性的更改不在一个动画块中时,所有图层行为都会返回null,以此来禁用隐式动画。
显式动画
CABasicAnimation和CAKeyframeAnimation
虚拟属性:keyPath = "transform.scale .position .rotation",不用创建CATransform3D指定动画变换。
[layer removeAnimationForKey]; // 通过key在动画过程中取消动画。
动画的暂停
CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
layer.speed = 0.0;
layer.timeOffset = pausedTime;
动画的继续
CFTimeInterval pausedTime = [layer timeOffset];
layer.speed = 1.0;
layer.timeOffset = 0.0;
layer.beginTime = 0.0;
CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
layer.beginTime = timeSincePause;
事务
事务是一系列属性动画集合,通过CATransaction管理事务组。CATransaction没有属性和实例方法,只能通过+begin和+commit管理出入栈,+setAnimationDuration管理事务的动画时长和+animationDuration获取值(默认0.25),setCompletionBlock:可以设置事务完成后的回调。
事务的管理机制
每个RunLoop周期都会自动开始一个新的事务,把更改的属性收集起来做一次默认0.25s的隐式动画。
注:一般手动更改隐式动画的时间,需要新开事务避免影响别的动画(如屏幕旋转)。
UIView的动画
UIView的begin/commitAnimation底层原理也是设置了CATransaction。
UIView的animateWithDuration:animations:的Block动画方法其实也是内部自动调用了CATransaction的begin和commit,这样Block中的属性改变都会被事务所包含。
呈现树
presentationLayer其实更改图层属性后,属性值立刻更新,但屏幕没有马上发生改变,因为它只是定义了图层动画结束后将要变化的外观,即图层树模型。但在设置新值后和新值生效前,屏幕渲染的过程中,当前显示的属性值,在某些场景下更有意义:1、在基于定时器的动画过程中获取当前图层的准确位置;2、在做动画的图层中,通过hitTest响应用户交互。(touchesBegan/Target-Action中,判断触摸点是否在presentationLayer内)
注:关键方法:locationInView / convertPoint->Point转化触摸点坐标系,presentationLayer -> CALayer获取呈现树中的视图位置,hitTest -> CALayer/ Contains-> Bool返回触摸的Layer / 判断触摸点是否在Layer上。
图片IO
imageWithContentsOfFile方法加载本地目录的图片文件,会在加载到内存之后才进行解码解压,所以会在绘制的时候影响性能,且不会进行内存缓存,适用于读取一次性大图的场景imageNamed可以避免延迟加载,会马上解压并加载到内存中缓存起来,但是只对assets中的资源有效。
强制解压和提前渲染
使用CGContext从上下文中绘制一个新的图片(直接绘制成CGImageRef位图)来代替(或绘制成一个点也会解压但不会节省绘制时间),优化了因解压增加的绘制时间,还可以丢弃解压后的图片节省内存,但需要更复杂的计算,CPU消耗总量增加。
总结:使用CGContext异步重绘图片,得到解压缩后的位图。
UIView和CALayer
View持有Layer用于显示,View可以响应交互负责管理,View的属性大部分从Layer映射而来,UIView支持自动布局;Layer的delegate是View,动画会通知View,Layer更轻量级。
提供View和layer两个平行的层级关系是为了职责分离,避免重复代码。
frame和bounds
frame:外部坐标,在父图层占据的位置bounds:内部坐标本地坐标系,作用于子视图,左上角通常是{0, 0}(通过更改bounds实现简单的scrollView)
注:frame是一个虚拟属性,是根据bounds+position+transform计算而来;当对图层进行旋转或缩放时,frame实际是变换后,补齐的正矩形区域,即宽高可能和bounds不一致。
anchorPoint
锚点,UIView未暴露的属性,默认中心{0.5, 0.5},左上角{0, 0} 右下角{1, 1},小于0大于1将放置在图层外。UIView的Center和CALayer的Position属性加上锚点,才是图层的实际位置,所以图层锚点更改,frame会发生变化。
masksToBounds
Layer属性,设置为YES截取子图层实现圆角(cornerRadius);设为NO避免截取阴影(shadowOpacity,shadowColor,shadowOffset,shadowRadius,shadowPath-CGPath)。
mask蒙版
Layer属性,定义了父视图的可见区域,mask图层的颜色不重要,实心部分将会保留,其他隐藏
CGAffineTransform
UIView 仿射变换,仅支持2D变换:MakeRotation,Scale,Translation等。
CATransform3D
CALayer 3D变换,方法和仿射变换类似,加入zPosition形成4*4矩阵。
CAShapeLayer
基于CGPath路径创建CAShapeLayer矢量图形(非CG的Bitmap),bezierPathWithRoundedRect: byRoundingCorners: cornerRadii:可以单独指定每个角的圆角,作为蒙版为宿主层加圆角(mask)。
AVPlayerLayer
AVFoundation提供,和CA紧密结合,是CALayer的子类。AVPlayer + AVPlayerLayer简单的播放视频。
UIView的布局更新和重绘机制
-loadViewIfNeeded
VC初始化后,强制提前执行viewdidload加载控件和数据装载。
-layoutSubviews
此函数用于,父视图的布局、大小等发生了改变,你想要同时更新其子视图的布局时,更新子视图布局的代码应写在父类重写后的layoutSubviews里(它默认不做任何事),但这时就不能在外部设置子视图的布局了。
注:初始化含frame(且显示出来)或改变frame,addSubview,滚动UIScrollView,旋转屏幕,改变子view的大小会触发layoutSubviews。
-setNeedsLayout
设置布局刷新标记——标记为需要重新布局。此方法会异步触发layoutSubviews(执行完成后刷新标记又置为NO),如果想立刻触发,应立刻调用layoutIfNeeded。
-layoutIfNeeded(写在animation内,用于实现更新约束时的动画效果)
view.layoutIfNeeded()后frame及坐标为约束完成后的值,可用于viewdidload中获取准确的frame。
配合setNeedsLayout,在有刷新标记的情况下立刻触发layoutSubviews。但在view的init方法后、第一次显示前(addSubview前),标记是“需要刷新”的。
self.testView= [[LayoutTestViewalloc]initWithFrame:CGRectMake(0,0,300,300)]; //初始化会调用两次setNeedsDisplay
//此时视图标记是“需要刷新”的,直接调用[self.testView layoutIfNeeded];会触发layoutSubviews,但之后的addSubview就不触发了
[self.view addSubview:self.testView]; //默认触发layoutSubviews
self.testView.frame = CGRectMake(0,0,500,500); //改变frame会再次触发layoutSubviews()
- (void)click {
[self.testView setNeedsLayout]; //标记为可刷新且异步触发layoutSubviews
[self.testView layoutIfNeeded]; //立刻触发layoutSubviews
//只触发一次
}
Drawing and Updating the View
-drawRect:(CGRect)rect(内存杀手)
view首次显示或调用setNeedsDisplay时执行。
重写此方法,执行自定义绘制内容任务。默认不起作用。永远不要直接调用drawRect。重写此方法多用于UIView结合CGContextRef的手动绘图(与CALayer对应的是drawInContext)
CGContextRef ctx =UIGraphicsGetCurrentContext(); //获取当前画板(图形上下文,只在UIView的drawRect中有效)(还可以使用)
注:view重写drawRect后如果不指定背景色或把opaque设成NO背景将变黑。
实际需求:1、在当前画板上画图像,曲线、虚线、几何图形、写字等等。
2、UITextView添加自定义placeholder的需求中,将holder的label添加在写drawRect内,设置holder字符串时调用setNeedsDisplay重绘UITextView。
-setNeedDisplay
在下一个绘画周期(1/60秒后)主动触发drawRect执行重绘。setNeedDisplayInRect方法可限定在rect范围内重绘。一般结合重写drawRect来使用。