神奇的CALayer
CALayer
类在概念上和UIView
类似,同样也是一些被层级关系树管理的矩形块,同样也可以包含一些内容(像图片,文本或者背景色),管理子图层的位置。它们有一些方法和属性用来做动画和变换。和UIView
最大的不同是CALayer
不处理用户的交互。
Core Animation
基于一个假设,说屏幕上的任何东西都可以(或者可能)做动画。动画并不需要你在Core Animation中手动打开,相反需要明确地关闭,否则他会一直存在。所以,CALayer
与UIView
还有一个更大的区别在于,当你改变CALayer
的一个可做动画的属性,它并不能立刻在屏幕上体现出来。相反,它是从先前的值平滑过渡到新的值。这一切都是默认的行为,你不需要做额外的操作,这就是隐式动画。
目录:
contents属性
contents
contentsScale
contentsRect
contentsCenter
视觉效果
蒙板
圆角
maskedCorners属性
UIRectCorner与贝塞尔曲线
高性能渲染圆型图标
阴影
shadowPath属性
拉伸过滤
contents属性
contents
contents
属性是一个id
属性,因为在MacOS上UIImage
和CGImage
同样对对其生效
但是在iOS上,只有CGImage
生效,赋值其他对象,只会得到一个空白的图层
layer.contents = (__bridge id)image.CGImage;
然后你就会的到下图所示:
可见这个雪人是有点胖了,你此时脑海中会立马浮现出一个似曾相识的属性
view.contentMode = UIViewContentModeScaleAspectFit;
是的没错,很接近了。不过在CALayer上是这样
layer.contentsGravity = kCAGravityResizeAspect;
好的雪人的身材恢复了:
layer.contentsGravity
与view.contentMode
对应,但是是一个NSString类型,可选的常量有:
kCAGravityCenter
kCAGravityTop
kCAGravityBottom
kCAGravityLeft
kCAGravityRight
kCAGravityTopLeft
kCAGravityTopRight
kCAGravityBottomLeft
kCAGravityBottomRight
kCAGravityResize
kCAGravityResizeAspect
kCAGravityResizeAspectFill
contentsScale
contentsScale
定义了寄宿图的像素尺寸和视图大小比例,默认为1.0
的浮点数
contentsScale
是用于支持高分屏机制的一部分[当然现在几乎所有设备都是高分屏了]
如果contentsScale
设置为1.0,将会以每个点1个像素绘制图片,如果设置为2.0,则会以每个点2个像素绘制图片
这个属性可以用来做CALayer
的拉伸,但是通常不这么做[而是使用transform
和affineTransform
],如果如果我们把contentsGravity
设置为kCAGravityCenter
,这个属性不会拉伸图片:
如图所示,雪人很大,像素点也很粗。这是因为使用UIImage
类去读取的图片是Retina屏的图片,而转换CGImage
赋值的时候,这个因素就丢失了。
所以我们可能需要手动修改这个问题:
UIImage *image = [UIImage imageNamed:@"Snowman.png"]; //add it directly to our view's layer
self.layerView.layer.contents = (__bridge id)image.CGImage; //center the image
self.layerView.layer.contentsGravity = kCAGravityCenter;
self.layerView.layer.contentsScale = image.scale;//此处也可以选择[UIScreen mainScreen].scale
好了我们的雪人变的正常了
划重点:
请记住contentsScale,否则在你发现你的CALayer相关的视图像素点变得很粗的时候你不一定想得到回来翻这一截节
contentsRect
CALayer的contentsRect
属性允许我们在图层边框里显示寄宿图的一个子域。
contentsRect
使用了单位坐标,单位坐标是指定在0-1之间的浮点数,表示相对于寄宿图的尺寸比例
周老师科普课堂
iOS有以下三种坐标系:
- 点
- 最常见的坐标系,点代表了逻辑像素(一个点在retina屏幕上是2*2个像素),点坐标使高分屏和低分屏有统一视觉效果
- 像素
- 物理像素坐标通常不会用在屏幕布局,UIImage是一个屏幕分辨率解决方案,所以指定点来度量大小。但是一些底层的图片表示如CGImage就会使用像素。
- 单位
- 以比例作为基本单位,对于图片大小或是图层边界相关的显示非常方便的度量方式,当大小改变时不需要再次调整。单位坐标多见于
OpenGL
这种纹理坐标系统,Core Animation
中也用到了单位坐标
contentsRect
默认为{0, 0, 1, 1},表示以左上角{0,0}为基准点,显示宽高为寄宿图宽高的1倍。如果我们指定为小一点的矩形,图片会被裁剪:
通过contentsRect实现图片的拼接
contentsRect
在APP中最有趣的地方在于一个叫做image sprites(图片拼合)的用法
图片拼合后可以打包整合到一张大图上一次性载入。相比多次载入不同的图片,这样做有时能够带来很多性能上的优化(单张大图比多张小图载入地更快)
好的我们这次放过可爱的雪人,把我们敬爱的杨总抬上来!
经过一番操作我们要实现这张杨总的图片切割
- (void)SpriteDemo {
UIImage *hq = [UIImage imageNamed:@"hq.jpg"];
[self addSpriteImage:hq contentRect:CGRectMake(0, 0, 0.5, 0.5) toLayer:self.view1.layer];
[self addSpriteImage:hq contentRect:CGRectMake(0.5, 0, 0.5, 0.5) toLayer:self.view2.layer];
[self addSpriteImage:hq contentRect:CGRectMake(0, 0.5, 0.5, 0.5) toLayer:self.view3.layer];
[self addSpriteImage:hq contentRect:CGRectMake(0.5, 0.5, 0.5, 0.5) toLayer:self.view4.layer];
}
- (void)addSpriteImage:(UIImage *)image withContentRect:(CGRect)rect toLayer:(CALayer *)layer {
layer.contents = (__bridge id)image.CGImage;
layer.contentsGravity = kCAGravityResizeAspect;
layer.contentsRect = rect;
}
之后我们完成了对这张图片的切割工作
这大图切小图的神技今天就讲到这里,欲知多张小图如何拼接成大图?请看下期专业图层之CATiledLayer
contentsCenter
这个属性,看名字好像和位置有关系,然并不,这货是一个CGRect。
contentsCenter
定义了一个固定边框和一个在图层上可拉伸的区域。
contentsCenter
默认为{0,0,1,1},表示Layer大小改变,寄宿图均匀伸展。如果我们增加原点并减小尺寸,则会在图片的创造一个边框。
非常常见的一个应用场景就是IM的消息气泡。
气泡资源是这样的:
设置contentsCenter
ww{0.5,0.5,0,0},一套操作之后变成了这样:
[我觉得你们应该都知道但是我还是觉得这个好神奇啊非要拉出来讲一讲不可]
视觉效果
蒙板
众所周知UIView
有一个clipsToBounds
属性,设置后会隐藏超出自身范围的subView。
对应的,CALayer有一个相同作用的属性masksToBounds
,效果与clipsToBounds
相同,可以由父视图约束子视图的显示范围。
这个时候就有人会问了,周老师我想让子图层作为蒙板控制父视图的显示范围要怎么操作呢:
mask属性
mask
:蒙板属性,[用过PhotoShop的同学们此处高呼:似里!]。
属性本身是一个CALayer
类型对象,类似于一个子视图,但是不做绘制工作,而是定义了父图层的可见区域。
mask
图层的颜色是无关紧要的,真正重要的是图层的轮廓。带有mask
的图层会保留mask
有颜色的部分,没有透明的部分会被抛弃:
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = self.imageView.bounds;
UIImage *maskImage = [UIImage imageNamed:@"Cone.png"];
maskLayer.contents = (__bridge id)maskImage.CGImage;
self.imageView.layer.mask = maskLayer;
CALayer蒙板图层真正厉害的地方在于蒙板图不局限于静态图。任何有图层构成的都可以作为mask
属性,这意味着你的蒙板可以通过代码甚至是动画实时生成。
圆角
啊圆角还要讲?不就是conerRadius
吗?周老师你怕不是在这里骗流量噢?
但是你知不知道回字有四。。。。
今天就来讲讲圆角的字的几种特殊写法
maskedCorners属性
小明同学今天开开心心吃着火锅唱着歌,突然产品打电话过来:”歪?小明?是我!那天那个图标,能不能做的飞扬一点”
肉眼可见,这个图标只有两个圆角。
小明哭着对我说:我真傻真的,我单知道一个图标可以做四个圆角,没想到策划居然会要我做两个
周通禅师指着屏幕:你看这是什么?
maskedCorners
属性,可控制圆角效果出现在矩形的指定角上
typedef NS_OPTIONS (NSUInteger, CACornerMask)
{
kCALayerMinXMinYCorner = 1U << 0,
kCALayerMaxXMinYCorner = 1U << 1,
kCALayerMinXMaxYCorner = 1U << 2,
kCALayerMaxXMaxYCorner = 1U << 3,
};
周通禅师一番变化
view.layer.cornerRadius = 50;
view.layer.maskedCorners = kCALayerMaxXMinYCorner | kCALayerMinXMaxYCorner;
登时立刻出现了小明同学想要的效果,小明欢欢喜喜的回家去了
UIRectCorner与贝塞尔曲线
第二天小明又来了:
“周通禅师,你这个属性只适配iOS11以上,用不了咋整啊”
周通禅师微微一笑:没事,你忘了我们刚刚学过的蒙板大法了吗?
//CAShapeLayer是一个通过矢量图形绘制的图层子类,此后专用图层会详细解说
CAShapeLayer *layer = [CAShapeLayer layer];
CGRect rect = view.bounds;
//设置半径为50 ———————— 个人实验发现这个radii属性仅.x生效表示圆角半径,.y无效,若有偏差还望指正
CGSize radii = CGSizeMake(50, 50);
//圆角对右上角和左上角生效
UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomLeft;
//创建圆角矩形曲线
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:radii];
//以曲线绘制CAShapeLayer
layer.path = path.CGPath;
//将CAShapeLayer设置为蒙板
self.view2.layer.mask = layer;
代码量变得很多,但是适配问题解决了,小明又欢欢喜喜的回家去了
高性能渲染圆型图标
众所周知,cornerRadius
操作和mask
操作都是会涉及比较消耗性能的离屏渲染的操作(大家都这么说),偶尔来一发还好,如果有数量较大的离屏渲染操作,可能会造成程序的卡顿。例如我们的好兄弟最近聊天列表,带有大量圆角图标(头像,用户信息配置化图标),如果全都采用离屏渲染的操作,势必带来体验上的落差。
此处解决方案是:通过赛贝尔曲线绘制一个圆角矩形(或者直接是一个圆),通过UIImage
的drawInRect:
方法将图标的内容直接绘制到指定区域中形成一个自带圆角的UIImage
对象
//此处直接参考MUIAvatar中相应代码
@implementation UIImage (CycleImage)
- (UIImage*)imageWithCornerRadius:(CGFloat)radius{
CGFloat w = self.size.width;
CGFloat h = self.size.height;
CGFloat scale = [UIScreen mainScreen].scale;
UIImage *image = nil;
CGRect imageFrame = CGRectMake(0, 0, w, h);
UIGraphicsBeginImageContextWithOptions(self.size, NO, scale);
//生成对应贝塞尔曲线
[[UIBezierPath bezierPathWithRoundedRect:imageFrame cornerRadius:radius] addClip];
//将Image内容写入上下文
[self drawInRect:imageFrame];
//从上下文中取出结果
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
阴影
CALayer
的阴影可以说是相当的简单,只要将shadowOpacity
设置为一个大于0的值,阴影就会出现在图层之下。
shadowOpacity
:阴影透明度,必须设置在0(不可见)和1.0(完全不透明)之间的浮点数
若要改变阴影的表现,你可以使用另外三个属性
shadowOffset
:设置一个阴影的偏移量
shadowColor
:设置阴影的颜色
shadowRadius
:设置阴影的圆角,(但是同时会增加阴影的模糊程度)
//上图对应代码为
layer.shadowOpacity = 0.5;
layer.shadowOffset = CGSizeMake(100, 100);
layer.shadowColor = [UIColor blueColor].CGColor;
layer.shadowRadius = 50;
阴影与图层边框不同,图层的阴影继承自内容的外形而不是边界和角半径
shadowPath属性
shadowPath
是独立于图层外用来指定阴影形状的属性,是一个CGPathRef
类型(一个指向CGPath
的指针)
当寄宿图内容过于复杂、图层有多个字图层、寄宿图甚至带有透明通道时,性能的消耗是非常巨大的。所以你事先知道阴影的形状或者希望指定阴影的形状,可以使用shadowPath
来提高性能。
//创建一个矩形阴影
CGMutablePathRef squarePath = CGPathCreateMutable();
CGPathAddRect(squarePath, NULL, self.layerView1.bounds);
view1.layer.shadowPath = squarePath;
CGPathRelease(squarePath);
//创建一个圆形阴影
CGMutablePathRef circlePath = CGPathCreateMutable();
CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds);
view2.layer.shadowPath = circlePath;
CGPathRelease(circlePath);
//其实不一定要用CGPath这么骚的东西,用贝塞尔曲线的path.CGPath也是可以的
拉伸过滤
通常情况下,视图显示一个图片的时候,我们希望它能够以1:1的比例显示图片。如此能够显示最好的画质,并且最大化利用性能。
但是总有天不如人愿的时候,比如你要显示一个头像或者缩略图。这种情况下再使用原图恐怕是要被人当成是[和谐词]了。
当图片需要显示不同的大小的时候,有一种叫做拉伸过滤的算法就起到作用了。它作用于原图的像素上并根据需要生成新的像素显示在屏幕上。
minificationFilter
和magnificationFilter
属性:缩小图片和放大图片过滤器
CALayer为过滤器提供了三种方式:
kCAFilterLinear(默认过滤器)
kCAFilterNearest
kCAFilterTrilinear
kCAFilterLinear
:过滤器采用双线性滤波算法,它在大多数情况下都表现良好。双线性滤波算法通过对多个像素取样最终生成新的值,得到一个平滑的表现不错的拉伸。但是当放大倍数比较大的时候图片就模糊不清了。
kCAFilterTrilinear
:与kCAFilterLinear
非常相似,大部分情况下二者都看不出来有什么差别。但是,较双线性滤波算法而言,三线性滤波算法存储了多个大小情况下的图片(也叫多重贴图),并三维取样,同时结合大图和小图的存储进而得到最后的结果。
这个方法的好处在于算法能够从一系列已经接近于最终大小的图片中得到想要的结果,也就是说不要对很多像素同步取样。这不仅提高了性能,也避免了小概率因舍入错误引起的取样失灵的问题。
对于大图来说,双线性滤波和三线性滤波表现得更出色
kCAFilterNearest
:一种比较武断的方法。这个算法(也叫最近过滤)就是取样最近的单像素点而不管其他的颜色。这样做非常快,也不会使图片模糊。但是,最明显的效果就是,会使得压缩图片更糟,图片放大之后也显得块状或是马赛克严重。
对于没有斜线的小图来说,最近过滤算法要好很多
总而言之,线性过滤保留了形状,最近过滤则保留了像素的差异