接上篇 View编程指南(一)
三、View的坐标系统
1. 坐标系统基础
UIKit默认的坐标系统是从左上角开始的。坐标系统里的点使用float表示,这样可以更加精确的布局和定位。图1-4显示了屏幕的坐标系统。除了屏幕的坐标系统之外,window和view有自己的本地坐标系统,这样可以允许你相对于view和window来进行定位。
因为view和window都有他们自己的本地坐标系统,因此你需要时刻注意哪个坐标系统正在生效。当你进行绘制的时候,你的坐标是相对于view自己的坐标系统。当你进行几何改变时,你的坐标是相对于superview的坐标系统。UIWindow和UIView都有帮助你进行坐标系统变换的方法,所以不要过于担心。
其他的一些黑科技例如Core Graphics和OpenGL ES使用的是左下角为原点的坐标系,因此当你的代码涉及到这两套坐标系时,记得进行相应的变换!
2. Frame、Bounds和Center属性之间的关系
- frame属性,它指定了当前view在其superview的坐标系统中的位置和大小
- bounds属性,它指定了当前view在其自身的本地坐标系统中的大小以及view的内容的原点在哪里
- center属性,它指出了当前view在其superview的坐标系统中的中心点位置
一般而言,你使用center和frame属性来进行view的几何操作。例如:如果你要改变view的位置,一般使用center属性,因为center属性几乎在任何情况下都是可用的,即使view在拉伸、旋转。
在绘制view时,一般会用到bounds属性。默认bounds是以view的本地坐标系的0,0作为原点,大小和frame的大小相同。
图1-5展示了这三者之间的关系
尽管你可以分别改变frame、bounds和center属性,但事实上改变其中之一会影响到其他的属性:
- 当你设置frame属性时,bounds属性中的size值也会随着frame属性的size值改变。center属性的值也会跟着改变以表示新的中心点
- 当你设置center属性时,你的frame的origin值会跟着改变
- 当你设置bounds属性的size值时,frame属性的size值也会相应的改变
默认情况下,view的frame没有clip到superview的frame,这样如果subview有部分是绘制到superview的frame外的话,那部分也会进行绘制,虽然那部分不可见。你可以设置superview的clipsToBounds属性为true。不管你有没有设置clip,触摸事件总是只会在可视区域里触发,也就是说如果在subview绘制在superview外的区域里产生点击事件,事件不会被传递到view里。
3. 坐标系统变换
坐标系统变换提供了一个方便快捷的方式来更改你的view或它的内容。仿射变换(affine transform)是指如何将某个坐标系统中的点映射到另一个坐标系统里。你可以用仿射变换来更改整个view的位置、大小或者它的朝向(orientation)。你也可以使用它来对绘制内容中的一部分来进行变换。如何使用仿射变换取决于你使用时的上下文:
- 应用于整个view,直接设置view的transform属性
- 应用于绘制内容中的一部分,在你的drawRect方法中,将仿射变换应用于给定的graphices上下文
通常修改view的transform属性是为了实现view的动画。你一般不会使用transform来改变你的view的位置或者大小。为了设置view的位置或大小,我们通常直接修改view的frame属性。
当修改view的transform属性时,所有的变换都是根据view的中心点(center)来进行的。
在你的drawRect方法里,通常根据(0,0)这个原点来对需要绘制的内容进行布局并应用变换。这样做的话,当你的view里的内容位置发生改变时,你只需要修改变换就可以了,这通常比重绘更快、性能更好。你可以通过CGContextGetCTM方法来活的graphics上下文关联的仿射变换对象。
Current transformation matrix(CTM)对象是指你当前正在使用的仿射变换。当你对整个view操作时,CTM就是存储在view的transform属性里的那个值。
每个subview的坐标系统是基于它的superview的,也就是说当你更改一个view的transform属性的时候,会影响到它所有的subview。但是这些改变只会影响到最终在屏幕上的绘制。因为每个view对于它的subview的绘制和布局都是根据它们自己的bounds,因此它会忽略superview的transform。
图1-6展示了两个不同的旋转变换是如何组合在一起的。在view的drawRect方法里我们将图形进行了45°的旋转。在view上我们又进行了45°的旋转。这样图形看上去被旋转了90°。
如果一个view的transform属性不是identity transform,view的frame属性必须被忽略。你应当使用view的bounds和center属性来获取size和位置。
4. 点和像素
在iOS里,所有坐标值都使用浮点型的值,单位是点。由于不同设备的像素密度是不同的,因此用点可以使用一种坐标值统一所有设备上的内容呈现。
当你确实需要使用图片或者其他基于像素的技术,例如:OpenGL ES。iOS会帮你管理这些点到像素之间的映射(还记得imageset里1x、2x、3x的图片么)。view也提供关于当前像素密度的信息,因此你可以根据这些信息来调整基于像素的绘制代码以适应高分辨率屏幕。
四、View的运行时交互模型
当用户和你的UI进行交互的时候或者当你编程来触发一些事件的时候,UIKit会有一些列复杂的事件序列产生。UIKit在适当的时候会通知你的view来响应这些事件。图1-7展示了从用户点击屏幕到最后图形系统相应点击并重绘屏幕的过程。
具体来说,当图1-7的情况发生时,产生了下列的事件序列:
- 用户点击屏幕(呵呵)
- 硬件向UIKit Framework报告点击事件
- UIKit Framework将点击事件打包成UIEvent对象传递给合适的view
- 事件处理代码响应事件,例如:
* 改变view或者subview的属性(frame, bounds, alpha等)
* 调用setNeedsLayout方法将view或者subview标记为需要更新布局
* 调用setNeedsDisplay或setNeedsDisplayInRect来标记view或者subview需要重绘
* 通知controller需要更新一些数据 - 如果view的几何外观改变了,UIKit会根据下面的规则去改变它的subview
a. 如果你有配置autoresizing规则,UIKit根据这些规则来调整view
b. 如果view实现了layoutSubviews方法,UIKit会调用它 - 如果view的任何一部分被标记为需要重绘,UIKit会让view重绘它。自定义view如果重载了drawRect方法,UIKit会调用它来进行重绘。
- 更新后的view和其他应用的可视内容会被发送到图形硬件来进行显示
- 图形硬件将绘制好的内容发送到屏幕
以上更新模型的步骤主要适用于使用了标准系统view的应用。使用OpenGL ES来绘制的应用通常直接在整个屏幕上进行绘制,并且不需要布局subview。
在上面的步骤中,自定义view的主要可干预点如下:
- 事件处理:
-touchesBegin:withEvent
-touchesMoved:withEvent
-touchesEnded:withEvent
-touchesCancelled:withEvent - layoutSubviews方法
- drawRect方法
五、高效使用View的技巧
1. 尽可能少的进行自定义绘制
尽管自定义绘制在有些时候是必须的,但是它也是你应当尽可能避免的。只有当系统的view没法提供你需要的外观或者功能时,你才需要这么做。任何时候,如果你可以通过组合使用系统的view来达到你的目的,那你就应当把它们组合在一个自定义的view层级结构里,而不是自己实现一个自定义view
2. 尽可能的利用content mode
适当的content mode可以减少你重绘所需的时间。你应当尽量避免使用UIViewContentModeRedraw。因为不管你使用什么content mode,你都能通过调用setNeedsDisplay或setNeedsDisplayInRect来强制你的view进行重绘
3. 尽可能将你的view设置成不透明
UIKit使用每个view的opaque属性来决定是否可以优化合成绘制操作。如果将这个值设置成true,UIKit就知道它不需要绘制任何这个view背后的内容(因为他们被你的view遮盖,并且不可见)。
4. 当滚动时调整view的绘制策略
滚动会在短时间内造成多次的view绘制。如果你的绘制代码没有优化过,滚动时就会非常的卡顿。因此不要尝试在滚动时保证你的view的内容都清晰可见,例如:你可以在滚动时将view的内容绘制质量临时降低,当滚动结束时,你再使用高质量重新绘制你的内容。
5. 不要通过嵌入subview来自定义系统控件
尽管技术上可以往标准系统控件(那些从UIControl继承的对象)里插入subview,但是请不要这么干!如果某个控件支持定制,它会在文档里详细说明,通过那些方法来定制是非常靠谱的。但是通过自己插入subview的方式来定制会造成你的应用在将来不可用(如果将来控件的实现机制改变了)。
下篇 View编程指南(三)