Core Animation基础
Core Animation利用了硬件加速和架构上的优化来实现快速渲染和实时动画。当视图的drawRect:
方法首次被调用时,层会将描画的结果捕捉到一个位图中,并在随后的重画中尽可能使用这个缓存的位图,以避免调用开销很大的drawRect:
方法。这个过程使Core Animation得以优化合成操作,取得期望的性能。
Core Animation把和视图对象相关联的层存储在一个被称为层树的层次结构中。和视图一 样,层树中的每个层都只有一个父亲,但可以嵌入任意数量的子层。缺省情况下,层树中对象的组织方式和视图在视图层次中的组织方式完全一样。但是,您可以在 层树中添加层,而不同时添加相应的视图。当您希望实现某种特殊的视觉效果、而又不需要在视图上保持这种效果时,就可能需要这种技术。
实际上,层对象是iPhone OS渲染和布局系统的推动力,大多数视图属性实际上是其层对象属性的一个很薄的封装。当您(直接使用CALayer
对 象)修改层树上层对象的属性时,您所做的改变会立即反映在层对象上。但是,如果该变化触发了相应的动画,则可能不会立即反映在屏幕上,而是必须随着时间的 变化以动画的形式表现在屏幕上。为了管理这种类型的动画,Core Animation额外维护两组层对象,我们称之为表示树和渲染树。
表示树反映的是层在展示给用户时的当前状态。假定您对层值的变化实行动画,则在动画开始时,表示层反映的是老的值;随着动画的进 行,Core Animation会根据动画的当前帧来更新表示树层的值;然后,渲染树就和表示树一起,将变化渲染在屏幕上。由于渲染树运行在单独的进程或线程上,所以 它所做的工作并不影响应用程序的主运行循环。虽然层树和表示树都是公开的,但是渲染树的接口是私有。
在视图后面设置层对象对描画代码的性能有很多重要的影响。使用层的好处在于视图的大多数几何变化都不需要重画。举例来说,改变视图的位置和尺寸并需要重画视图的内容,只需简单地重用层缓存的位图就可以了。对缓存的内容实行动画比每次都重画内容要有效得多。
使用层的缺点在于层是额外的缓存数据,会增加应用程序的内存压力。如果您的应用程序创建太多的视图,或者创建多个很大的视图,则可能很 快就会出现内存不够用的情形。您不用担心在应用程序中使用视图,但是,如果有现成的视图可以重用,就不要创建新的视图对象。换句话说,您应该设法使内存中 同时存在的视图对象数量最小。
有关Core Animation的进一步概述、对象树、以及如何创建动画,请参见Core Animation编程指南。
改变视图的层
在iPhone OS系统中,由于视图必须有一个与之关联的层对象,所以UIView
类在初始化时会自动创建相应的层。您可以通过视图的layer
属性访问这个层,但是不能在视图创建完成后改变层对象。
如果您希望视图使用不同类型的层,必须重载其layerClass
类方法,并在该方法中返回您希望使用的层对象。使用不同层类的最常见理由是为了实现一个基于OpenGL的应用程序。为了使用OpenGL描画命令,视图下面的层必须是CAEAGLLayer
类的实例,这种类型的层可以和OpenGL渲染调用进行交互,最终在屏幕上显示期望的内容。
重要提示:您永远不应修改视图层的delegate
属性,该属性用于存储一个指向视图的指针,应该被认为是私有的。类似地,由于一个视图只能作为一个层的委托,所以您必须避免将它作为其它层对象的委托,否则会导致应用程序崩溃。
动画支持
iPhone OS的每个视图后面都有一个层对象,这样做的好处之一是使视图内容更加易于实现动画。请记住,动画并不一定是为了在视觉上吸引眼球,它可以将应用程序界面 变化的上下文呈现给用户。举例来说,当您在屏幕转移过程中使用过渡时,过渡本身就向用户指示屏幕之间的联系。系统自动支持了很多经常使用的动画,但您也可 以为界面上的其它部分创建动画。
UIView
类的很多属性都被设计为可动画的(animatable)。可动画的属性是指当属性从一个值变为另一个值的时候,可以半自动地支持动画。您仍然必须告诉UIKit希望执行什么类型的动画,但是动画一旦开始,Core Animation就会全权负责。UIView
对象中支持动画的属性有如下几个:
frame
bounds
center
transform
alpha
虽然其它的视图属性不直接支持动画,但是您可以为其中的一部分显式创建动画。显式动画要求您做很多管理动画和渲染内容的工作,通过使用Core Animation提供的基础设施,这些工作仍然可以得到良好的性能。
有关如何通过UIView
类创建动画的更多信息,请参见“实现视图动画”部分;有关如何创建显式动画的更多信息,则请参见Core Animation编程指南。
视图坐标系统
UIKit中的坐标是基于这样的坐标系统:以左上角为坐标的原点,原点向下和向右为坐标轴正向。坐标值由浮点数来表示,内容的布局和定位因此具有更高的精度,还可以支持与分辨率无关的特性。图2-3显示了这个相对于屏幕的坐标系统,这个坐标系统同时也用于UIWindow
和UIView
类。视图坐标系统的方向和Quartz及Mac OS X使用的缺省方向不同,选择这个特殊的方向是为了使布局用户界面上的控件及内容更加容易。
图2-3 视图坐标系统
您在编写界面代码时,需要知道当前起作用的坐标系统。每个窗口和视图对象都维护一个自己本地的坐标系统。视图中发生的所有描画都是相对 于视图本地的坐标系统。但是,每个视图的边框矩形都是通过其父视图的坐标系统来指定,而事件对象携带的坐标信息则是相对于应用程序窗口的坐标系统。为了方 便,UIWindow
和UIView
类都提供了一些方法,用于在不同对象之间进行坐标系统的转换。
虽然Quartz使用的坐标系统不以左上角为原点,但是对于很多Quartz调用来说,这并不是问题。在调用视图的drawRect:
方法之前,UIKit会自动对描画环境进行配置,使左上角成为坐标系统的原点,在这个环境中发生的Quartz调用都可以正确地在视图中描画。您唯一需要考虑不同坐标系统之间差别的场合是当您自行通过Quartz建立描画环境的时候。
更多有关坐标系统、Quartz、和描画的一般信息,请参见“图形和描画”部分。
边框、边界、和中心的关系
视图对象通过frame
、bounds
、和center
属性声明来跟踪自己的大小和位置。frame
属性包含一个矩形,即边框矩形,用于指定视图相对于其父视图坐标系统的位置和大小。bounds
属性也包含一个矩形,即边界矩形,负责定义视图相对于本地坐标系统的位置和大小。虽然边界矩形的原点通常被设置为 (0, 0),但这并不是必须的。center
属性包含边框矩形的中心点。
在代码中,您可以将frame
、bounds
、和center
属性用于不同的目的。边界矩形代表视图本地的坐标系统,因此,在描画和事件处理代码中,经常借助它来取得视图中发生事件或需要更新的位置。中心点代表视图的中心,改变中心点一直是移动视图位置的最好方法。边框矩形是一个通过bounds
和center
属性计算得到的便利值,只有当视图的变换属性被设置恒等变换时,边框矩形才是有效的。
图2-4显示了边框矩形和边界矩形之间的关系。右边的整个图像 是从视图的(0, 0)开始描画的,但是由于边界的大小和整个图像的尺寸不相匹配,所以位于边界矩形之外的图像部分被自动裁剪。在视图和它的父视图进行合成的时候,视图在其 父视图中的位置是由视图边框矩形的原点决定的。在这个例子中,该原点是(5, 5)。结果,视图的内容就相对于父视图的原点向下向右移动相应的尺寸。
图2-4 视图的边框和边界之间的关系
如果没有经过变换,视图的位置和大小就由上述三个互相关联的属性决定的。当您在代码中通过initWithFrame:
方法创建一个视图对象时,其frame
属性就会被设置。该方法同时也将bounds
矩形的原点初始化为(0.0, 0.0),大小则和视图的边框相同。然后center
属性会被设置为边框的中心点。
虽然您可以分别设置这些属性的值,但是设置其中的一个属性会引起其它属性的改变,具体关系如下:
- 当您设置
frame
属性时,bounds
属性的大小会被设置为与frame
属性的大小相匹配的值,center
属性也会被调整为与新的边框中心点相匹配的值。 - 当您设置
center
属性时,frame
的原点也会随之改变。 - 当您设置
bounds
矩形的大小时,frame
矩形的大小也会随之改变。
您可以改变bounds
的原点而不影响其它两个属性。当您这样做时,视图会显示您标识的图形部分。在图2-4中,边界的原点被设置为(0.0, 0.0)。在图2-5中,该原点被移动到(8.0, 24.0)。结果,显示出来的是视图图像的不同部分。但是,由于边框矩形并没有改变,新的内容在父视图中的位置和之前是一样的。
图2-5 改变视图的边界
请注意:缺省情况下,视图的边框并不会被父视图的边框裁剪。如果您希望让一个视图裁剪其子视图,需要将其clipsToBounds
属性设置为YES
。
坐标系统变换
在视图的drawRect:
方法中常常借助坐标系统变换来进行描画。而在iPhone OS系统中,您还可以用它来实现视图的某些视觉效果。举例来说,UIView
类中包含一个transform
属性声明,您可以通过它来对整个视图实行各种类型的平移、比例缩放、和变焦缩放效果。缺省情况下,这个属性的值是一个恒等变换,不会改变视图的外观。在加入变换之前,首先要得到该属性中存储的CGAffineTransform
结构,用相应的Core Graphics函数实行变换,然后再将修改后的变换结构重新赋值给视图的transform
属性。
请注意:当您将变换应用到视图时,所有执行的变换都是相对于视图的中心点。
平移一个视图会使其所有的子视图和视图本身的内容一起移动。由于子视图的坐标系统是继承并建立在这些变化的基础上的,所以比例缩放也会影响子视图的描画。有关如何控制视图内容缩放的更多信息,请参见“内容模式和比例缩放”部分。
重要提示:如果transform
属性的值不是恒等变换,则frame
属性的值就是未定义的,必须被忽略。在设置变换属性之后,请使用bounds
和center
属性来获取视图的位置和大小。
有关如何在drawRect:
方法中使用变换的信息,请参见“坐标和坐标变换”部分;有关用于修改CGAffineTransform
结构的函数,则请参见CGAffineTransform参考。
内容模式与比例缩放
当您改变视图的边界,或者将一个比例因子应用到视图的transform
属性声明时,边框矩形会发生等量的变化。根据内容模式的不同,视图的内容也可能被缩放或重新定位,以反映上述的变化。视图的contentMode
属性决定了边界变化和缩放操作作用到视图上产生的效果。缺省情况下,这个属性的值被设置为UIViewContentModeScaleToFill
,意味着视图内容总是被缩放,以适应新的边框尺寸。作为例子,图2-6显示了当视图的水平缩放因子放大一倍时产生的效果。
图2-6 使用scale-to-fill内容模式缩放视图
视图内容的缩放仅在首次显示视图的时候发生,渲染后的内容会被缓存在视图下面的层上。当边界或缩放因子发生变化时,UIKit并不强制视图进行重画,而是根据其内容模式决定如何显示缓存的内容。图2-7比较了在不同的内容模式下,改变视图边界或应用不同的比例缩放因子时产生的结果。
图2-7 内容模式比较
对视图应用一个比例缩放因子总是会使其内容发生缩放,而边界的改变在某些内容模式下则不会发生同样的结果。不同的UIViewContentMode
常量(比如UIViewContentModeTop
和UIViewContentModeBottomRight
)可以使当前的内容在视图的不同角落或沿着视图的不同边界显示,还有一种模式可以将内容显示在视图的中心。在这些模式的作用下,改变边界矩形只会简单地将现有的视图内容移动到新的边界矩形中对应的位置上。
当您希望在应用程序中实现尺寸可调整的控件时,请务必考虑使用内容模式。这样做可以避免控件的外观发生变形,以及避免编写定制的描画代 码。按键和分段控件(segmented control)特别适合基于内容模式的描画。它们通常使用几个图像来创建控件外观。除了有两个固定尺寸的盖帽图像之外,按键可以通过一个可伸展的、宽度 只有一个像素的中心图像来实现水平方向的尺寸调整。它将每个图像显示在自己的图像视图中,而将可伸展的中间图像的内容模式设置为UIViewContentModeScaleToFill
,使得在尺寸调整时两端的外观不会变形。更为重要的是,每个图像视图的关联图像都可以由Core Animation来缓存,因此不需要编写描画代码就可以支持动画,从而使大大提高了性能。
内容模式通常有助于避免视图内容的描画,但是当您希望对缩放和尺寸调整过程中的视图外观进行特别的控制时,也可以使用UIViewContentModeRedraw
模式。将视图的内容模式设置为这个值可以强制Core Animation使视图的内容失效,并调用视图的drawRect:
方法,而不是自动进行缩放或尺寸调整。
自动尺寸调整行为
当您改变视图的边框矩形时,其内嵌子视图的位置和尺寸往往也需要改变,以适应原始视图的新尺寸。如果视图的autoresizesSubviews
属性声明被设置为YES
,则其子视图会根据autoresizingMask
属性的值自动进行尺寸调整。简单配置一下视图的自动尺寸调整掩码常常就能使应用程序得到合适的行为;否则,应用程序就必须通过重载layoutSubviews
方法来提供自己的实现。
设置视图的自动尺寸调整行为的方法是通过位OR操作符将期望的自动尺寸调整常量连结起来,并将结果赋值给视图的autoresizingMask
属性。表2-1列举了自动尺寸调整常量,并描述这些常量如何影响给定视图的尺寸和位置。举例来说,如果要使一个视图和其父视图左下角的相对位置保持不变,可以加入UIViewAutoresizingFlexibleRightMargin
和UIViewAutoresizingFlexibleTopMargin
常量,并将结果赋值给autoresizingMask
属性。当同一个轴向有多个部分被设置为可变时,尺寸调整的裕量会被平均分配到各个部分上。
自动尺寸调整掩码 |
描述 |
---|---|
UIViewAutoresizingNone
|
这个常量如果被设置,视图将不进行自动尺寸调整。 |
UIViewAutoresizingFlexibleHeight
|
这个常量如果被设置,视图的高度将和父视图的高度一起成比例变化。否则,视图的高度将保持不变。 |
UIViewAutoresizingFlexibleWidth
|
这个常量如果被设置,视图的宽度将和父视图的宽度一起成比例变化。否则,视图的宽度将保持不变。 |
UIViewAutoresizingFlexibleLeftMargin
|
这个常量如果被设置,视图的左边界将随着父视图宽度的变化而按比例进行调整。否则,视图和其父视图的左边界的相对位置将保持不变。 |
UIViewAutoresizingFlexibleRightMargin
|
这个常量如果被设置,视图的右边界将随着父视图宽度的变化而按比例进行调整。否则,视图和其父视图的右边界的相对位置将保持不变。 |
UIViewAutoresizingFlexibleBottomMargin
|
这个常量如果被设置,视图的底边界将随着父视图高度的变化而按比例进行调整。否则,视图和其父视图的底边界的相对位置将保持不变。 |
UIViewAutoresizingFlexibleTopMargin
|
这个常量如果被设置,视图的上边界将随着父视图高度的变化而按比例进行调整。否则,视图和其父视图的上边界的相对位置将保持不变。 |
图2-8为这些常量值的位置提供了一个图形表示。如果这些常量之一被省略,则视图在相应方向上的布局就被固定;如果某个常量被包含在掩码中,在该方向的视图布局就就灵活的。
图2-8 视图的自动尺寸调整掩码常量
如果您通过Interface Builder配置视图,则可以用Size查看器的Autosizing控制来设置每个视图的自动尺寸调整行为。上图中的灵活宽度及高度常量和 Interface Builder中位于同样位置的弹簧具有同样的行为,但是空白常量的行为则是正好相反。换句话说,如果要将灵活右空白的自动尺寸调整行为应用到 Interface Builder的某个视图,必须使相应方向空间的Autosizing控制为空,而不是放置一个支柱。幸运的是,Interface Builder通过动画显示了您的修改对视图自动尺寸调整行为的影响。
如果视图的autoresizesSubviews
属性被设置为NO
,则该视图的直接子视图的所有自动尺寸调整行为将被忽略。类似地,如果一个子视图的自动尺寸调整掩码被设置为UIViewAutoresizingNone
,则该子视图的尺寸将不会被调整,因而其直接子视图的尺寸也不会被调整。
请注意:为了使自动尺寸调整的行为正确,视图的transform
属性必须设置为恒等变换;其它变换下的尺寸自动调整行为是未定义的。
自动尺寸调整行为可以适合一些布局的要求,但是如果您希望更多地控制视图的布局,可以在适当的视图类中重载layoutSubviews
方法。有关视图布局管理的更多信息,请参见“响应布局的变化”部分。
创建和管理视图层次
管理用户界面的视图层次是开发应用程序用户界面的关键部分。视图的组织方式不仅定义了应用程序的视觉外观,而且还定义了应用程序如何响 应变化。视图层次中的父-子关系可以帮助我们定义应用程序中负责处理触摸事件的对象链。当用户旋转设备时,父-子关系也有助于定义每个视图的尺寸和位置是 如何随着界面方向的变化而变化的。
图2-9显示了一个简单的例子,说明如何通过视图的分层来创建期望的视觉效果。在Clock程序中,页签条和导航条视图,以及定制视图混合在一起,实现了整个界面。
图2-9 Clock程序的视图层
如果您探究Clock程序中视图之间的关系,就会发现它们很像“改变视图的层”部分中显示的关系,窗口对象是应用程序的页签条、导航条、和定制视图的根视图。
图2-10 Clock程序的视图层次
在iPhone应用程序的开发过程中,有几种建立视图层次的方法,包括基于Interface Builder的可视化方法和通过代码编程的方法。本文的下面部分将向您介绍如何装配视图层次,以及如何在建立视图层次之后寻找其中的视图,还有如何在不 同的视图坐标系统之间进行转换。
创建一个视图对象
创建视图对象的最简单方法是使用Interface Builder进行制作,然后将视图对象从作成的nib文件载 入内存。在Interface Builder的图形环境中,您可以将新的视图从库中拖出,然后放到窗口或另一个视图中,以快速建立需要的视图层次。Interface Builder使用的是活的视图对象,因此,当您用这个图形环境构建用户界面时,所看到的就是运行时装载的外观,而且不需要为视图层次中的每个视图编写单 调乏味的内存分配和初始化代码。
如果您不喜欢Interface Builder和nib文件,也可以通过代码来创建视图。创建一个新的视图对象时,需要为其分配内存,并向该对象发送一个initWithFrame:
消息,以对其进行初始化。举例来说,如果您要创建一个新的UIView
类的实例作为其它视图的容器,则可以使用下面的代码:
CGRect viewRect = CGRectMake(0, 0, 100, 100); |
UIView* myView = [[UIView alloc] initWithFrame:viewRect]; |
请注意:虽然所有系统提供的视图对象都支持initWithFrame:
消息,但是其中的一部分可能有自己偏好的初始化方法,您应该使用那些方法。有关定制初始化方法的更多信息,请参见相应的类参考文档。
您在视图初始化时指定的边框矩形代表该视图相对于未来父视图的位置和大小。在将视图显示于屏幕上之前,您需要将它加入到窗口或其它视图中。在这个时候,UIKit会根据您指定的边框矩形将视图放置到其父视图的相应位置中。有关如何将视图添加到视图层次的信息,请参见“添加和移除子视图”部分。
添加和移除子视图
Interface Builder是建立视图层次的最便利工具,因为它可以让您看到视图在运行时的外观。在界面制作完成后,它将视图对象及其层次关系保存在nib文件中。在 运行时,系统会按照nib文件的内容为应用程序重新创建那些对象和关系。当一个nib文件被装载时,系统会自动调用重建视图层次所需要的UIView
方法。
如果您不喜欢通过Interface Builder和nib文件来创建视图层次,则可以通过代码来创建。如果一个视图必须具有某些子视图才能工作,则应该在其initWithFrame:
方法中进行对其创建,以确保子视图可以和视图一起被显示和初始化。如果子视图是应用程序设计的一部分(而不是视图工作必需的),则应该在视图的初始化代码之外进行创建。在iPhone程序中,有两个地方最常用于创建视图和子视图,它们是应用程序委托对象的applicationDidFinishLaunching:
方法和视图控制器的loadView
方法。
您可以通过下面的方法来操作视图层次中的视图对象:
- 调用父视图的
addSubview:
方法来添加视图,该方法将一个视图添加到子视图列表的最后。 - 调用父视图的
insertSubview:...
方法可以在父视图的子视图列表中间插入视图。 - 调用父视图的
bringSubviewToFront:
、sendSubviewToBack:
、或exchangeSubviewAtIndex:withSubviewAtIndex:
方法可以对父视图的子视图进行重新排序。使用这些方法比从父视图中移除子视图并再次插入要快一些。 - 调用子视图(而不是父视图)的
removeFromSuperview
方法可以将子视图从父视图中移除。
在添加子视图时,UIKit会根据子视图的当前边框矩形确定其在父视图中的初始位置。您可以随时通过修改子视图的frame
属性声明来改变其位置。缺省情况下,边框位于父视图可视边界外部的子视图不会被裁剪。如果您希望激活裁剪功能,必须将父视图的clipsToBounds
属性设置为YES
。
程序清单2-1显示了一个应用程序委托对象的applicationDidFinishLaunching:
方法示例。在这个例子中,应用程序委托在启动时通过代码创建全部的用户界面。界面中包含两个普通的UIView
对象,用于显示基本颜色。每个视图都被嵌入到窗口中,窗口也是UIView
的一个子类,因此可以作为父视图。父视图会保持它们的子视图,因此这个方法释放了新创建的视图对象,以避免重复保持。
程序清单2-1 创建一个带有视图的窗口
- (void)applicationDidFinishLaunching:(UIApplication *)application { |
// Create the window object and assign it to the |
// window instance variable of the application delegate. |
window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; |
window.backgroundColor = [UIColor whiteColor]; |
// Create a simple red square |
CGRect redFrame = CGRectMake(10, 10, 100, 100); |
UIView *redView = [[UIView alloc] initWithFrame:redFrame]; |
redView.backgroundColor = [UIColor redColor]; |
// Create a simple blue square |
CGRect blueFrame = CGRectMake(10, 150, 100, 100); |
UIView *blueView = [[UIView alloc] initWithFrame:blueFrame]; |
blueView.backgroundColor = [UIColor blueColor]; |
// Add the square views to the window |
[window addSubview:redView]; |
[window addSubview:blueView]; |
// Once added to the window, release the views to avoid the |
// extra retain count on each of them. |
[redView release]; |
[blueView release]; |
// Show the window. |
[window makeKeyAndVisible]; |
} |
重要提示:在内存管理方面,可以将子视图考虑为其它的集合对象。特别是当您通过addSubview:
方法将一个视图作为子视图插入时,父视图会对其进行保持操作。反过来,当您通过removeFromSuperview
方法将子视图从父视图移走时,子视图会被自动释放。在将视图加入视图层次之后释放该对象可以避免多余的保持操作,从而避免内存泄露。
有关Cocoa内存管理约定的更多信息,请参见Cocoa内存管理编程指南。
当您为某个视图添加子视图时,UIKit会向相应的父子视图发送几个消息,通知它们当前发生的状态变化。您可以在自己的定制视图中对诸如willMoveToSuperview:
、willMoveToWindow:
、willRemoveSubview:
、didAddSubview:
、didMoveToSuperview
、和didMoveToWindow
这样的方法进行重载,以便在事件发生的前后进行必要的处理,并根据发生的变化更新视图的状态信息。
在视图层次建立之后,您可以通过视图的superview
属性来取得其父视图,或者通过subviews
属性取得视图的子视图。您也可以通过isDescendantOfView:
方法来判定一个视图是否在其父视图的视图层中。一个视图层次的根视图没有父视图,因此其superview
属性被设置为nil
。对于当前被显示在屏幕上的视图,窗口对象通常是整个视图层次的根视图。
您可以通过视图的window
属性来取得指向其父窗口(如果有的话)的指针,如果视图还没有被链接到窗口上,则该属性会被设置为nil
。
视图层次中的坐标转换
很多时候,特别是处理事件的时候,应用程序可能需要将一个相对于某边框的坐标值转换为相对于另一个边框的值。例如,触摸事件通常使用基于窗口指标系统的坐标值来报告事件发生的位置,但是视图对象需要的是相对于视图本地坐标的位置信息,两者可能是不一样的。UIView
类定义了下面这些方法,用于在不同的视图本地坐标系统之间进行坐标转换:
convertPoint:fromView:
convertRect:fromView:
convertPoint:toView:
convertRect:toView:
convert...:fromView:
方法将指定视图的坐标值转换为视图本地坐标系统的坐标值;convert...:toView:
方法则将视图本地坐标系统的坐标值转换为指定视图坐标系统的坐标值。如果传入nil
作为视图引用参数的值,则上面这些方法会将视图所在窗口的坐标系统作为转换的源或目标坐标系统。
除了UIView
的转换方法之外,UIWindow
类也定义了几个转换方法。这些方法和UIView
的版本类似,只是UIView
定义的方法将视图本地坐标系统作为转换的源或目标坐标系统,而UIWindow
的版本则使用窗口坐标系统。
convertPoint:fromWindow:
convertRect:fromWindow:
convertPoint:toWindow:
convertRect:toWindow:
当参与转换的视图没有被旋转,或者被转换的对象仅仅是点的时候,坐标转换相当直接。如果是在旋转之后的视图之间转换矩形或尺寸数据,则其几何结构必须经过合理的改变,才能得到正确的结果坐标。在对矩形结构进行转换时,UIView
类假定您希望保证原来的屏幕区域被覆盖,因此转换后的矩形会被放大,其结果是使放大后的矩形(如果放在对应的视图中)可以完全覆盖原来的矩形区域。图2-11显示了将rotatedView
对象的坐标系统中的矩形转换到其超类(outerView
)坐标系统的结果。
图2-11 对旋转后视图中的值进行转换
对于尺寸信息,UIView
简单地将它处理为分别相对于源视图和目标视图(0.0, 0.0)点的偏移量。虽然偏移量保持不变,但是相对于坐标轴的差额会随着视图的旋转而移动。在转换尺寸数据时,UIKit总是返回正的数值。
标识视图
UIView
类中包含一个tag
属性。借助这个属性,您可以通过一个整数值来标识一个视图对象。您可以通过这个属性来唯一标识视图层次中的视图,以及在运行时进行视图的检索(基于tag标识的检索比您自行遍历视图层次要快)。tag
属性的缺省值为0
。
您可以通过UIView
的viewWithTag:
方法来检索标识过的视图。该方法从消息的接收者自身开始,通过深度优先的方法来检索接收者的子视图。
在运行时修改视图
应用程序在接收用户输入时,需要通过调整自己的用户界面来进行响应。应用程序可能重新排列界面上的视图、刷新屏幕上模型数据已被改变的 视图、或者装载一组全新的视图。在决定使用哪种技术时,要考虑您的用户界面,以及您希望实现什么。但是,如何初始化这些技术对于所有应用程序都是一样的。 本章的下面部分将描述这些技术,以及如何通过这些技术在运行时更新您的用户界面。
请注意:如果您需要了解UIKit如何在框架内部和您的定制代码之间转移事件和消息的背景信息,请在继续阅读本文之前查阅“视图交互模型”部分。
实现视图动画
动画为用户界面在不同状态之间的迁移过程提供流畅的视觉效果。在iPhone OS中,动画被广泛用于视图的位置调整、尺寸变化、甚至是alpha值的变化(以实现淡入淡出的效果)。动画支持对于制作易于使用的应用程序是至关重要的,因此,UIKit直接将它集成到UIView
类中,以简化动画的创建过程。
UIView
类定义了几个内在支持动画的属性声明—也就是说,当这些属性值发生变化时,视图为其变化过程提供内建的动画支持。虽然执行动画所需要的工作由UIView
类自动完成,但您仍然必须在希望执行动画时通知视图。为此,您需要将改变给定属性的代码包装在一个动画块中。
动画块从调用UIView
的beginAnimations:context:
类方法开始,而以调用commitAnimations
类方法作为结束。在这两个调用之间,您可以配置动画的参数和改变希望实行动画的属性值。一旦调用commitAnimations
方法,UIKit就会开始执行动画,即把给定属性从当前值到新值的变化过程用动画表现出来。动画块可以被嵌套,但是在最外层的动画块提交之前,被嵌套的动画不会被执行。
表2-2列举了UIView
类中支持动画的属性。
属性 |
描述 |
---|---|
frame
|
视图的边框矩形,位于父视图的坐标系中。 |
bounds
|
视图的边界矩形,位于视图的坐标系中。 |
center
|
边框的中心,位于父视图的坐标系中。 |
transform
|
视图上的转换矩阵,相对于视图边界的中心。 |
alpha
|
视图的alpha值,用于确定视图的透明度。 |
配置动画的参数
除了在动画块中改变属性值之外,您还可以对其它参数进行配置,以确定您希望得到的动画行为。为此,您可以调用下面这些UIView
的类方法:
- 用
setAnimationStartDate:
方法来设置动画在commitAnimations
方法返回之后的发生日期。缺省行为是使动画立即在动画线程中执行。 - 用
setAnimationDelay:
方法来设置实际发生动画和commitAnimations
方法返回的时间点之间的间隔。 - 用
setAnimationDuration:
方法来设置动画持续的秒数。 - 用
setAnimationCurve:
方法来设置动画过程的相对速度,比如动画可能在启示阶段逐渐加速,而在结束阶段逐渐减速,或者整个过程都保持相同的速度。 - 用
setAnimationRepeatCount:
方法来设置动画的重复次数。 - 用
setAnimationRepeatAutoreverses:
方法来指定动画在到达目标值时是否自动反向播放。您可以结合使用这个方法和setAnimationRepeatCount:
方法,使各个属性在初始值和目标值之间平滑切换一段时间。
commitAnimations
类方法在调用之后和动画开始之前立刻返回。UIKit在一个独立的、和应用程序的主事件循环分离的线程中执行动画。commitAnimations
方法将动画发送到该线程,然后动画就进入线程中的队列,直到被执行。缺省情况下,只有在当前正在运行的动画块执行完成后,Core Animation才会启动队列中的动画。但是,您可以通过向动画块中的setAnimationBeginsFromCurrentState:
类方法传入YES
来重载这个行为,使动画立即启动。这样做会停止当前正在执行的动画,而使新动画在当前状态下开始执行。
缺省情况下,所有支持动画的属性在动画块中发生的变化都会形成动画。如果您希望让动画块中发生的某些变化不产生动画效果,可以通过setAnimationsEnabled:
方法来暂时禁止动画,在完成修改后才重新激活动画。在调用setAnimationsEnabled:
方法并传入NO
值之后,所有的改变都不会产生动画效果,直到用YES
值再次调用这个方法或者提交整个动画块时,动画才会恢复。您可以用areAnimationsEnabled
方法来确定当前是否激活动画。
配置动画的委托
您可以为动画块分配一个委托,并通过该委托接收动画开始和结束的消息。当您需要在动画开始前和结束后立即执行其它任务时,可能就需要这样做。您可以通过UIView
的setAnimationDelegate:
类方法来设置委托,并通过setAnimationWillStartSelector:
和setAnimationDidStopSelector:
方法来指定接收消息的选择器方法。消息处理方法的形式如下:
- (void)animationWillStart:(NSString *)animationID context:(void *)context; |
- (void)animationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context; |
上面两个方法的animationID和context参数和动画块开始时传给beginAnimations:context:
方法的参数相同:
- animationID - 应用程序提供的字符串,用于标识一个动画块中的动画。
- context - 也是应用程序提供的对象,用于向委托对象传递额外的信息。
setAnimationDidStopSelector:
选择器方法还有一个参数—即一个布尔值。如果动画顺利完成,没有被其它动画取消或停止,则该值为YES
。
响应布局的变化
任何时候,当视图的布局发生改变时,UIKit会激活每个视图的自动尺寸调整行为,然后调用各自的layoutSubviews
方法,使您有机会进一步调整子视图的几何尺寸。下面列举的情形都会引起视图布局的变化:
- 视图边界矩形的尺寸发生变化。
- 滚动视图的内容偏移量—也就是可视内容区域的原点—发生变化。
- 和视图关联的转换矩阵发生变化。
- 和视图层相关联的Core Animation子层组发生变化。
- 您的应用程序调用视图的
setNeedsLayout
或layoutIfNeeded
方法来强制进行布局。 - 您的应用程序调用视图背后的层对象的
setNeedsLayout
方法来强制进行布局。
子视图的初始布局由视图的自动尺寸调整行为来负责。应用这些行为可以保证您的视图接近其设计的尺寸。有关自动尺寸调整行为如何影响视图的尺寸和位置的更多信息,请参见“自动尺寸调整行为”部分。
有些时候,您可能希望通过layoutSubviews
方法来手工调整子视图的布局,而不是完全依赖自动尺 寸调整行为。举例来说,如果您要实现一个由几个子视图元素组成的定制控件,则可以通过手工调整子视图来精确控制控件在一定尺寸范围内的外观。还有,如果一 个视图表示的滚动内容区域很大,可以选择将内容显示为一组平铺的子视图,在滚动过程中,可以回收离开屏幕边界的视图,并在填充新内容后将它重新定位,使它 成为下一个滚入屏幕的视图。
请注意:您也可以用layoutSubviews
方法来调整作为子层链接到视图层的定制CALayer
对象。您可以通过对隐藏在视图后面的层层次进行管理,实现直接基于Core Animation的高级动画。有关如何通过Core Animation管理层层次的更多信息,请参见Core Animation编程指南。
在编写布局代码时,请务必在应用程序支持的每个方向上都进行测试。对于同时支持景观方向和肖像方向的应用程序,必须确认其是否能正确处 理两个方向上的布局。类似地,您的应用程序应该做好处理其它系统变化的准备,比如状态条高度的变化,如果用户在使用您的应用程序的同时接听电话,然后再挂 断,就会发生这种变化。在挂断时,负责管理视图的视图控制器可能会调整视图的尺寸,以适应缩小的状态条。之后,这样的变化会向下渗透到应用程序的其它视图。
重画视图的内容
有些时候,应用程序数据模型的变化会影响到相应的用户界面。为了反映这些变化,您可以将相应的视图标识为需要刷新(通过调用setNeedsDisplay
或setNeedsDisplayInRect:
方法)。和简单创建一个图形上下文并进行描画相比,将视图标识为需要刷新的方法使系统有机会更有效地执行描画操作。举例来说,如果您在某个运行周期中将一个视图的几个区域标识为需要刷新,系统就会将这些需要刷新的区域进行合并,并最终形成一个drawRect:
方法的调用。结果,只需要创建一个图形上下文就可以描画所有这些受影响的区域。这个做法比连续快速创建几个图形上下文要有效得多。
实现drawRect:
方法的视图总是需要检查传入的矩形参数,并用它来限制描画操作的范围。因为描画是开销相对昂贵的操作,以这种方式来限制描画是提高性能的好方法。
缺省情况下,视图在几何上的变化并不自动导致重画。相反,大多数几何变化都由Core Animation来自动处理。具体来说,当您改变视图的frame
、bounds
、center
、或transform
属 性时,Core Animation会将相应的几何变化应用到与视图层相关联的缓存位图上。在很多情况下,这种方法是完全可以接受的,但是如果您发现结果不是您期望得到 的,则可以强制UIKit对视图进行重画。为了避免Core Animation自动处理几何变化,您可以将视图的contentMode
属性声明设置为UIViewContentModeRedraw
。更多有关内容模式的信息,请参见“内容模式和比例缩放”部分。
隐藏视图
您可以通过改变视图的hidden
属性声明来隐藏或显示视图。将这个属性设置为YES
会隐藏视图,设置为NO
则可以显示视图。对一个视图进行隐藏会同时隐藏其内嵌的所有子视图,就好象它们自己的hidden
属性也被设置一样。
当您隐藏一个视图时,该视图仍然会保留在视图层次中,但其内容不会被描画,也不会接收任何触摸事件。由于隐藏视图仍然存在于视图层次 中,所以会继续参与自动尺寸调整和其它布局操作。如果被隐藏的视图是当前的第一响应者,则该视图会自动放弃其自动响应者的状态,但目标为第一响应者的事件 仍然会传递给隐藏视图。有关响应者链的更多信息,请参见“响应者对象和响应者链”部分。
创建一个定制视图
UIView
类为在屏幕上显示内容及处理触摸事件提供了潜在的支持,但是除了在视图区域内描画带有alpha值的背景色之外,UIView
类的实例不做其它描画操作,包括其子视图的描画。如果您的应用程序需要显示定制的内容,或以特定的方式处理触摸事件,必须创建UIView
的定制子类。
本章的下面部分将描述一些定制视图对象可能需要实现的关键方法和行为。有关子类化的更多信息,请参见UIView类参考。
初始化您的定制视图
您定义的每个新的视图对象都应该包含initWithFrame:
初始化方法。该方法负责在创建对象时对类进行初始化,使之处于已知的状态。在通过代码创建您的视图实例时,需要使用这个方法。
程序清单2-2显示了标准的initWithFrame:
方法的一个框架实现。该实现首先调用继承自超类的实现,然后初始化类的实例变量和状态信息,最后返回初始化完成的对象。您通常需要首先执行超类的实现,以便在出现问题时可以简单地终止自己的初始化代码,返回nil
。
程序清单2-2 初始化一个视图的子类
- (id)initWithFrame:(CGRect)aRect { |
self = [super initWithFrame:aRect]; |
if (self) { |
// setup the initial properties of the view |
... |
} |
return self; |
} |
如果您从nib文件中装载定制视图类的实例,则需要知道:在iPhone OS中,装载nib的代码并不通过initWithFrame:
方法来实例化新的视图对象,而是通过NSCoding
协议定义的initWithCoder:
方法来进行。
即使您的视图采纳了NSCoding
协议,Interface Builder也不知道它的定制属性,因此不知道如何将那些属性编码到nib文件中。所以,当您从nib文件装载定制视图时,initWithCoder:
方法不具有进行正确初始化所需要的信息。为了解决这个问题,您可以在自己的类中实现awakeFromNib
方法,特别用于从nib文件装载的定制类。
描画您的视图内容
当您改变视图内容时,可以通过setNeedsDisplay
或setNeedsDisplayInRect:
方法来将需要重画的部分通知给系统。在应用程序返回运行循环之后,会对所有的描画请求进行合并,计算界面中需要被更新的部分;之后就开始遍历视图层次,向需要更新的视图发送drawRect:
消息。遍历的起点是视图层次的根视图,然后从后往前遍历其子视图。在可视边界内显示定制内容的视图必须实现其drawRect:
方法,以便对该内容进行渲染。
在调用视图的drawRect:
方法之前,UIKit会为其配置描画的环境,即创建一个图形上下文,并调整其坐标系统和裁剪区,使之和视图的坐标系统及边界相匹配。因此,在您的drawRect:
方法被调用时,您可以使用UIKit的类和函数、Quartz的函数、或者使用两者相结合的方法来直接进行描画。需要的话,您可以通过UIGraphicsGetCurrentContext
函数来取得当前图形上下文的指针,实现对它的访问。
重要提示:只有当定制视图的drawRect:
方法被调用的期间,当前图形上下文才是有效的。UIKit可能为该方法的每个调用创建不同的图形上下文,因此,您不应该对该对象进行缓存并在之后使用。
程序清单2-3显示了drawRect:
方法的一个简单实现,即在视图边界描画一个10像素宽的红色边界。由于UIKit描画操作的实现也是基于Quartz,所以您可以像下面这样混合使用不同的描画调用来得到期望的结果。
程序清单2-3 一个描画方法
- (void)drawRect:(CGRect)rect { |
CGContextRef context = UIGraphicsGetCurrentContext(); |
CGRect myFrame = self.bounds; |
CGContextSetLineWidth(context, 10); |
[[UIColor redColor] set]; |
UIRectFrame(myFrame); |
} |
如果您能确定自己的描画代码总是以不透明的内容覆盖整个视图的表面,则可以将视图的opaque
属性声明设置为YES
,以提高描画代码的总体效率。当您将视图标识为不透明时,UIKit会避免对该视图正下方的内容进行描画。这不仅减少了描画开销的时间,而且减少内容合成需要的工作。然而,只有当您能确定视图提供的内容为不透明时,才能将这个属性设置为YES
;如果您不能保证视图内容总是不透明,则应该将它设置为NO
。
提高描画性能(特别是在滚动过程)的另一个方法是将视图的clearsContextBeforeDrawing
属性设置为NO
。当这个属性被设置为YES
时,UIKIt会在调用drawRect:
方法之前,把即将被该方法更新的区域填充为透明的黑色。将这个属性设置为NO
可以取消相应的填充操作,而由应用程序负责完全重画传给drawRect:
方法的更新矩形中的部分。这样的优化在滚动过程中通常是一个好的折衷。
响应事件
UIView
类是UIResponder
的一个子类,因此能够接收用户和视图内容交互时产生的触摸事件。触摸事件从发生触摸的视图开始,沿着响应者链进行传递,直到最后被处理。视图本身就是响应者,是响应者链的参与者,因此可以收到所有关联子视图派发给它们的触摸事件。
处理触摸事件的视图通常需要实现下面的所有方法,更多细节请参见“事件处理”部分:
touchesBegan:withEvent:
touchesMoved:withEvent:
touchesEnded:withEvent:
touchesCancelled:withEvent:
请记住,在缺省情况下,视图每次只响应一个触摸动作。如果用户将第二个手指放在屏幕上,系统会忽略该触摸事件,而不会将它报告给视图对象。如果您希望在视图的事件处理器方法中跟踪多点触摸手势,则需要重新激活多点触摸事件,具体方法是将视图的multipleTouchEnabled
属性声明设置为YES
。
某些视图,比如标签和图像视图,在初始状态下完全禁止事件处理。您可以通过改变视图的userInteractionEnabled
属性值来控制视图是否可以对事件进行处理。当某个耗时很长的操作被挂起时,您可以暂时将这个属性设置为NO
,使用户无法对视图的内容进行操作。为了阻止事件到达您的视图,还可以使用UIApplication
对象的beginIgnoringInteractionEvents
和endIgnoringInteractionEvents
方法。这些方法影响的是整个应用程序的事件分发,而不仅仅是某个视图。
在处理触摸事件时,UIKit会通过UIView
的hitTest:withEvent:
和pointInside:withEvent:
方法来确定触摸事件是否发生在指定的视图上。虽然很少需要重载这些方法,但是您可以通过重载来使子视图无法处理触摸事件。
视图对象的清理
如果您的视图类分配了任何内存、存储了任何对象的引用、或者持有在释放视图时也需要被释放的资源,则必须实现其dealloc
方法。当您的视图对象的保持数为零、且视图本身即将被解除分配时,系统会调用其dealloc
方法。您在这个方法的实现中应该释放视图持有的对象和资源,然后调用超类的实现,如程序程序清单2-4所示。
程序清单2-4 实现dealloc
方法
- (void)dealloc { |
// Release a retained UIColor object |
[color release]; |
// Call the inherited implementation |
[super dealloc]; |
} |