不熟悉几何学的人就不要来这里了 --柏拉图学院入口的签名
在第二章里面,咱们介绍了图层背后的图片,和一些控制图层坐标和旋转的属性。在这一章中,咱们将要看一看图层内部是如何根据父图层和兄弟图层来控制位置和尺寸的。另外咱们也会涉及如何管理图层的几何结构,以及它是如何被自动调整和自动布局影响的。面试
UIView
有三个比较重要的布局属性:frame
,bounds
和center
,CALayer
对应地叫作frame
,bounds
和position
。为了能清楚区分,图层用了“position”,视图用了“center”,可是他们都表明一样的值。算法
frame
表明了图层的外部坐标(也就是在父图层上占据的空间),bounds
是内部坐标({0, 0}一般是图层的左上角),center
和position
都表明了相对于父图层anchorPoint
所在的位置。anchorPoint
的属性将会在后续介绍到,如今把它想成图层的中心点就行了。图3.1显示了这些属性是如何相互依赖的。框架
图3.2 旋转一个视图或者图层以后的frame
属性ide
以前提到过,视图的center
属性和图层的position
属性都指定了anchorPoint
相对于父图层的位置。图层的anchorPoint
经过position
来控制它的frame
的位置,你能够认为anchorPoint
是用来移动图层的把柄。函数
默认来讲,anchorPoint
位于图层的中点,因此图层的将会以这个点为中心放置。anchorPoint
属性并无被UIView
接口暴露出来,这也是视图的position
属性被叫作“center”
的缘由。可是图层的anchorPoint
能够被移动,好比你能够把它置于图层frame的左上角,因而图层的内容将会向右下角的position
方向移动(图3.3),而不是居中了。工具
图3.4 组成钟面和钟表的四张图片布局
闹钟的组件经过IB来排列(图3.5),这些图片视图嵌套在一个容器视图以内,而且自动调整和自动布局都被禁用了。这是由于自动调整会影响到视图的frame
,而根据图3.2的演示,当视图旋转的时候,frame
是会发生改变的,这将会致使一些布局上的失灵。性能
一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个个人iOS交流群:1012951431, 分享BAT,阿里面试题、面试经验,讨论技术, 你们一块儿交流学习成长!但愿帮助开发者少走弯路。学习
咱们用NSTimer
来更新闹钟,使用视图的transform
属性来旋转钟表(若是你对这个属性不太熟悉,不要着急,咱们将会在第5章“变换”当中详细说明),具体代码见清单3.1字体
图3.6 钟面,和不对齐的钟指针
你也许会认为能够在Interface Builder当中调整指针图片的位置来解决,但其实并不能达到目的,由于若是不放在钟面中间的话,一样不能正确的旋转。
也许在图片末尾添加一个透明空间也是个解决方案,但这样会让图片变大,也会消耗更多的内存,这样并不优雅。
更好的方案是使用anchorPoint
属性,咱们来在-viewDidLoad
方法中添加几行代码来给每一个钟指针的anchorPoint
作一些平移(清单3.2),图3.7显示了正确的结果。
清单3.2
- (void)viewDidLoad { [super viewDidLoad]; // adjust anchor points self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f); self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f); self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f); // start timer }
和视图同样,图层在图层树当中也是相对于父图层按层级关系放置,一个图层的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;
这些方法能够把定义在一个图层坐标系下的点或者矩形转换成另外一个图层坐标系下的点或者矩形.
常规说来,在iOS上,一个图层的position
位于父图层的左上角,可是在Mac OS上,一般是位于左下角。Core Animation能够经过geometryFlipped
属性来适配这两种状况,它决定了一个图层的坐标是否相对于父图层垂直翻转,是一个BOOL
类型。在iOS上经过设置它为YES
意味着它的子图层将会被垂直翻转,也就是将会沿着底部排版而不是一般的顶部(它的全部子图层也同理,除非把它们的geometryFlipped
属性也设为YES
)。
和UIView
严格的二维坐标系不一样,CALayer
存在于一个三维空间当中。除了咱们已经讨论过的position
和anchorPoint
属性以外,CALayer
还有另外两个属性,zPosition
和anchorPointZ
,两者都是在Z轴上描述图层位置的浮点类型。
注意这里并无更深的属性来描述由宽和高作成的bounds了,图层是一个彻底扁平的对象,你能够把它们想象成相似于一页二维的坚硬的纸片,用胶水粘成一个空洞,就像三维结构的折纸同样。
zPosition
属性在大多数状况下其实并不经常使用。在第五章,咱们将会涉及CATransform3D
,你会知道如何在三维空间移动和旋转图层,除了作变换以外,zPosition
最实用的功能就是改变图层的显示顺序了。
一般,图层是根据它们子图层的sublayers
出现的顺序来类绘制的,这就是所谓的画家的算法--就像一个画家在墙上做画--后被绘制上的图层将会遮盖住以前的图层,可是经过增长图层的zPosition
,就能够把图层向相机方向前置,因而它就在全部其余图层的前面了(或者至少是小于它的zPosition
值的图层的前面)。
这里所谓的“相机”其实是相对于用户是视角,这里和iPhone背后的内置相机没任何关系。
图3.8显示了在Interface Builder内的一对视图,正如你所见,首先出如今视图层级绿色的视图被绘制在红色视图的后面。
图3.9 绿色视图被绘制在红色视图的前面
第一章“图层树”证明了最好使用图层相关视图,而不是建立独立的图层关系。其中一个缘由就是要处理额外复杂的触摸事件。
CALayer
并不关心任何响应链事件,因此不能直接处理触摸事件或者手势。可是它有一系列的方法帮你处理事件:-containsPoint:
和-hitTest:
。
-containsPoint:
接受一个在本图层坐标系下的CGPoint
,若是这个点在图层frame范围内就返回YES
。如清单3.4所示第一章的项目的另外一个合适的版本,也就是使用-containsPoint:
方法来判断究竟是白色仍是蓝色的图层被触摸了 (图3.10)。这须要把触摸坐标转换成每一个图层坐标系下的坐标,结果很不方便。
清单3.4 使用containsPoint判断被点击的图层
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *layerView; @property (nonatomic, weak) CALayer *blueLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //create sublayer self.blueLayer = [CALayer layer]; self.blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); self.blueLayer.backgroundColor = [UIColor blueColor].CGColor; //add it to our view [self.layerView.layer addSublayer:self.blueLayer]; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { //get touch position relative to main view CGPoint point = [[touches anyObject] locationInView:self.view]; //convert point to the white layer's coordinates point = [self.layerView.layer convertPoint:point fromLayer:self.view.layer]; //get layer using containsPoint: if ([self.layerView.layer containsPoint:point]) { //convert point to blueLayer’s coordinates point = [self.blueLayer convertPoint:point fromLayer:self.layerView.layer]; if ([self.blueLayer containsPoint:point]) { [[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show]; } else { [[[UIAlertView alloc] initWithTitle:@"Inside White Layer" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show]; } } } @end
你可能用过UIViewAutoresizingMask
类型的一些常量,应用于当父视图改变尺寸的时候,相应UIView的frame
也跟着更新的场景(一般用于横竖屏切换)。
在iOS6中,苹果介绍了自动排版机制,它和自动调整不一样,而且更加复杂。
在Mac OS平台,CALayer
有一个叫作layoutManager
的属性能够经过CALayoutManager
协议和CAConstraintLayoutManager
类来实现自动排版的机制。但因为某些缘由,这在iOS上并不适用。
当使用视图的时候,能够充分利用UIView
类接口暴露出来的UIViewAutoresizingMask
和NSLayoutConstraintAPI
,但若是想随意控制CALayer
的布局,就须要手工操做。最简单的方法就是使用CALayerDelegate
以下函数:
- (void)layoutSublayersOfLayer:(CALayer *)layer;
当图层的bounds
发生改变,或者图层的-setNeedsLayout
方法被调用的时候,这个函数将会被执行。这使得你能够手动地从新摆放或者从新调整子图层的大小,可是不能像UIView
的autoresizingMask
和constraints
属性作到自适应屏幕旋转。
这也是为何最好使用视图而不是单独的图层来构建应用程序的另外一个重要缘由之一。
本章涉及了CALayer
的集合结构,包括它的frame
,position
和bounds
,介绍了三维空间内图层的概念,以及如何在独立的图层内响应事件,最后简单说明了在iOS平台,Core Animation对自动调整和自动布局支持的缺少。
在第四章“视觉效果”当中,咱们接着介绍一些图层外表的特性。
嗯,圆和椭圆还不错,但若是是带圆角的矩形呢?
咱们如今能作到那样了么?
史蒂芬·乔布斯
咱们在第三章『图层几何学』中讨论了图层的frame,第二章『寄宿图』则讨论了图层的寄宿图。可是图层不只仅能够是图片或是颜色的容器;还有一系列内建的特性使得创造美丽优雅的使人深入的界面元素成为可能。在这一章,咱们将会探索一些可以经过使用CALayer属性实现的视觉效果。
圆角矩形是iOS的一个标志性审美特性。这在iOS的每个地方都获得了体现,不管是主屏幕图标,仍是警告弹框,甚至是文本框。按照这流行程度,你可能会认为必定有不借助Photoshop就能轻易建立圆角举行的方法。恭喜你,猜对了。
CALayer有一个叫作conrnerRadius
的属性控制着图层角的曲率。它是一个浮点数,默认为0(为0的时候就是直角),可是你能够把它设置成任意值。默认状况下,这个曲率值只影响背景颜色而不影响背景图片或是子图层。不过,若是把masksToBounds
设置成YES的话,图层里面的全部东西都会被截取。
咱们能够经过一个简单的项目来演示这个效果。在Interface Builder中,咱们放置一些视图,他们有一些子视图。并且这些子视图有一些超出了边界(如图4.1)。你可能没法看到他们超出了边界,由于在编辑界面的时候,超出的部分老是被Interface Builder裁切掉了。不过,你相信我就行了 :)
&nbp; CALayer另外两个很是有用属性就是borderWidth
和borderColor
。两者共同定义了图层边的绘制样式。这条线(也被称做stroke)沿着图层的bounds绘制,同时也包含图层的角。
&nbp; borderWidth
是以点为单位的定义边框粗细的浮点数,默认为0.borderColor
定义了边框的颜色,默认为黑色。
&nbp; borderColor
是CGColorRef类型,而不是UIColor,因此它不是Cocoa的内置对象。不过呢,你确定也清楚图层引用了borderColor
,虽然属性声明并不能证实这一点。CGColorRef
在引用/释放时候的行为表现得与NSObject
极其类似。可是Objective-C语法并不支持这一作法,因此CGColorRef
属性即使是强引用也只能经过assign关键字来声明。
&nbp; 边框是绘制在图层边界里面的,并且在全部子内容以前,也在子图层以前。若是咱们在以前的示例中(清单4.2)加入图层的边框,你就能看到究竟是怎么一回事了(如图4.3).
清单4.2 加上边框
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //set the corner radius on our layers self.layerView1.layer.cornerRadius = 20.0f; self.layerView2.layer.cornerRadius = 20.0f; //add a border to our layers self.layerView1.layer.borderWidth = 5.0f; self.layerView2.layer.borderWidth = 5.0f; //enable clipping on the second layer self.layerView2.layer.masksToBounds = YES; } @end
[图片上传失败...(image-f6b464-1574399886581)]
图4.4 边框是跟随图层的边界变化的,而不是图层里面的内容
iOS的另外一个常见特性呢,就是阴影。阴影每每能够达到图层深度暗示的效果。也可以用来强调正在显示的图层和优先级(好比说一个在其余视图以前的弹出框),不过有时候他们只是单纯的装饰目的。
给shadowOpacity
属性一个大于默认值(也就是0)的值,阴影就能够显示在任意图层之下。shadowOpacity
是一个必须在0.0(不可见)和1.0(彻底不透明)之间的浮点数。若是设置为1.0,将会显示一个有轻微模糊的黑色阴影稍微在图层之上。若要改动阴影的表现,你可使用CALayer的另外三个属性:shadowColor
,shadowOffset
和shadowRadius
。
显而易见,shadowColor
属性控制着阴影的颜色,和borderColor
和backgroundColor
同样,它的类型也是CGColorRef
。阴影默认是黑色,大多数时候你须要的阴影也是黑色的(其余颜色的阴影看起来是否是有一点点奇怪。。)。
shadowOffset
属性控制着阴影的方向和距离。它是一个CGSize
的值,宽度控制这阴影横向的位移,高度控制着纵向的位移。shadowOffset
的默认值是 {0, -3},意即阴影相对于Y轴有3个点的向上位移。
为何要默认向上的阴影呢?尽管Core Animation是从图层套装演变而来(能够认为是为iOS建立的私有动画框架),可是呢,它倒是在Mac OS上面世的,前面有提到,两者的Y轴是颠倒的。这就致使了默认的3个点位移的阴影是向上的。在Mac上,shadowOffset
的默认值是阴影向下的,这样你就能理解为何iOS上的阴影方向是向上的了(如图4.5).
经过masksToBounds
属性,咱们能够沿边界裁剪图形;经过cornerRadius
属性,咱们还能够设定一个圆角。可是有时候你但愿展示的内容不是在一个矩形或圆角矩形。好比,你想展现一个有星形框架的图片,又或者想让一些古卷文字慢慢渐变成背景色,而不是一个突兀的边界。
使用一个32位有alpha通道的png图片一般是建立一个无矩形视图最方便的方法,你能够给它指定一个透明蒙板来实现。可是这个方法不能让你以编码的方式动态地生成蒙板,也不能让子图层或子视图裁剪成一样的形状。
CALayer有一个属性叫作mask
能够解决这个问题。这个属性自己就是个CALayer类型,有和其余图层同样的绘制和布局属性。它相似于一个子图层,相对于父图层(即拥有该属性的图层)布局,可是它却不是一个普通的子图层。不一样于那些绘制在父图层中的子图层,mask
图层定义了父图层的部分可见区域。
mask
图层的Color
属性是可有可无的,真正重要的是图层的轮廓。mask属性就像是一个饼干切割机,mask
图层实心的部分会被保留下来,其余的则会被抛弃。(如图4.12)
若是mask
图层比父图层要小,只有在mask
图层里面的内容才是它关心的,除此之外的一切都会被隐藏起来。
最后咱们再来谈谈minificationFilter
和magnificationFilter
属性。总得来说当咱们视图显示一个图片的时候都应该正确地显示这个图片意即以正确的比例和正确的11像素显示在屏幕上。缘由以下
不过有时候显示一个非真实大小的图片确实是咱们须要的效果。好比说一个头像或是图片的缩略图再好比说一个能够被拖拽和伸缩的大图。这些状况下为同一图片的不一样大小存储不一样的图片显得又不切实际。
当图片须要显示不一样的大小的时候有一种叫作拉伸过滤的算法就起到做用了。它做用于原图的像素上并根据须要生成新的像素显示在屏幕上。
事实上重绘图片大小也没有一个统一的通用算法。这取决于须要拉伸的内容放大或是缩小的需求等这些因素。CALayer为此提供了三种拉伸过滤方法他们是
minification缩小图片和magnification放大图片默认的过滤器都是kCAFilterLinear
这个过滤器采用双线性滤波算法它在大多数状况下都表现良好。双线性滤波算法经过对多个像素取样最终生成新的值获得一个平滑的表现不错的拉伸。可是当放大倍数比较大的时候图片就模糊不清了。
kCAFilterTrilinear
和kCAFilterLinear
很是类似大部分状况下两者都看不出来有什么差异。可是较双线性滤波算法而言三线性滤波算法存储了多个大小状况下的图片也叫多重贴图并三维取样同时结合大图和小图的存储进而获得最后的结果。
这个方法的好处在于算法可以从一系列已经接近于最终大小的图片中获得想要的结果也就是说不要对不少像素同步取样。这不只提升了性能也避免了小几率因舍入错误引发的取样失灵的问题
图4.15 对于没有斜线的小图来讲最近过滤算法要好不少
总的来讲对于比较小的图或者是差别特别明显极少斜线的大图最近过滤算法会保留这种差别明显的特质以呈现更好的结果。可是对于大多数的图尤为是有不少斜线或是曲线轮廓的图片来讲最近过滤算法会致使更差的结果。换句话说线性过滤保留了形状最近过滤则保留了像素的差别。
让咱们来实验一下。咱们对第三章的时钟项目改动一下用LCD风格的数字方式显示。咱们用简单的像素字体一种用像素构成字符的字体而非矢量图形创造数字显示方式用图片存储起来并且用第二章介绍过的拼合技术来显示如图4.16。
UIView有一个叫作alpha
的属性来肯定视图的透明度。CALayer有一个等同的属性叫作opacity
,这两个属性都是影响子层级的。也就是说,若是你给一个图层设置了opacity
属性,那它的子图层都会受此影响。
iOS常见的作法是把一个控件的alpha值设置为0.5(50%)以使其看上去呈现为不可用状态。对于独立的视图来讲还不错,可是当一个控件有子视图的时候就有点奇怪了,图4.20展现了一个内嵌了UILabel的自定义UIButton;左边是一个不透明的按钮,右边是50%透明度的相同按钮。咱们能够注意到,里面的标签的轮廓跟按钮的背景很不搭调。
这一章介绍了一些能够经过代码应用到图层上的视觉效果,好比圆角,阴影和蒙板。咱们也了解了拉伸过滤器和组透明。
另外,若是你想一块儿进阶,不妨添加一下交流群1012951431,选择加入一块儿交流,一块儿学习。期待你的加入!
在第五章,『变换』中,咱们将会研究图层变化和3D转换