图层树
Core Animation是一个复合引擎,它的职责是尽可能快地组合屏幕上不同的可视内容。这些内容被分解成独立的图层,存储在一个叫图层树的体系中。在屏幕上所看见的一切内容,在底层的实现其实就是一个或多个图层树。
图层与视图
真正现在屏幕上显示和做动画的并不是UIView,而是UIVIew所对应的一个CALayer实例(backing layer)。UIview的职责只是创建并管理这个layer,以确保当子视图在层级关系中添加或者被移除的时候,它们对应的图层也在图层树中有相应的操作。UIView仅仅是对CALayer的一个封装,它提供了对用户交互的处理和Core Animation底层方法的高级接口。
事实上除了视图层级和图层树之外,还存在呈现树和渲染树,它们每一个都扮演着不同的角色。
图层的能力
- 阴影,圆角,带颜色的边框
- 3D变换
- 非矩形范围
- 透明遮罩
- 多级非线性动画
寄宿图
contents属性
为layer添加一张寄宿图:
layer.contents = (__bridge id)image.CGImage;
设置寄宿图显示模式:
layer.contentsGravity = kCAGravityResizeAspect;
设置图片缩放比:
layer.contentsScale = [UIScreen mainScreen].scale;
设置超出部分是否显示:
layer.masksToBounds = YES;
设置图片显示区域:
layer.contentsRect = CGRectMake(0, 0, 0.5, 0.5);
设置固定边框和拉伸区域:
layer.contentsCenter = CGRectMake(0.25, 0.25, 0.5, 0.5);
自定义绘制
实现UIView的
-drawRect:
方法
当视图显示在屏幕上的时候-drawRect:
方法就会被调用,它当中的代码就会利用Core Graphics绘制一个寄宿图,然后内容将会缓存起来直到它需要被更新(开发者调用了setNeedsDisplay
方法,或者影响视图表象效果的属性被改变时,视图将会被自动重绘,如bounds
属性)。-
实现CALayer的非正式协议
CALayerDelegate
当CALayer需要被重绘时,它会通过调用下面的方法来请求它的代理给它一个寄宿图来显示。
(void)displayLayer:(CALayerCALayer *)layer;
在上面的代理方法中,通过设置layer的contents属性,就可以设置一个寄宿图。如果代理没有实现
displayLayer
方法,CALayer会转而尝试调用下面这个方法:- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
在调用这个方法之前,CALayer创建了一个合适尺寸和空寄宿图(尺寸有bounds和contentsScale决定)和一个Core Graphics的绘制上下文环境,为绘制寄宿图做准备。
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //create sublayer CALayer *blueLayer = [CALayer layer]; blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); blueLayer.backgroundColor = [UIColor blueColor].CGColor; //set controller as layer delegate blueLayer.delegate = self; //ensure that layer backing image uses correct scale blueLayer.contentsScale = [UIScreen mainScreen].scale; //add layer to our view [self.layerView.layer addSublayer:blueLayer]; //force layer to redraw [blueLayer display]; } - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx { //draw a thick red circle CGContextSetLineWidth(ctx, 10.0f); CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor); CGContextStrokeEllipseInRect(ctx, layer.bounds); } @end
需要注意的事情:
- 当图层显示在屏幕上时,CALayer不会自动重绘它的内容,需要开发者显示调用
-display
方法。 - 当使用CALayerDelegate绘制寄宿图时,不会对超出图层边界外的内容提供绘制支持。
- 当图层显示在屏幕上时,CALayer不会自动重绘它的内容,需要开发者显示调用
图层几何学
布局
与UIView的center
属性对应的是CALayer中的position
属性,他们都代表了相对于父图层anchorPoint
所在的位置。而不是视觉上所观察到的图层中心的位置,在anchorPoint
的值不为图层的中心时,center
和position
的值将会和视觉上图层中心点的值不一致。
对于视图和图层来说,frame
其实是一个虚拟的属性,是根据bounds
,position
和transform
计算而来,所以当其中任何一个值发生改变,frame
都会变化。相反,改变frame
的值同样会影响它们的值。
当对图层做变换的时候(如旋转/缩放),frame
实际上代表了在图层旋转之后整个轴对其的矩形区域。此时,frame
的宽高和bounds
的宽高并不一致。
锚点(anchorPoint)
图层的anchorPoint
属性可以看做是图层上一个不动的点,当图层进行旋转等操作的时候都是以此点为圆心进行的,类似于用一个钉子将纸钉在平面上,无论如何旋转,钉子的位置是不会动的。在默认情况下anchorPoint
位于图层的中心(0.5, 0.5)。
坐标系
CALayer给不同坐标系之间的图层转换提供了一些工具类方法:
- (CGPoint)convertPoint:(CGPoint)point fromLayer:(CALayer *)layer;
- (CGPoint)convertPoint:(CGPoint)point toLayer:(CALayer *)layer;
- (CGRect)convertRect:(CGRect)rect fromLayer:(CALayer *)layer;
- (CGRect)convertRect:(CGRect)rect toLayer:(CALayer *)layer;
这些方法可以把定义在一个图层坐标系下的点或者矩形转换成另一个图层坐标系下的点或者矩形。
垂直翻转子图层:
layer.geometryFlipped = YES;
Hit Testing
判断触摸点是否在图层内:
BOOL isContain = [layer containsPoint:point];
返回图层本身,或者包含这个坐标点的叶子节点图层:
CALayer *layer = [layer hitTest:point];
自动布局
当使用视图的时候,可以充分利用UIView类接口暴露出来的UIViewAutoresizingMask和NSLayoutConstraintAPI,但如果想随意控制CALayer的布局,就需要手工操作。
图层手动布局:
- (void)layoutSublayersOfLayer:(CALayer *)layer;
当图层的bounds发生改变,或者图层的-setNeedsLayout方法被调用的时候,这个函数将会被执行。这使得我们可以手动地重新摆放或者重新调整子图层的大小,但是不能像UIView的autoresizingMask和constraints属性做到自适应屏幕旋转。
视觉效果
圆角
设置图层角曲率(默认为0):
layer.conrnerRadius = 5.0f
图层边框
设置图层边框宽度和颜色:
layer.borderWidth = 3.0f;
layer.borderColor = [UIColor greenColor].CGColor;
阴影
设置图层阴影颜色,方向和距离,模糊度
layer.shadowColor = [UIColor orangeColor].CGColor;
layer.shadowOffset = CGSizeMake(50, 50);
layer.shadowRadius = 10;
当图层的masksToBounds
属性值设置为YES
时,所有从图层中突出来的内容都会被剪裁掉。因此阴影效果将会失效。想要为这样的图层添加阴影,就需要在要添加阴影的图层范围上覆盖一个只画阴影的空图层。
使用shadowPath
指定任意形状的阴影:
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView1;
@property (nonatomic, weak) IBOutlet UIView *layerView2;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//enable layer shadows
self.layerView1.layer.shadowOpacity = 0.5f;
self.layerView2.layer.shadowOpacity = 0.5f;
//create a square shadow
CGMutablePathRef squarePath = CGPathCreateMutable();
CGPathAddRect(squarePath, NULL, self.layerView1.bounds);
self.layerView1.layer.shadowPath = squarePath; CGPathRelease(squarePath);
//create a circular shadow
CGMutablePathRef circlePath = CGPathCreateMutable();
CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds);
self.layerView2.layer.shadowPath = circlePath; CGPathRelease(circlePath);
}
@end
图层蒙版
设置图层蒙版:
//create mask layer
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = self.layerView.bounds;
UIImage *maskImage = [UIImage imageNamed:@"Cone.png"];
maskLayer.contents = (__bridge id)maskImage.CGImage;
//apply mask to image layer
self.imageView.layer.mask = maskLayer;
拉伸过滤
设置图层拉伸过滤算法:
view.layer.magnificationFilter = kCAFilterNearest;
透明度
透明度混合叠加问题:当现实一个透明度为50%的图层时,图层的每个像素都会一半现实自己的颜色,另一半显示图层下面的颜色。当图层包含一个同样50%透明的子图层时,所看到的视图,50%来自子视图,25%来自图层本身的颜色,另外25%则来自背景色。这时子视图的可见的则为75%,显示效果将会相当糟糕。
理想状态下,设置一个图层的透明度,是希望它所包含的图层树向一个整体一样的透明效果。这可以通过设置Info.plist文件中的UIViewGroupOpacity为YES来达到这个效果,但是这个设置会影响到这个应用,整个app可能会受到不良影响。
另一个方法就是设置CALayer的一个叫做shouldRasterize属性来实现组透明的效果,如果它被设置为YES,在应用透明度实现之前,图层及其子图层都会被整合成一个整体的图片,这样就没有透明度混合的问题了。
为了启用shouldRasterize属性,需要图层的rasterizationScale属性。默认情况下,所有图层拉伸都是1.0, 所以如果使用了shouldRasterize属性,就要确保你设置了rasterizationScale属性去匹配屏幕,以防止出现Retina屏幕像素化的问题。
layer.shouldRasterize = YES;
layer.rasterizationScale = [UIScreen mainScreen].scale;