系统化学习,知其然,知其所以然
一、简介
在iOS中,可以使用 Windows 和 Views 在屏幕上显示应用程序的内容。 Windows 本身没有任何可见的内容,为 App 展示 Views 提供一个 root 容器。 Views 有2个任务:
- 使用 Window 一部分或者全部来展示指定内容的容器;
- 管理子视图。
所以每个 App 至少有一个 Window 和一个 View 来显示其内容。
UIKit 和其他 System Frameworks 提供了预定义的 Views,可以使用它来呈现你的内容。 这些 Views 的范围从简单的按钮和文本标签到更复杂的视图,如table views、picker views 和 scroll views。 在预定义视图满足不了需要的地方,还可以自定义 View。
二、View、Window架构
无论使用系统视图还是创建自己的自定义视图,都需要了解 UIView 和 UIWindow 类提供的基础功能。 这些类提供先进的工具来管理视图的布局和表示。 了解这些类的工作方式对于及时调整 View 非常重要。
2.1 View 基础架构
View在屏幕上定义了一个矩形区域,并处理该区域中的绘图和触摸事件。也可以作为其他视图的父项,并协调这些视图的布局和大小。
View 与 Layer 一起工作来处理视图内容的渲染和动画。 UIKit中的每个View都有一个Layer对象(通常是CALayer类的一个实例)支持,该对象负责渲染工作。一般情况下执行的大多数操作应该通过UIView接口;但是在需要更多地控制视图的渲染或动画行为的情况下,可以通过其Layer执行操作。
sequenceDiagram
UIView->>CALayer: 持有,通过layer属性访问
CALayer->>UIView: 处理渲染和动画相关内容
下图有助于理解View和Layer关系
[图片上传失败...(image-910a4c-1511255110112)]
Layer对象的使用对性能有重要的影响。 所以
- 尽可能少地调用View对象的实际绘图代码,
- 并且当调用代码时,结果被Core Animation缓存,并尽可能被重用。
重用已有内容消除了一般情况下更新视图需要经历的的耗时。 当动画中有重复内容时,重用非常重要,这种重复使用比创建新内容代价小得多。
2.2 View 层次结构和子视图管理
除了提供自己的内容之外,视图还可以充当其他视图的容器。当一个视图包含另一个视图时,两个视图之间会创建一个父子关系。
sequenceDiagram
父视图->>子视图: 是子视图的superview
子视图->>父视图: 是父视图的subview
Superview将其子视图存储在有序数组中,并且该数组中的顺序也会影响每个子视图的可见性。如果两个subview彼此重叠,则最后添加的子视图(或移动到子视图数组的末尾)会出现在另一个之上。Subview的显示效果受Superview影响,例如位置、大小、透明度等。
。
2.3 The View Drawing Cycle
graph LR
首次显示-->绘制内容
绘制内容-->获取快照
获取快照-->展示快照
展示快照-->内容更改
内容更改-->首次显示
UIView按需绘制模型来呈现内容。当一个视图第一次出现在屏幕上时,系统按要求它画出其内容。系统捕获此内容的快照,并将该快照用作视图的视觉表示。如果你永远不改变视图的内容,视图的绘图代码可能永远不会再被调用。大多数涉及视图的操作都会重用快照图像。如果您更改内容,则通知系统视图已更改。该视图然后重复绘制视图并捕获新结果的快照的过程。
当你的视图的内容改变时,你不要直接重绘这些改变。而是使用
- (void)setNeedsDisplay;
或
- (void)setNeedsDisplayInRect:(CGRect)rect;
方法使视图失效。这些方法告诉系统,视图的内容改变了,需要在下一个机会重新绘制。在启动任何绘图操作之前,系统等待当前运行循环的结束。这种延迟使您有机会使多个视图失效,从您的层次结构添加或删除视图,隐藏视图,调整视图大小,并一次重新定位视图。然后你所做的所有改变都会同时反映出来。
注:更改View的几何属性不会自动导致系统重新绘制视图的内容。视图的contentMode属性确定如何对视图几何体的更改。大多数contentMode在视图的边界内拉伸或重新定位现有的快照,而不是创建一个新的快照。
当呈现视图的内容时,实际的绘图过程会根据视图及其配置而变化。系统视图通常实现私有绘图方法来呈现其内容。这些相同的系统视图经常公开可用于配置视图的实际外观的接口。对于自定义UIView子类,通常会覆盖视图的drawRect:方法,并使用该方法绘制视图的内容。还有其他方法可以提供视图的内容,比如直接设置底层的内容,但是覆盖drawRect:方法是最常用的技术。
2.4 显示模式 Content Modes
每个视图都有一个Content Mode,用于控制视图如何响应其内容以适应视图几何属性的变化以及是否回收其内容。当视图第一次显示时,它像往常一样渲染其内容,并将结果捕获在底层位图中。之后,对视图几何属性的更改并不总是会导致重新创建位图。相反,contentMode属性中的值决定是否缩放位图以适应新的边界,或者只是固定到视图的一个角或边缘。
视图的Content Mode在执行以下操作时用到:
- 更改 View 的 frame 或 bounds 的宽度或高度。
- view's transform 有 scaling 参数变化。
默认情况下,大多数视图的contentMode属性被设置为UIViewContentModeScaleToFill
,这会导致视图的内容被缩放以适应新的帧大小。下图显示了一些可用的内容模式的结果。从图中可以看出,并不是所有的内容模式都会导致视图的边界被完全填充,而那些内容模式可能会扭曲视图的内容
慎用UIViewContentModeRedraw 属性,尤其是 System Views
2.5 拉伸视图 Stretchable Views
可以指定视图的一部分为可拉伸的,以便当视图的大小改变时,只有可拉伸部分的内容受到影响。
通常在按钮或其他视图中使用可拉伸区域,其中部分视图定义了可重复的图案。 指定的可拉伸区域可以允许沿视图的一个或两个轴伸展。 当然,当沿着两个轴伸展视图时,视图的边缘也必须定义可重复的图案以避免任何失真。
如图显示了这种扭曲是如何在视图中体现出来的。 来自每个视图的原始像素的颜色被复制以填充大视图中的对应区域。
[图片上传失败...(image-3ca96e-1511255110112)]
可以使用contentStretch属性指定视图的可拉伸区域。该属性接受一个矩形,其值被规范化为0.0到1.0的范围。当拉伸视图时,系统将这些归一化值乘以视图的当前边界和比例因子,以确定哪些像素或像素需要拉伸。每当视图边界发生变化时,使用规范化值就可以减少更新contentStretch属性的必要性。
contentStretch属性已废弃,使用新接口
视图的 Content Mode 在确定如何使用视图的可拉伸区域方面也起着重要作用。仅当 Content Mode 会导致视图的内容被缩放时才使用可伸缩区域。这意味着只有
UIViewContentModeScaleToFill,
UIViewContentModeScaleAspectFit,
UIViewContentModeScaleAspectFill
才支持可伸缩视图。如果指定将内容固定到边或角的Content Mode(因此实际上不会缩放内容),则视图将忽略可拉伸区域。
注意:在为视图指定背景时,建议使用contentStretch属性来创建可拉伸的UIImage对象。可伸缩视图完全在Core Animation层中处理,通常可以提供更好的性能。
2.6 内置动画支持 Built-In Animation Support
在每个视图背后都有一个图层对象的好处之一是可以轻松地动画许多与视图相关的更改。动画是向用户传递信息的有效方法,在设计应用程序时应始终考虑动画。 UIView类的许多属性都是可以动画的 - 也就是说,半自动支持存在从一个值到另一个值的动画。要为其中一个动画属性执行动画,您只需执行以下操作:
- 告诉UIKit你想要执行一个动画
- 更改属性的值。
你可以在UIView对象上动画的属性如下:
- frame — Use this to animate position and size changes for the view.
- bounds — Use this to animate changes to the size of the view.
- center — Use this to animate the position of the view.
- transform — Use this to rotate or scale the view.
- alpha — Use this to change the transparency of the view.
- backgroundColor — Use this to change the background color of the view.
- contentStretch — Use this to change how the view’s contents stretch.
动画非常重要的一个地方是从一组视图转换到另一组视图。通常,使用视图控制器来管理与用户界面各部分之间的重大更改相关的动画。例如,对于涉及从较高层级到较低层级的接口,通常使用导航控制器来管理View直接的跳转和显示。可以在view controller跳转动画不能满足需要时,使用Views进行动画跳转。
除了使用UIKit类创建的动画外,还可以使用Core Animation图层创建动画。底层图层可以更好地控制动画的时间和属性。
三、视图几何和坐标系统
UIKit坐标原点
[图片上传失败...(image-b47bbf-1511255110112)]
(注意:Core Graphics 和 OpenGL ES 左边原点在左下角)
3.1 Frame、Bounds、Center关系
属性 | 详情 |
---|---|
Frame | 在其父视图的坐标系中指定视图的大小和位置 |
Bounds | 在视图自己的本地坐标系统中指定视图(及其内容原点)的大小。 |
Center | 在其父视图的坐标系中指定视图中心点 |
虽然以上三者都可以独立修改,但是修改其中一个会影响到其他值。
属性 | 影响值 |
---|---|
Frame | Bounds、Center |
Bounds | Frame |
Center | Frame |
下图有助于理解三者关系
3.2 坐标系转换(Coordinate System Transformations)
坐标系转换提供了一种快速方便地更改视图(或其内容)的方法。仿射变换是一个数学矩阵,指定一个坐标系中的点如何映射到不同坐标系中的点。您可以将仿射变换应用于整个视图,以相对于其超视图更改视图的大小,位置或方向。您还可以在绘图代码中使用仿射变换对各个渲染内容执行相同类型的操作。
如何应用仿射变换取决于 context:
要修改整个视图,请在视图的transform属性中修改仿射变换。Translating, Scaling, and Rotating Views
要修改视图的drawRect:方法中特定的内容片段,请修改与 active graphics context 关联的仿射变换。Drawing and Printing Guide for iOS
当实现动画时,通常会修改视图的transform属性。例如,您可以使用此属性来创建围绕其中心点旋转的视图的动画。不要使用此属性对您的视图进行永久更改,例如在其父视图的坐标空间内修改其视图的位置或大小。对于这种类型的更改,您应该修改视图的 frame 属性。
注意:修改视图的transform属性时,所有的转换都是相对于视图的中心点执行的。
在您的视图的drawRect:方法中,使用仿射变换来定位和定位您打算绘制的项目。不是在视图的某个位置固定对象的位置,而是相对于固定点(通常为(0,0))创建每个对象并在绘制之前立即使用变换来定位对象。这样,如果对象的位置在您的视图中发生变化,您所要做的就是修改变换,这比在新位置重新创建对象要快得多,成本也更低。您可以使用CGContextGetCTM函数检索与图形上下文关联的仿射变换,并且可以使用相关的核心图形函数在绘图期间设置或修改此变换。
当前变换矩阵(CTM)是在任何给定时间使用的仿射变换。在操作整个视图的几何图形时,CTM是存储在视图的transform属性中的仿射变换。在drawRect:方法中,CTM是与活动图形上下文关联的仿射变换。
每个子视图的坐标系建立在其祖先的坐标系上。所以,当你修改一个视图的transform属性时,这个改变会影响视图及其所有的子视图。但是,这些更改仅影响屏幕上视图的最终呈现。由于每个视图都是绘制其内容,并将其子视图相对于其边界进行布局,所以在绘制和布局过程中可以忽略其超视图的变换。
[图片上传失败...(image-b31b78-1511255110112)]
重要提示:如果视图的transform属性不是 identity transform,则该视图的frame属性值是未定义的,必须忽略。 将变换应用于视图时,必须使用视图的边界和中心属性来获取视图的大小和位置。 其子视图的 frame 仍然有效,因为它们是相对于它的 bounds 属性。
3.3 点和像素(Points Versus Pixels)
一点不一定对应于屏幕上的一个像素。 对应关系由操作系统决定,不需要关注。
四、视图交互过程(The Runtime Interaction Model for Views)
每当用户与您的用户界面进行交互时,或者您自己的代码以编程方式更改某些内容时,都会在UIKit内部发生一系列复杂的事件来处理该交互。
在这个序列的特定时间点,UIKit会调用您的视图类,并让他们有机会代表您的应用程序进行响应。理解这些标注点对于理解视图适合系统的位置很重要。图1-7显示了用户触摸屏幕开始的事件的基本顺序,以图形系统作为响应更新屏幕内容结束。任何由程序启动的动作也会发生相同的事件序列。
以下步骤将图中的事件序列进一步分解,并解释每个阶段会发生什么情况,以及应用程序如何响应。
用户触摸屏幕
硬件将触发事件派发到UIKit
UIKit将触摸事件封装到UIEvent对象中,并将其分派到相应的View。
-
View处理事件并做出响应。例如,
- 更改view或其subviews的属性(frame,bounds,alpha等)。
- 调用setNeedsLayout方法将view(或subview)标记为需要更新布局。
- 调用setNeedsDisplay或setNeedsDisplayInRect:方法将view(或subview)标记为需要重绘。
- 通知控制器关于某些数据的更改。
-
如果view的几何属性改变,则UIKit根据以下规则更新subview:
- 如果为view设置了自动调整规则,则UIKit会根据这些规则调整每个视图。Handling Layout Changes Automatically Using Autoresizing Rules
- 如果view实现了layoutSubviews方法,UIKit会调用它。
可以在自定义视图中重写此方法,并使用它来调整任何子视图的位置和大小。例如,提供大的可滚动区域的视图将需要使用多个子视图作为“图块”,而不是创建较大视图(内存原因)。执行此方法时,视图将隐藏或重定位不在屏幕上的任何子视图,并使用它们绘制新开的内容。其中,视图的布局代码也可以使任何需要重绘的视图失效。
-
如果任何view的任何部分被标记为需要重绘,UIKit会要求view重绘自己。
- 对于明确定义drawRect:方法的自定义视图,UIKit会调用此方法。这个方法的实现应该尽可能快的重绘视图的指定区域,而不是其他的。此时不要进行额外的布局更改,也不要对应用程序的数据模型进行其他更改。此方法的目的是更新视图的视觉内容。
- 标准系统视图通常不执行drawRect:方法,但是管理他们的绘图。
任何较新的视图都与应用程序的其余可见内容合成,并发送到graphics hardware进行显示
Graphics hardware 将渲染的内容传输到屏幕上。
在前面的一系列步骤中,自定义view的主要集成点是:
- 事件处理方法:
- (void)touchesBegan:(NSSet<UITouch *> *)touches
withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches
withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches
withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches
withEvent:(UIEvent *)event;
layoutSubviews方法
drawRect:方法
这些是视图中最常用的重写方法,但是可能不需要重写所有这些方法。
如果使用手势识别器来处理事件,则不需要重写任何事件处理方法。
如果您的视图不包含子视图或其大小不会更改,则没有理由重写layoutSubviews方法。
只有在视图的内容可以在运行时更改并且使用原生技术(如UIKit或Core Graphics)进行绘制时,才需要drawRect:方法。
这些是主要的整合点,但不是所有的。其他请参考 UIView。
五、 提升使用效率 Tips for Using Views Effectively
当系统提供的标准视图不能满足需要时,需要自定义view。自定义view需要注意优化性能,可以从以下几点考虑
5.1 Views Do Not Always Have a Corresponding View Controller
视图和视图控制器之间很少有一对一的关系。视图控制器的工作是管理一个视图层次结构,通常由多个视图组成,用于实现一些独立的功能。对于iPhone应用程序,每个视图层次结构通常填充整个屏幕,但对于iPad应用程序,视图层次结构可能只填充屏幕的一部分。
在设计应用程序的用户界面时,考虑视图控制器将扮演的角色非常重要。视图控制器提供了许多重要的行为,例如协调屏幕上的视图显示,协调从屏幕上移除这些视图,响应低内存警告释放内存,以及响应接口方向更改而旋转视图。避免这些行为可能会导致您的应用程序出现错误或意外的行为。
5.2 尽量减少自定义绘图(Minimize Custom Drawing)
尽量使用系统提供的标准view,不行试试组合view,实在不行再去自定义绘图。
5.3 Take Advantage of Content Modes
Content Modes式可以减少重绘视图的时间。 默认情况下,视图使用 UIViewContentModeScaleToFill ,该 Content Modes 缩放视图的现有内容以适合视图的框架矩形。 您可以根据需要更改此模式以不同的方式调整您的内容,但是如果可以的话,应该避免使用 UIViewContentModeRedraw 内容模式。 无论使用哪一种Content Modes,都可以通过调用 setNeedsDisplay 或 setNeedsDisplayInRect:来强制视图重绘其内容。
5.4 尽量使用不透明View(Declare Views as Opaque Whenever Possible)
UIKit使用每个视图的opaque属性来确定视图是否可以优化合成操作。建议opaque = YES, 告诉UIKit它不需要在视图后面呈现任何内容, 较少的渲染会导致您的绘图代码的性能提高。 当然,如果将opaque属性设置为YES,则视图必须用完全不透明的内容完全填充其bounds。
5.5 滚动时调整视图的绘图(Adjust Your View’s Drawing Behavior When Scrolling)
滚动可以在很短的时间内产生大量的视图更新。如果您的视图的绘制代码没有适当地调整,则视图的滚动性能可能会很低。在开始滚动操作时,不要试图确保视图的内容始终处于原始状态,而应考虑更改视图的行为。例如,您可以暂时降低渲染内容的质量,或在滚动正在进行时更改内容模式。当滚动停止时,您可以将视图返回到之前的状态,并根据需要更新内容。
5.6 不要通过嵌入子视图来自定义控件(Do Not Customize Controls by Embedding Subviews)
尽管在技术上可以将子视图添加到标准系统控件(从UIControl继承的对象),但不应该以这种方式自定义它们。可以通过控件类本身的明确和详细记录的接口来实现自定义效果。例如,UIButton类包含设置按钮的标题和背景图像的方法。使用标准接口能够展示正常效果,非常收到会导致异常。