Chapter 1
</br>
Core Animation不是一个严谨的框架。CAAnimation
才是一个框架名,它是QuartzCore
的一部分。
Core Animation是用C写的,所以要注意类型转换,用“无缝桥接” __bridge
。
</br>
UIView
是视图,CALayer
是图层。
</br>
UIView
和 CALayer
的关系:
UIView
处理用户交互,CALayer
不管。每个
UIView
都有一个CALayer
实例的图层属性,不能在Interface Builder中设置UIView
的layer
属性。
</br>
处理视图比单独处理图层更方便。
Chapter 2
</br>
知识点:
-
CALayer
的contents
属性 -
UIView
的contentMode
属性和CALayer
的contentsGravity
属性 -
CALayer
的contentsScale
属性 -
UIView
的clipsToBounds
属性和CALayer
的maskToBounds
属性 -
CALayer
的contentsRect
属性 -
CALayer
的contentsCenter
属性(可在Interface Builder
中设置) -
CALayer
的delegate
属性,遵循CALayerDelegate
-
CALayer
的-displayer
方法
</br>
寄宿图(图层中包含的图):
CALayer
的contents
属性
UIImage *image = [UIImage imageNamed:@"picture"];
self.layerView.layer.contents = (__bridge id)(image.CGImage);
这样的寄宿图会在图层中被拉伸以适应视图的大小,可以加上下面这句,让显示的图片适应原来的比例
self.layerView.contentMode = UIViewContentModeScaleAspectFit;
由此可见,UIView
大多数视觉相关的属性比如上面用的contentMode
,对这些属性的操作其实是对对应图层的操作。
CALayer
的contentMode
属性叫contentsGravity
self.layerView.layer.contentsGravity = kCAGravityResizeAspect;
可以达到像刚才设置UIView
的contentMode
属性同样的效果。
注意防止用错误的contentsScale
去显示Retina图片。CGImage
没有UIImage
中的拉伸概念。当用UIImage
读取的图片都是Retina图片,在UIImage
转换为CGImage
时,拉伸的元素将会丢失。contentsScale
默认值为1.0
,如果把contentsGravity
属性设为kCAGravityCenter
,不设置contentsScale
(即采用默认值),就会出现这种错误现实Retina图片的情况。
正确做法:
UIImage *image = [UIImage imageNamed:@"picture"];
self.layerView.layer.contents = (__bridge id)(image.CGImage);
self.layerView.layer.contentsGravity = kCAGravityCenter;
self.layerView.layer.contentsScale = image.scale;
默认情况下,UIView
和CALayer
都会绘制超过边界的内容或子视图。
只要设置UIView
的属性clipsToBounds
或CALayer
的maskToBounds
设置为YES
。就不会超出边界了。
self.layerView.clipsToBounds = YES;
//或
self.layerView.layer.maskToBounds = YES;
除了可以使用CALayer
的contents
属性设置寄宿图,还可以-drawRect
方法自定义绘图,此方法没有默认的实现,因为UIView不一定有寄宿图,如果不需要寄宿图,就不要实现此方法,否则会浪费资源。
CALayer
要显式调用-display
方法,当图层显示在屏幕上时,CALayer
不会自动重绘它的内容。它把重绘的决定权交给了开发者。
没有用masksToBounds
属性,绘制的那个圆仍然沿边界被裁剪了。这是因为使用CALayerDelegate
绘制寄宿图的时候,并没有对超出边界外的内容提供绘制支持。
除非你创建了一个单独的图层,你几乎没有机会用到CALayerDelegate协议。因为当UIView创建了它的宿主图层时,它就会自动地把图层的delegate设置为它自己,并提供了一个-displayLayer:的实现,那所有的问题就都没了。
但是,当使用寄宿了视图的图层的时候,也不必实现-displayLayer:
和-drawLayer:inContext:
方法来绘制你的寄宿图。通常做法是实现UIView
的-drawRect:
方法,UIView
就会帮你做完剩下的工作,包括在需要重绘的时候调用-display
方法。
</br>
Chapter 3
</br>
知识点:
-
UIView
的frame
、bounds
、center
属性和CALayer
的frame
、bounds
、position
属性 -
CALayer
的anchorPoint
属性 -
CALayer
的zPosition
属性 -
CALayer
的-containsPoint:
方法和-hitTest:
方法
布局
UIView
三个重要布局属性:frame
、bounds
、center
对应CALayer
的frame
、bounds
、position
。
frame
代表了图层的外部坐标(也就是在父图层上占据的空间),bounds
是内部坐标({0, 0}通常是图层的左上角),center
和position
都代表了相对于父图anchorPoint
所在的位置。
视图的frame
,bounds
和center
属性仅仅是存取方法,当操纵视图的frame
,实际上是在改变位于视图下方CALayer
的frame
,不能够独立于图层之外改变视图的frame
。对于视图或者图层来说,frame
并不是一个非常清晰的属性,它其实是一个虚拟属性,是根据bounds
,position
和transform
计算而来,所以当其中任何一个值发生改变,frame
都会变化。相反,改变frame
的值同样会影响到他们当中的值。
当对图层做变换的时候,比如旋转或者缩放,
frame
实际上代表了覆盖在图层旋转之后的整个轴对齐的矩形区域,也就是说
frame
的宽高可能和bounds
的宽高不再一致了。根据上图可以看到这点。
锚点
视图的center
属性和图层的position
属性都指定了anchorPoint
相对于父图层的位置。图层的anchorPoint
通过position
来控制它的frame
的位置,你可以认为anchorPoint
是用来移动图层的把柄。默认来说,anchorPoint
位于图层的中点,所以图层的将会以这个点为中心放置。anchorPoint
属性并没有被UIView
接口暴露出来,这也是视图的position
属性被叫做“center”
的原因。但是图层的anchorPoint
可以被移动,比如你可以把它置于图层frame
的左上角,于是图层的内容将会向右下角的position
方向移动,而不是居中了。
注意到,当改变了
anchorPoint
,position
属性保持固定的值并没有发生改变,但是frame
却移动了。
和contentsRect
与contentsCenter
属性类似,anchorPoint
用单位坐标来描述,也就是图层的相对坐标,图层左上角是{0, 0}
,右下角是{1, 1}
,因此默认坐标是{0.5, 0.5}
。anchorPoint
可以通过指定x
和y
值小于0
或者大于1
,使它放置在图层范围之外。
钟摆例子使用anchorPoint
:
给各时针修改锚点:
self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
</br>
坐标系
和视图一样,图层在图层树当中也是相对于父图层按层级关系放置,一个图层的position
依赖于它父图层的bounds
,如果父图层发生了移动,它的所有子图层也会跟着移动。
这样对于放置图层会更加方便,因为你可以通过移动根图层来将它的子图层作为一个整体来移动,但是有时候你需要知道一个图层的绝对位置,或者是相对于另一个图层的位置,而不是它当前父图层的位置。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
这些方法可以把定义在一个图层坐标系下的点或者矩形转换成另一个图层坐标系下的点或者矩形。
Z坐标轴
和UIView
严格的二维坐标系不同,CALayer
存在于一个三维空间当中。除了我们已经讨论过的position
和anchorPoint
属性之外,CALayer
还有另外两个属性,zPosition
和anchorPointZ
,二者都是在Z轴上描述图层位置的浮点类型。
zPosition
属性在大多数情况下其实并不常用。在第五章,我们将会涉及CATransform3D
,你会知道如何在三维空间移动和旋转图层,除了做变换之外,zPosition
最实用的功能就是改变图层的显示顺序了。
通常,图层是根据它们子图层的sublayers
出现的顺序来类绘制的,这就是所谓的画家的算法--就像一个画家在墙上作画--后被绘制上的图层将会遮盖住之前的图层,但是通过增加图层的zPosition
,就可以把图层向相机方向前置,于是它就在所有其他图层的前面了(或者至少是小于它的zPosition
值的图层的前面)。
为greenView图层设置
zPosition
属性:
self.greenView.layer.zPosition = 1.0f;
Hit Testing
最好使用图层相关视图,而不是创建独立的图层关系。其中一个原因就是要处理额外复杂的触摸事件。
CALayer
并不关心任何响应链事件,所以不能直接处理触摸事件或者手势。但是它有一系列的方法帮你处理事件:-containsPoint:
和-hitTest:
。
-containsPoint:
接受一个在本图层坐标系下的CGPoint
,如果这个点在图层frame
范围内就返回YES
。下面看例:
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *layerView;
@property (strong, nonatomic) CALayer *blueLayer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.blueLayer = [CALayer layer];
self.blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
self.blueLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.layerView.layer addSublayer:self.blueLayer];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
CGPoint point = [[touches anyObject] locationInView:self.view];
point = [self.layerView.layer convertPoint:point fromLayer:self.view.layer];
if ([self.layerView.layer containsPoint:point]) {
point = [self.blueLayer convertPoint:point fromLayer:self.layerView.layer];
if ([self.blueLayer containsPoint:point]) {
NSLog(@"Inside Blue Layer.");
} else {
NSLog(@"Inside White Layer.");
}
}
}
当点击白色视图图层的子图层时,打印
Inside Blue Layer
,点击白色视图图层时,打印Inside White Layer
。
-hitTest:
方法同样接受一个CGPoint
类型参数,它返回图层本身,或者包含这个坐标点的叶子节点图层。这意味着不再需要像使用-containsPoint:
那样,人工地在每个子图层变换或者测试点击的坐标。如果这个点在最外面图层的范围之外,则返回nil
。具体使用-hitTest:
方法被点击图层的代码如下:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
CGPoint point = [[touches anyObject] locationInView:self.view];
CALayer *layer = [self.layerView.layer hitTest:point];
if (layer == self.blueLayer) {
NSLog(@"Inside Blue Layer");
} else if (layer == self.layerView.layer) {
NSLog(@"Inside White Layer");
}
}
注意当调用图层的
-hitTest:
方法时,测算的顺序严格依赖于图层树当中的图层顺序(和UIView
处理事件类似)。之前提到的zPosition
属性可以明显改变屏幕上图层的顺序,但不能改变事件传递的顺序。
这意味着如果改变了图层的z轴顺序,你会发现将不能够检测到最前方的视图点击事件,这是因为被另一个图层遮盖住了,虽然它的zPosition
值较小,但是在图层树中的顺序靠前。
</br>
Chapter 4
</br>
知识点:
-
CALayer
的cornerRadius
属性 -
CALayer
的borderWidth
属性 -
CALayer
的borderColor
属性 -
CALayer
的shadowOpacity
属性 -
CALayer
的shadowColor
属性 -
CALayer
的shadowOffset
属性 -
CALayer
的shadowRadius
属性 -
CALayer
的mask
属性 -
CALayer
的magnificationFilter
属性和minificationFilter
属性 -
CALayer
的shouldRasterize
属性 -
CALayer
的rasterizationScale
属性
</br>
制造矩形圆角,用CALayer
的cornerRadius
属性,CGFloat
类型,默认值为0(直角)。
borderWidth
属性用来设置图层的边界宽度,CGFloat
类型。borderColor
属性就是设置图层的颜色,CGColorRef
类型,默认为黑色。如果图层的子图层超过了边界,或者是寄宿图在透明区域有一个透明蒙板,边框仍然会沿着图层的边界绘制出来。
阴影:shadowOpacity
属性取值在0.0(不可见)到1.0(完全不透明)之间,CGFloat
类型。可以改变三个相关属性改变阴影的表现:shadowColor
,shadowOffset
,shadowRadius
。
shadowOffset
是CGSize
,宽度控制横向位移,高度控制纵向位移,其默认值为{0, -3}
(阴影向上)。
shadowRadius
控制阴影的模糊度,CGFloat
类型。当它值为0(默认值)时,阴影就有一个和视图一样有一个明确地边界值,值越大边界越模糊,越接近自然阴影的效果。
和图层边框不同,图层的阴影继承自内容的外形,而不是根据边界和角半径来确定。为了计算出阴影的形状,Core Animation
会将寄宿图(包括子视图,如果有的话)考虑在内,然后通过这些来完美搭配图层形状从而创建一个阴影。
但是CALayer
的maskToBounds
属性会把阴影也去掉:
如果想沿着内容裁切,你需要用到两个图层:一个只画阴影的空的外图层,和一个用masksToBounds
裁剪内容的内图层。
我们已经知道图层阴影并不总是方的,而是从图层内容的形状继承而来。这看上去不错,但是实时计算阴影也是一个非常消耗资源的,尤其是图层有多个子图层,每个图层还有一个有透明效果的寄宿图的时候。
因此如果事先知道阴影形状,可以使用shadowPath
提高性能,其类型为CGPathRef
。
CGPath
是一个Core Graphics
对象,用来指定任意的一个矢量图形。我们可以通过这个属性单独于图层形状之外指定阴影的形状。
//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;
//create a circular shadow
CGMutablePathRef circlePath = CGPathCreateMutable();
CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds);
self.layerView2.layer.shadowPath = circlePath;
如果是一个矩形或者是圆,用
CGPath
会相当简单明了。但是如果是更加复杂一点的图形,UIBezierPath
类会更合适,它是一个由UIKit
提供的在CGPath
基础上的Objective-C包装类。
图层蒙板
CALayer
的mask
属性,其类型也是CALayer
。有和其他图层一样的绘制和布局属性。它类似于一个子图层,相对于父图层布局,但是它却不是一个普通的子图层。不同于那些绘制在父图层中的子图层,mask图层定义了父图层的部分可见区域。如果mask图层比父图层要小,只有在mask图层里面的内容才是它关心的,除此以外的一切都会被隐藏起来。
下面用一个UIImageView
(UIView
的子类)演示一下:
UIImage *viewImage = [UIImage imageNamed:@"Igloo"];
//set mask
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = self.imageView.bounds;
UIImage *maskImage = [UIImage imageNamed:@"Cone.png"];
maskLayer.contents = (__bridge id)maskImage.CGImage;
self.imageView.image = viewImage;
self.imageView.layer.mask = maskLayer;
CALayer
蒙板图层真正厉害的地方在于蒙板图不局限于静态图。任何有图层构成的都可以作为mask
属性,这意味着你的蒙板可以通过代码甚至是动画实时生成。
</br>
拉伸过滤
</br>
总得来讲,当我们视图显示一个图片的时候,都应该正确地显示这个图片(意即:以正确的比例和正确的1:1像素显示在屏幕上)。原因如下:
- 能够显示最好的画质,像素既没有被压缩也没有被拉伸。
- 能更好的使用内存,因为这就是所有你要存储的东西。
- 最好的性能表现,CPU不需要为此额外的计算。
不过有时候,显示一个非真实大小的图片确实是我们需要的效果。比如说一个头像或是图片的缩略图,再比如说一个可以被拖拽和伸缩的大图。这些情况下,为同一图片的不同大小存储不同的图片显得又不切实际。
当图片需要显示不同的大小的时候,有一种叫做拉伸过滤
的算法就起到作用了。它作用于原图的像素上并根据需要生成新的像素显示在屏幕上。
重绘图片大小没有一个统一的通用算法。这取决于需要拉伸的内容,放大或是缩小的需求等这些因素。CALayer
为此提供了三种拉伸过滤方法:
(1)kCAFilterLinear
(2)kCAFilterNearest
(3)kCAFilterTrilinear
minification
(缩小图片)和magnification
(放大图片)默认的过滤器都是kCAFilterLinear
,这个过滤器采用双线性滤波算法,它在大多数情况下都表现良好。双线性滤波算法通过对多个像素取样最终生成新的值,得到一个平滑的表现不错的拉伸。但是当放大倍数比较大的时候图片就模糊不清了。
kCAFilterTrilinear
和kCAFilterLinear
非常相似,大部分情况下二者都看不出来有什么差别。但是,较双线性滤波算法而言,三线性滤波算法存储了多个大小情况下的图片(也叫多重贴图),并三维取样,同时结合大图和小图的存储进而得到最后的结果。
这个方法的好处在于算法能够从一系列已经接近于最终大小的图片中得到想要的结果,也就是说不要对很多像素同步取样。这不仅提高了性能,也避免了小概率因舍入错误引起的取样失灵的问题。对于大图来说,双线性滤波和三线性滤波表现得更出色。
kCAFilterNearest
是一种比较武断的方法。从名字不难看出,这个算法(也叫最近过滤)就是取样最近的单像素点而不管其他的颜色。这样做非常快,也不会使图片模糊。但是,最明显的效果就是,会使得压缩图片更糟,图片放大之后也显得块状或是马赛克严重。对于没有斜线的小图来说,最近过滤算法要好很多。
总的来说,对于比较小的图或者是差异特别明显,极少斜线的大图,最近过滤算法会保留这种差异明显的特质以呈现更好的结果。但是对于大多数的图尤其是有很多斜线或是曲线轮廓的图片来说,最近过滤算法会导致更差的结果。换句话说,线性过滤保留了形状,最近过滤则保留了像素的差异。
拉伸实验,使用一个1x的图片显示:
</br>
iOS常见的做法是把一个空间的alpha
值设置为0.5
(50%)以使其看上去呈现为不可用状态。对于独立的视图来说还不错,但是当一个控件有子视图的时候就有点奇怪了,例如一个内嵌了UILabel的自定义UIButton:
左边是一个不透明的按钮,右边是50%透明度的相同按钮。我们可以注意到,里面的标签的轮廓跟按钮的背景很不搭调。
这是由透明度的混合叠加造成的,当你显示一个50%透明度的图层时,图层的每个像素都会一般显示自己的颜色,另一半显示图层下面的颜色。这是正常的透明度的表现。但是如果图层包含一个同样显示50%透明的子图层时,你所看到的视图,50%来自子视图,25%来自图层本身的颜色,另外的25%则来自背景色。
可以设置CALayer
的一个叫做shouldRasterize
属性来实现组透明的效果,如果它被设置为YES
,在应用透明度之前,图层及其子图层都会被整合成一个整体的图片,这样就没有透明度混合的问题了。
为了启用shouldRasterize
属性,我们设置了图层的rasterizationScale
属性。默认情况下,所有图层拉伸都是1.0
, 所以如果你使用了shouldRasterize
属性,你就要确保你设置了rasterizationScale
属性去匹配屏幕,以防止出现Retina屏幕像素化的问题。
</br>
Chapter 5
</br>
第三章中UIView
的transform
属性是一个CGAffineTransform
类型,用于在二维空间做旋转,缩放和平移。CGAffineTransform
是一个可以和二维空间向量(例如CGPoint
)做乘法的3X2的矩阵:
式中用
CGPoint
的每一列和CGAffineTransform
矩阵的每一行对应元素相乘再求和,就形成了一个新的CGPoint
类型的结果。要解释一下图中显示的灰色元素,为了能让矩阵做乘法,左边矩阵的列数一定要和右边矩阵的行数个数相同,所以要给矩阵填充一些标志值,使得既可以让矩阵做乘法,又不改变运算结果,并且没必要存储这些添加的值,因为它们的值不会发生变化,但是要用来做运算。
因此,通常会用3×3
(而不是2×3
)的矩阵来做二维变换,你可能会见到3行2列格式的矩阵,这是所谓的以列为主的格式,上图所示的是以行为主的格式,只要能保持一致,用哪种格式都无所谓。
当对图层应用变换矩阵,图层矩形内的每一个点都被相应地做变换,从而形成一个新的四边形的形状。CGAffineTransform
中的“仿射”的意思是无论变换矩阵用什么值,图层中平行的两条线在变换之后任然保持平行,CGAffineTransform
可以做出任意符合上述标注的变换,�下图显示了一些仿射的和非仿射的变换:
UIView
可以通过设置transform
属性做变换,但实际上它只是封装了内部图层的变换。
CALayer
同样也有一个transform
属性,但它的类型是CATransform3D
,而不是CGAffineTransform
,本章后续将会详细解释。CALayer
对应于UIView
的transform
属性叫做affineTransform
。
</br>
旋转
</br>
使用CALayer
的affineTransform
属性把一个图片旋转45°:
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
self.layerView.layer.affineTransform = transform;
注意我们使用的旋转常量是
M_PI_4
,而不是你想象的45
,因为iOS的变换函数使用弧度而不是角度作为单位。弧度用数学常量pi
的倍数表示,一个pi
代表180°
,所以四分之一的pi就是45°
。C的数学函数库(iOS会自动引入)提供了
pi
的一些简便的换算,M_PI_4
于是就是pi
的四分之一
</br>
混合变换
</br>
Core Graphics
提供了一系列的函数可以在一个变换的基础上做更深层次的变换
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)
当操纵一个变换的时候,初始生成一个什么都不做的变换很重要--也就是创建一个CGAffineTransform
类型的空值,矩阵论中称作单位矩阵,Core Graphics
同样也提供了一个方便的常量:
CGAffineTransformIdentity
最后,如果需要混合两个已经存在的变换矩阵,就可以使用如下方法,在两个变换的基础上创建一个新的变换:
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);