最近研究核心动画,做个自我分享
在刚开始做iOS开发的时候碰到图片问题就UIImageVIew,碰到动画就UIViewAnimation,CoreAnimation只是知道,能做各种路径动画组动画等等,这周就抽出时间研究下这块。于是就想进行一周持续的总结,搞点经验给自己日后回头看。
刚开始我就发现CoreAnimation能处理的东西不仅仅包括动画,他其实是一个图层框架,与UIView分离开专门处理界面各种分散图形的展示,而将交互交给UIView而已,很强大,很专一。
展示图片
我用对比来说,之前我们想让图层显示特定图片:
UIImage *image = [UIImage imageNamed:@"111.png"];
UIImageView *imageview = [UIImageView alloc]initWithImage:image];
[self.view addSubView:imageview];
现在用图层来做:
self.view.layer.content = (__bridge id)image.GCImage;
content的类型其实是id,但是这只跟苹果的过去有关,相信我,这里只能用CGImageRef,而且因为类型问题,在ARC下需要桥接;
图片显示方式(可以调整图片填充或者是合适)contentMode
我们首先先到的就是view.contentMode (枚举)
对应图层里的显示方式属性contentsGravity(NSString *)
{
kCAGravityCenter
kCAGravityTop
kCAGravityBottom
kCAGravityLeft
kCAGravityRight
kCAGravityTopLeft
kCAGravityTopRight
kCAGravityBottomLeft
kCAGravityBottomRight
kCAGravityResize
kCAGravityResizeAspect
kCAGravityResizeAspectFill
}
self.view.layer.contentsGravity = kCAGravityResizeAspect;
contentsScale
contentsScale属性定义了寄宿图的像素尺寸和视图大小的比例,默认情况下它是一个值为1.0的浮点数。
这个属性平时我用的很少,感觉用不用没啥区别,但是别忘了,这是因为我们之前设置过contentMode了,显示模式设定之后,基本一个图片在视图中如何显示已经确定了,所以看不到效果,那么,这个属性到底是干嘛用的呢
contentsScale属性其实属于支持高分辨率(又称Hi-DPI或Retina)屏幕机制的一部分。它用来判断在绘制图层的时候应该为寄宿图创建的空间大小,和需要显示的图片的拉伸度(假设并没有设置contentsGravity属性)。UIView有一个类似功能但是非常少用到的contentScaleFactor属性。
如果contentsScale设置为1.0,将会以每个点1个像素绘制图片,如果设置为2.0,则会以每个点2个像素绘制图片,这就是我们熟知的Retina屏幕。整日充耳的retina(并不是retain!)终于在我们实际开发中有那么点意义了。
这并不会对我们在使用kCAGravityResizeAspect时产生任何影响,因为它就是拉伸图片以适应图层而已,根本不会考虑到分辨率问题。但是如果我们把contentsGravity设置为kCAGravityCenter(这个值并不会拉伸图片),那么,你考虑到分辨率的问题就要设置contentsScale了,不然在retina屏上会出现意想不到的效果。
self.layerView.layer.contentsGravity = kCAGravityCenter;
解决办法:self.view.layer.contentsScale = image.scale;
不设置为kCAGravityCenter时这样写 :layer.contentsScale = [UIScreen mainScreen].scale;
maskToBounds
self.view.layer.cornerRadius = 1;
self.view.layer.masksToBounds = yes;
其实我对这两句代码忍很久了,为什么设置一个头像圆角给予合适的值就能变圆,这两句分别有什么用,终于,layer层的研究,来解析一下:
UIView有一个叫做clipsToBounds的属性可以用来决定是否显示超出边界的内容,CALayer对应的属性叫做masksToBounds,这个属性就是告诉你,超过设定边界的图片是否裁掉,而cornerRadius则是询问,我要设置的圆角是多大的(CGFloat *),一般经验告诉我:边长的一半。
contentsRect
CALayer的contentsRect属性允许我们在图层边框里显示寄宿图的一个子域。这涉及到图片是如何显示和拉伸的,所以要比contentsGravity灵活多了
通俗的讲,就是你现在可以选择显示所选图片的一部分或者全部;
和bounds,frame不同,contentsRect不是按点来计算的,它使用了单位坐标,单位坐标指定在0到1之间,是一个相对值(像素和点就是绝对值)。所以他们是相对与寄宿图的尺寸的。iOS使用了以下的坐标系统:
点 —— 在iOS和Mac OS中最常见的坐标体系。点就像是虚拟的像素,也被称作逻辑像素。在标准设备上,一个点就是一个像素,但是在Retina设备上,一个点等于2*2个像素。iOS用点作为屏幕的坐标测算体系就是为了在Retina设备和普通设备上能有一致的视觉效果。
像素 —— 物理像素坐标并不会用来屏幕布局,但是仍然与图片有相对关系。UIImage是一个屏幕分辨率解决方案,所以指定点来度量大小。但是一些底层的图片表示如CGImage就会使用像素,所以你要清楚在Retina设备和普通设备上,他们表现出来了不同的大小。
单位 —— 对于与图片大小或是图层边界相关的显示,单位坐标是一个方便的度量方式, 当大小改变的时候,也不需要再次调整。单位坐标在OpenGL这种纹理坐标系统中用得很多,Core Animation中也用到了单位坐标。
默认的contentsRect是{0, 0, 1, 1},这意味着整个寄宿图默认都是可见的,如果我们指定一个小一点的矩形,图片就会被裁剪
事实上给contentsRect设置一个负数的原点或是大于{1, 1}的尺寸也是可以的。这种情况下,最外面的像素会被拉伸以填充剩下的区域。
contentsRect在app中最有趣的地方在于一个叫做image sprites(图片拼合)的用法。如果你有游戏编程的经验,那么你一定对图片拼合的概念很熟悉,图片能够在屏幕上独立地变更位置。抛开游戏编程不谈,这个技术常用来指代载入拼合的图片,跟移动图片一点关系也没有。
典型地,图片拼合后可以打包整合到一张大图上一次性载入。相比多次载入不同的图片,这样做能够带来很多方面的好处:内存使用,载入时间,渲染性能等等
拿小本本记下:提高项目性能的方法之一:图片拼合;
2D游戏引擎入Cocos2D使用了拼合技术,它使用OpenGL来显示图片。不过我们可以使用拼合在一个普通的UIKit应用中,对!就是使用contentsRect
首先,我们需要一个拼合后的图表 —— 一个包含小一些的拼合图的大图片。
接下来,我们要在app中载入并显示这些拼合图。规则很简单:像平常一样载入我们的大图,然后把它赋值给四个独立的图层的contents,然后设置每个图层的contentsRect来去掉我们不想显示的部分。
我们的工程中需要一些额外的视图。(为了避免太多代码。我们将使用Interface Builder来拜访他们的位置,如果你愿意还是可以用代码的方式来实现的)。
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *coneView;
@property (nonatomic, weak) IBOutlet UIView *shipView;
@property (nonatomic, weak) IBOutlet UIView *iglooView;
@property (nonatomic, weak) IBOutlet UIView *anchorView;
@end
@implementation ViewController
- (void)addSpriteImage:(UIImage *)image withContentRect:(CGRect)rect ?toLayer:(CALayer *)layer //set image
{
layer.contents = (__bridge id)image.CGImage;
//scale contents to fit
layer.contentsGravity = kCAGravityResizeAspect;
//set contentsRect
layer.contentsRect = rect;
}
- (void)viewDidLoad
{
[super viewDidLoad]; //load sprite sheet
UIImage *image = [UIImage imageNamed:@"Sprites.png"];
//set igloo sprite
[self addSpriteImage:image withContentRect:CGRectMake(0, 0, 0.5, 0.5) toLayer:self.iglooView.layer];
//set cone sprite
[self addSpriteImage:image withContentRect:CGRectMake(0.5, 0, 0.5, 0.5) toLayer:self.coneView.layer];
//set anchor sprite
[self addSpriteImage:image withContentRect:CGRectMake(0, 0.5, 0.5, 0.5) toLayer:self.anchorView.layer];
//set spaceship sprite
[self addSpriteImage:image withContentRect:CGRectMake(0.5, 0.5, 0.5, 0.5) toLayer:self.shipView.layer];
}
@end
严谨负责的总结一下,就是将你需要用到的属于并列属性的图片拼到一张大图上,然后在每个需要放图片的地方放上layer,然后用contentsRect来显示需要显示的地方。
优点:拼合不仅给app提供了一个整洁的载入方式,还有效地提高了载入性能(单张大图比多张小图载入地更快);
缺点:如果有手动安排的话,他们还是有一些不方便的,如果你需要在一个已经创建好的品和图上做一些尺寸上的修改或者其他变动,无疑是比较麻烦的。
解决方案:Mac上有一些商业软件可以为你自动拼合图片,这些工具自动生成一个包含拼合后的坐标的XML或者plist文件,拼合图片的使用大大简化。这个文件可以和图片一同载入,并给每个拼合的图层设置contentsRect,这样开发者就不用手动写代码来摆放位置了。
这些文件通常在OpenGL游戏中使用,不过呢,你要是有兴趣在一些常见的app中使用拼合技术,那么一个叫做LayerSprites的开源库(https://github.com/nicklockwood/LayerSprites),它能够读取Cocos2D格式中的拼合图并在普通的Core Animation层中显示出来。
contentsCenter
本章我们介绍的最后一个和内容有关的属性是contentsCenter,看名字你可能会以为它可能跟图片的位置有关,不过这名字着实误导了你。contentsCenter其实是一个CGRect,它定义了一个固定的边框和一个在图层上可拉伸的区域。 改变contentsCenter的值并不会影响到寄宿图的显示,除非这个图层的大小改变了,你才看得到效果。
默认情况下,contentsCenter是{0, 0, 1, 1},这意味着如果大小(由conttensGravity决定)改变了,那么寄宿图将会均匀地拉伸开。但是如果我们增加原点的值并减小尺寸。我们会在图片的周围创造一个边框。
这意味着我们可以随意重设尺寸,边框仍然会是连续的。他工作起来的效果和UIImage里的-resizableImageWithCapInsets: 方法效果非常类似,只是它可以运用到任何寄宿图,甚至包括在Core Graphics运行时绘制的图形。
严谨负责的总结:其实他就是在图层中设置了一片区域,作为展示拉伸的部分,而这个区域(默认是和图层一样大小,刚好遮罩的,但CGRect是可以改变的,现在假设你把它变小并居于图层中央)与图层边缘的距离在图层拉伸的过程中是不会改变的。举个简单的栗子,我在做即时通讯的时候,聊天界面要用到两种不同的cell,但是每种cell都是一个View上加载一个Label,然后sizetofit,不论你输入文字的内容多少,label边缘总会与对话框边缘留出一定间距,这样才不会逼死强迫症。然后contentscenter这个属性就类似于中间那个label。
Customer Drawing
先bb一下,这是本次学习的最后一节,但是以我的理解来说这个自定义绘画好像并没有很重要的实用价值,不过作为学习嘛,先记住,再慢慢理解。
给contents赋CGImage的值不是唯一的设置寄宿图的方法。我们也可以直接用Core Graphics直接绘制寄宿图。能够通过继承UIView并实现-drawRect:方法来自定义绘制。
-drawRect: 方法没有默认的实现,因为对UIView来说,寄宿图并不是必须的,它不在意那到底是单调的颜色还是有一个图片的实例。如果UIView检测到-drawRect: 方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以 contentsScale的值。
如果你不需要寄宿图,那就不要创建这个方法了,这会造成CPU资源和内存的浪费,这也是为什么苹果建议:如果没有自定义绘制的任务就不要在子类中写一个空的-drawRect:方法。
当视图在屏幕上出现的时候 -drawRect:方法就会被自动调用。-drawRect:方法里面的代码利用Core Graphics去绘制一个寄宿图,然后内容就会被缓存起来直到它需要被更新(通常是因为开发者调用了-setNeedsDisplay方法,尽管影响到表现效果的属性值被更改时,一些视图类型会被自动重绘,如bounds属性)。虽然-drawRect:方法是一个UIView方法,事实上都是底层的CALayer安排了重绘工作和保存了因此产生的图片。
CALayer有一个可选的delegate属性,实现了CALayerDelegate协议,当CALayer需要一个内容特定的信息时,就会从协议中请求。CALayerDelegate是一个非正式协议,其实就是说没有CALayerDelegate @protocol可以让你在类里面引用啦。你只需要调用你想调用的方法,CALayer会帮你做剩下的。(delegate属性被声明为id类型,所有的代理方法都是可选的)。
当需要被重绘时,CALayer会请求它的代理给他一个寄宿图来显示。它通过调用下面这个方法做到的:
(void)displayLayer:(CALayerCALayer *)layer;
趁着这个机会,如果代理想直接设置contents属性的话,它就可以这么做,不然没有别的方法可以调用了。如果代理不实现-displayLayer:方法,CALayer就会转而尝试调用下面这个方法:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
在调用这个方法之前,CALayer创建了一个合适尺寸的空寄宿图(尺寸由bounds和contentsScale决定)和一个Core Graphics的绘制上下文环境,为绘制寄宿图做准备,他作为ctx参数传入。
注意一下一些有趣的事情:
我们在blueLayer上显式地调用了-display。不同于UIView,当图层显示在屏幕上时,CALayer不会自动重绘它的内容。它把重绘的决定权交给了开发者。
尽管我们没有用masksToBounds属性,绘制的那个圆仍然沿边界被裁剪了。这是因为当你使用CALayerDelegate绘制寄宿图的时候,并没有对超出边界外的内容提供绘制支持。
现在你理解了CALayerDelegate,并知道怎么使用它。但是除非你创建了一个单独的图层,你几乎没有机会用到CALayerDelegate协议。因为当UIView创建了它的宿主图层时,它就会自动地把图层的delegate设置为它自己,并提供了一个-displayLayer:的实现,那所有的问题就都没了。
当使用寄宿了视图的图层的时候,你也不必实现-displayLayer:和-drawLayer:inContext:方法咦绘制你的寄宿图。通常做法是实现UIView的-drawRect:方法,UIView就会帮你做完剩下的工作,包括在需要重绘的时候调用-display方法。
今日总结
今天学习了寄宿图和一些相关的属性。学到了如何显示和放置图片, 使用拼合技术来显示, 以及用CALayerDelegate和Core Graphics来绘制图层内容。
明天学"图层几何学",探讨一下图层的几何,观察他们是如何放置和改变相互的尺寸的。
学习原文《iOS-Core-Animation-Advanced-Techniques》;