窗口和视图
窗口和视图是为iPhone应用程序构造用户界面的可视组件。窗口为内容显示提供背景平台,而视图负责绝大部分的内容描画,并负责响应用户的交互。虽然本章讨论的概念和窗口及视图都相关联,但是讨论过程更加关注视图,因为视图对系统更为重要。
视图对iPhone应用程序是如此的重要,以至于在一个章节中讨论视图的所有方面是不可能的。本章将关注窗口和视图的基本属性、各个属性之间的关系、以及在应用程序中如何创建和操作这些属性。本章不讨论视图如何响应触摸事件或如何描画定制内容,有关那些主题的更多信息,请分别参见“事件处理”和“图形和描画”部分。
什么是窗口和视图?
和Mac OS X一样,iPhone OS通过窗口和视图在屏幕上展现图形内容。虽然窗口和视图对象之间在两个平台上有很多相似性,但是具体到每个平台上,它们的作用都有轻微的差别。
UIWindow的作用
和Mac OS X的应用程序有所不同,iPhone应用程序通常只有一个窗口,表示为一个UIWindow
类的实例。您的应用程序在启动时创建这个窗口(或者从nib文件进行装载),并往窗口中加入一或多个视图,然后将它显示出来。窗口显示出来之后,您很少需要再次引用它。
在iPhone OS中,窗口对象并没有像关闭框或标题栏这样的视觉装饰,用户不能直接对其进行关闭或其它操作。所有对窗口的操作都需要通过其编程接口来实现。应用程序可以借助窗口对象来进行事件传递。窗口对象会持续跟踪当前的第一响应者对象,并在UIApplication
对象提出请求时将事件传递它。
还有一件可能让有经验的Mac OS X开发者觉得奇怪的事是UIWindow
类的继承关系。在Mac OS X中,NSWindow
的父类是NSResponder
;而在iPhone OS中,UIWindow
的父类是UIView
。因此,窗口在iPhone OS中也是一个视图对象。不管其起源如何,您通常可以将iPhone OS上的窗口和Mac OS X的窗口同样对待。也就是说,您通常不必直接操作UIWindow
对象中与视图有关的属性变量。
在创建应用程序窗口时,您应该总是将其初始的边框尺寸设置为整个屏幕的大小。如果您的窗口是从nib文件装载得到,Interface Builder并不允许创建比屏幕尺寸小的窗口;然而,如果您的窗口是通过编程方式创建的,则必须在创建时传入期望的边框矩形。除了屏幕矩形之外,没有理由传入其它边框矩形。屏幕矩形可以通过UIScreen
对象来取得,具体代码如下所示:
UIWindow* aWindow = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease]; |
虽然iPhone OS支持将一个窗口叠放在其它窗口的上方,但是您的应用程序永远不应创建多个窗口。系统自身使用额外的窗口来显示系统状态条、重要的警告、以及位于应用程序窗口上方的其它消息。如果您希望在自己的内容上方显示警告,可以使用UIKit提供的警告视图,而不应创建额外的窗口。
UIView是作用
视图是UIView
类的实例,负责在屏幕上定义一个矩形区域。在iPhone的应用程序中,视图在展示用户界面及响应用户界面交互方面发挥关键作用。每个视图对象都要负责渲染视图矩形区域中的内容,并响应该区域中发生的触碰事件。这一双重行为意味着视图是应用程序与用户交互的重要机制。在一个基于模型-视图-控制器的应用程序中,视图对象明显属于视图部分。
除了显示内容和处理事件之外,视图还可以用于管理一或多个子视图。子视图是指嵌入到另一视图对象边框内部的视图对象,而被嵌入的视图则被称为父视图或超视图。视图的这种布局方式被称为视图层次,一个视图可以包含任意数量的子视图,通过为子视图添加子视图的方式,视图可以实现任意深度的嵌套。视图在视图层次中的组织方式决定了在屏幕上显示的内容,原因是子视图总是被显示在其父视图的上方;这个组织方法还决定了视图如何响应事件和变化。每个父视图都负责管理其直接的子视图,即根据需要调整它们的位置和尺寸,以及响应它们没有处理的事件。
由于视图对象是应用程序和用户交互的主要途径,所以需要在很多方面发挥作用,下面是其中的一小部分:
-
描画和动画
-
视图负责对其所属的矩形区域进行描画。
-
某些视图属性变量可以以动画的形式过渡到新的值。
-
-
布局和子视图管理
-
视图管理着一个子视图列表。
-
视图定义了自身相对于其父视图的尺寸调整行为。
-
必要时,视图可以通过代码调整其子视图的尺寸和位置。
-
视图可以将其坐标系统下的点转换为其它视图或窗口坐标系统下的点。
-
-
事件处理
-
视图可以接收触摸事件。
-
视图是响应者链的参与者。
-
在iPhone应用程序中,视图和视图控制器紧密协作,管理若干方面的视图行为。视图控制器的作用是处理视图的装载与卸载、处理由于设备旋转导致的界面旋转,以及和用于构建复杂用户界面的高级导航对象进行交互。更多这方面的信息请参见“视图控制器的作用”部分。
本章的大部分内容都着眼于解释视图的这些作用,以及说明如何将您自己的定制代码关联到现有的UIView
行为中。
UIKit的视图类
UIView
类定义了视图的基本行为,但并不定义其视觉表示。相反,UIKit通过其子类来为像文本框、按键、及工具条这样的标准界面元素定义具体的外观和行为。图2-1显示了所有UIKit视图类的层次框图。除了UIView
和UIControl
类是例外,这个框图中的大多数视图都设计为可直接使用,或者和委托对象结合使用。
这个视图层次可以分为如下几个大类:
-
容器
容器视图用于增强其它视图的功能,或者为视图内容提供额外的视觉分隔。比如,
UIScrollView
类可以用于显示因内容太大而无法显示在一个屏幕上的视图。UITableView
类是UIScrollView
类的子类,用于管理数据列表。表格的行可以支持选择,所以通常也用于层次数据的导航—比如用于挖掘一组有层次结构的对象。UIToolbar
对象则是一个特殊类型的容器,用于为一或多个类似于按键的项提供视觉分组。工具条通常出现在屏幕的底部。Safari、Mail、和Photos程序都使用工具条来显示一些按键,这些按键代表经常使用的命令。工具条可以一直显示,也可以根据应用程序的需要进行显示。 -
控件
控件用于创建大多数应用程序的用户界面。控件是一种特殊类型的视图,继承自
UIControl
超类,通常用于显示一个具体的值,并处理修改这个值所需要的所有用户交互。控件通常使用标准的系统范式(比如目标-动作模式和委托模式)来通知应用程序发生了用户交互。控件包括按键、文本框、滑块、和切换开关。 -
显示视图
控件和很多其它类型的视图都提供了交互行为,而另外一些视图则只是用于简单地显示信息。具有这种行为的UIKit类包括
UIImageView
、UILabel
、UIProgressView
、UIActivityIndicatorView
。 -
文本和web视图
文本和web视图为应用程序提供更为高级的显示多行文本的方法。
UITextView
类支持在滚动区域内显示和编辑多行文本;而UIWebView
类则提供了显示HTML内容的方法,通过这个类,您可以将图形和高级的文本格式选项集成到应用程序中,并以定制的方式对内容进行布局。 -
警告视图和动作表单
警告视图和动作表单用于即刻取得用户的注意。它们向用户显示一条消息,同时还有一或多个可选的按键,用户通过这些按键来响应消息。警告视图和动作表单的功能类似,但是外观和行为不同。举例来说,
UIAlertView
类在屏幕上弹出一个蓝色的警告框,而UIActionSheet
类则从屏幕的底部滑出动作框。 -
导航视图
页签条和导航条和视图控制器结合使用,为用户提供从一个屏幕到另一个屏幕的导航工具。在使用时,您通常不必直接创建
UITabBar
和UINavigationBar
的项,而是通过恰当的控制器接口或Interface Builder来对其进行配置。 -
窗口
窗口提供一个描画内容的表面,是所有其它视图的根容器。每个应用程序通常都只有一个窗口。更多信息请参见“UIWindow的作用”部分。
除了视图之外,UIKit还提供了视图控制器,用于管理这些对象。更多信息请参见“视图控制器的作用”部分。
视图控制器的作用
运行在iPhone OS上的应用程序在如何组织内容和如何将内容呈现给用户方面有很多选择。含有很多内容的应用程序可以将内容分为多个屏幕。在运行时,每个屏幕的背后都是一组视图对象,负责显示该屏幕的数据。一个屏幕的视图后面是一个视图控制器其作用是管理那些视图上显示的数据,并协调它们和应用程序其它部分的关系。
UIViewController
类负责创建其管理的视图及在低内存时将它们从内容中移出。视图控制器还为某些标准的系统行为提供自动响应。比如,在响应设备方向变化时,如果应用程序支持该方向,视图控制器可以对其管理的视图进行尺寸调整,使其适应新的方向。您也可以通过视图控制器来将新的视图以模式框的方式显示在当前视图的上方。
除了基础的UIViewController
类之外,UIKit还包含很多高级子类,用于处理平台共有的某些高级接口。特别需要提到的是,导航控制器用于显示多屏具有一定层次结构的内容;而页签条控制器则支持用户在一组不同的屏幕之间切换,每个屏幕都代表应用程序的一种不同的操作模式。
有关如何通过视图控制器管理用户界面上视图的更多信息,请参见iPhone OS的视图控制器编程指南。
视图架构和几何属性
由于视图是iPhone应用程序的焦点对象,所以对视图与系统其它部分的交互机制有所了解是很重要的。UIKit中的标准视图类为应用程序免费提供相当数量的行为,还提供了一些定义良好的集成点,您可以通过这些集成点来对标准行为进行定制,完成应用程序需要做的工作。
本文的下面部分将解释视图的标准行为,并说明哪些地方可以集成您的定制代码。如果需要特定类的集成点信息,请参见该类的参考文档。您可以从UIKit框架参考中取得所有类参考文档的列表。
视图交互模型
任何时候,当用户和您的程序界面进行交互、或者您的代码以编程的方式进行某些修改时,UIKit内部都会发生一个复杂的事件序列。在事件序列的一些特定的点上,UIKit会调用您的视图类,使它们有机会代表应用程序进行事件响应。理解这些调用点是很重要的,有助于理解您的视图对象和系统在哪里进行结合。图2-2显示了从用户触击屏幕到图形系统更新屏幕内容这一过程的基本事件序列。以编程方式触发事件的基本步骤与此相同,只是没有最初的用户交互。
下面的步骤说明进一步刨析了图2-2中的事件序列,解释了序列的每个阶段都发生了什么,以及应用程序可能如何进行响应。
-
用户触击屏幕。
-
硬件将触击事件报告给UIKit框架。
-
UIKit框架将触击信息封装为一个
UIEvent
对象,并派发给恰当的视图(有关UIKit如何将事件递送给您的视图的详细解释,请参见“事件的传递”部分)。 -
视图的事件处理方法可以通过下面的方式来响应事件:
当然,上述的哪些事情需要做及调用什么方法来完成是由视图来决定的。
-
如果视图被标识为需要重新布局,UIKit就调用视图的
layoutSubviews
方法。您可以在自己的定制视图中重载这个方法,以便调整子视图的尺寸和位置。举例来说,如果一个视图具有很大的滚动区域,就需要使用几个子视图来“平铺”,而不是创建一个内存很可能装不下的大视图。在这个方法的实现中,视图可以隐藏所有不需显示在屏幕上的子视图,或者在重新定位之后将它们用于显示新的内容。作为这个过程的一部分,视图也可以将用于“平铺”的子视图标识为需要重画。
-
如果视图的任何部分被标识为需要重画,UIKit就调用该视图的
drawRect:
方法。UIKit只对那些需要重画的视图调用这个方法。在这个方法的实现中,所有视图都应该尽可能快地重画指定的区域,且都应该只重画自己的内容,不应该描画子视图的内容。在这个调用点上,视图不应该尝试进一步改变其属性或布局。
-
所有更新过的视图都和其它可视内容进行合成,然后发送给图形硬件进行显示。
-
图形硬件将渲染完成的内容转移到屏幕。
请注意:上述的更新模型主要适用于采纳内置视图和描画技术的应用程序。如果您的应用程序使用OpenGL ES来描画内容,则通常要配置一个全屏的视图,然后直接在OpenGL的图形上下文中进行描画。您的视图仍然需要处理触碰事件,但不需要对子视图进行布局或者实现drawRect:
方法。有关OpenGL ES的更多信息,请参见“用OpenGL ES进行描画”部分。
基于上述的步骤说明可以看出,UIKit为您自己定制的视图提供如下主要的结合点:
-
下面这些事件处理方法:
-
layoutSubviews
方法 -
drawRect:
方法
大多数定制视图通过实现这些方法来得到自己期望的行为。您可能不需要重载所有方法,举例来说,如果您实现的视图是固定尺寸的,则可能不需要重载layoutSubviews
方法。类似地,如果您实现的视图只是显示简单的内容,比如文本或图像,则通常可以通过简单地嵌入UIImageView
和UILabel
对象作为子视图来避免描画。
重要的是要记住,这些是主要的结合点,但不是全部。UIView
类中有几个方法的设计目的就是让子类重载的。您可以通过查阅UIView类参考中的描述来了解哪些方法可以被重载。
视图渲染架构
虽然您通过视图来表示屏幕上的内容,但是UIView
类自身的很多基础行为却严重依赖于另一个对象。UIKit中每个视图对象的背后都有一个Core Animation层对象,它是一个CALayer
类的实例,该类为视图内容的布局和渲染、以及合成和动画提供基础性的支持。
和Mac OS X(在这个平台上Core Animation支持是可选的)不同的是,iPhone OS将Core Animation集成到视图渲染实现的核心。虽然Core Animation发挥核心作用,但是UIKit在Core Animation上面提供一个透明的接口层,使编程体验更为流畅。这个透明的接口使开发者在大多数情况下不必直接访问Core Animation的层,而是通过UIView
的方法和属性声明取得类似的行为。然而,当UIView
类没有提供您需要的接口时,Core Animation就变得重要了,在那种情况下,您可以深入到Core Animation层,在应用程序中实现一些复杂的渲染。
本文的下面部分将介绍Core Animation技术,描述它通过UIView
类为您提供的一些功能。有关如何使用Core Animation进行高级渲染的更多信息,请参见Core Animation编程指南。
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
对象中支持动画的属性有如下几个:
虽然其它的视图属性不直接支持动画,但是您可以为其中的一部分显式创建动画。显式动画要求您做很多管理动画和渲染内容的工作,通过使用Core Animation提供的基础设施,这些工作仍然可以得到良好的性能。
有关如何通过UIView
类创建动画的更多信息,请参见“实现视图动画”部分;有关如何创建显式动画的更多信息,则请参见Core Animation编程指南。
视图坐标系统
UIKit中的坐标是基于这样的坐标系统:以左上角为坐标的原点,原点向下和向右为坐标轴正向。坐标值由浮点数来表示,内容的布局和定位因此具有更高的精度,还可以支持与分辨率无关的特性。图2-3显示了这个相对于屏幕的坐标系统,这个坐标系统同时也用于UIWindow
和UIView
类。视图坐标系统的方向和Quartz及Mac OS X使用的缺省方向不同,选择这个特殊的方向是为了使布局用户界面上的控件及内容更加容易。
您在编写界面代码时,需要知道当前起作用的坐标系统。每个窗口和视图对象都维护一个自己本地的坐标系统。视图中发生的所有描画都是相对于视图本地的坐标系统。但是,每个视图的边框矩形都是通过其父视图的坐标系统来指定,而事件对象携带的坐标信息则是相对于应用程序窗口的坐标系统。为了方便,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)。结果,视图的内容就相对于父视图的原点向下向右移动相应的尺寸。
如果没有经过变换,视图的位置和大小就由上述三个互相关联的属性决定的。当您在代码中通过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)。结果,显示出来的是视图图像的不同部分。但是,由于边框矩形并没有改变,新的内容在父视图中的位置和之前是一样的。
请注意:缺省情况下,视图的边框并不会被父视图的边框裁剪。如果您希望让一个视图裁剪其子视图,需要将其clipsToBounds
属性设置为YES
。
坐标系统变换
在视图的drawRect:
方法中常常借助坐标系统变换来进行描画。而在iPhone OS系统中,您还可以用它来实现视图的某些视觉效果。举例来说,UIView
类中包含一个transform
属性声明,您可以通过它来对整个视图实行各种类型的平移、比例缩放、和变焦缩放效果。缺省情况下,这个属性的值是一个恒等变换,不会改变视图的外观。在加入变换之前,首先要得到该属性中存储的CGAffineTransform
结构,用相应的Core Graphics函数实行变换,然后再将修改后的变换结构重新赋值给视图的transform
属性。
平移一个视图会使其所有的子视图和视图本身的内容一起移动。由于子视图的坐标系统是继承并建立在这些变化的基础上的,所以比例缩放也会影响子视图的描画。有关如何控制视图内容缩放的更多信息,请参见“内容模式和比例缩放”部分。
有关如何在drawRect:
方法中使用变换的信息,请参见“坐标和坐标变换”部分;有关用于修改CGAffineTransform
结构的函数,则请参见CGAffineTransform参考。
内容模式与比例缩放
当您改变视图的边界,或者将一个比例因子应用到视图的transform
属性声明时,边框矩形会发生等量的变化。根据内容模式的不同,视图的内容也可能被缩放或重新定位,以反映上述的变化。视图的contentMode
属性决定了边界变化和缩放操作作用到视图上产生的效果。缺省情况下,这个属性的值被设置为UIViewContentModeScaleToFill
,意味着视图内容总是被缩放,以适应新的边框尺寸。作为例子,图2-6显示了当视图的水平缩放因子放大一倍时产生的效果。
视图内容的缩放仅在首次显示视图的时候发生,渲染后的内容会被缓存在视图下面的层上。当边界或缩放因子发生变化时,UIKit并不强制视图进行重画,而是根据其内容模式决定如何显示缓存的内容。图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
属性。当同一个轴向有多个部分被设置为可变时,尺寸调整的裕量会被平均分配到各个部分上。
自动尺寸调整掩码 | 描述 |
---|---|
这个常量如果被设置,视图将不进行自动尺寸调整。 | |
这个常量如果被设置,视图的高度将和父视图的高度一起成比例变化。否则,视图的高度将保持不变。 | |
这个常量如果被设置,视图的宽度将和父视图的宽度一起成比例变化。否则,视图的宽度将保持不变。 | |
这个常量如果被设置,视图的左边界将随着父视图宽度的变化而按比例进行调整。否则,视图和其父视图的左边界的相对位置将保持不变。 | |
这个常量如果被设置,视图的右边界将随着父视图宽度的变化而按比例进行调整。否则,视图和其父视图的右边界的相对位置将保持不变。 | |
这个常量如果被设置,视图的底边界将随着父视图高度的变化而按比例进行调整。否则,视图和其父视图的底边界的相对位置将保持不变。 | |
这个常量如果被设置,视图的上边界将随着父视图高度的变化而按比例进行调整。否则,视图和其父视图的上边界的相对位置将保持不变。 |
图2-8为这些常量值的位置提供了一个图形表示。如果这些常量之一被省略,则视图在相应方向上的布局就被固定;如果某个常量被包含在掩码中,在该方向的视图布局就就灵活的。
如果您通过Interface Builder配置视图,则可以用Size查看器的Autosizing控制来设置每个视图的自动尺寸调整行为。上图中的灵活宽度及高度常量和Interface Builder中位于同样位置的弹簧具有同样的行为,但是空白常量的行为则是正好相反。换句话说,如果要将灵活右空白的自动尺寸调整行为应用到Interface Builder的某个视图,必须使相应方向空间的Autosizing控制为空,而不是放置一个支柱。幸运的是,Interface Builder通过动画显示了您的修改对视图自动尺寸调整行为的影响。
如果视图的autoresizesSubviews
属性被设置为NO
,则该视图的直接子视图的所有自动尺寸调整行为将被忽略。类似地,如果一个子视图的自动尺寸调整掩码被设置为UIViewAutoresizingNone
,则该子视图的尺寸将不会被调整,因而其直接子视图的尺寸也不会被调整。
自动尺寸调整行为可以适合一些布局的要求,但是如果您希望更多地控制视图的布局,可以在适当的视图类中重载layoutSubviews
方法。有关视图布局管理的更多信息,请参见“响应布局的变化”部分。
创建和管理视图层次
管理用户界面的视图层次是开发应用程序用户界面的关键部分。视图的组织方式不仅定义了应用程序的视觉外观,而且还定义了应用程序如何响应变化。视图层次中的父-子关系可以帮助我们定义应用程序中负责处理触摸事件的对象链。当用户旋转设备时,父-子关系也有助于定义每个视图的尺寸和位置是如何随着界面方向的变化而变化的。
图2-9显示了一个简单的例子,说明如何通过视图的分层来创建期望的视觉效果。在Clock程序中,页签条和导航条视图,以及定制视图混合在一起,实现了整个界面。
如果您探究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
类定义了下面这些方法,用于在不同的视图本地坐标系统之间进行坐标转换:
convert...:fromView:
方法将指定视图的坐标值转换为视图本地坐标系统的坐标值;convert...:toView:
方法则将视图本地坐标系统的坐标值转换为指定视图坐标系统的坐标值。如果传入nil
作为视图引用参数的值,则上面这些方法会将视图所在窗口的坐标系统作为转换的源或目标坐标系统。
除了UIView
的转换方法之外,UIWindow
类也定义了几个转换方法。这些方法和UIView
的版本类似,只是UIView
定义的方法将视图本地坐标系统作为转换的源或目标坐标系统,而UIWindow
的版本则使用窗口坐标系统。
当参与转换的视图没有被旋转,或者被转换的对象仅仅是点的时候,坐标转换相当直接。如果是在旋转之后的视图之间转换矩形或尺寸数据,则其几何结构必须经过合理的改变,才能得到正确的结果坐标。在对矩形结构进行转换时,UIView
类假定您希望保证原来的屏幕区域被覆盖,因此转换后的矩形会被放大,其结果是使放大后的矩形(如果放在对应的视图中)可以完全覆盖原来的矩形区域。图2-11显示了将rotatedView
对象的坐标系统中的矩形转换到其超类(outerView
)坐标系统的结果。
对于尺寸信息,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
类中支持动画的属性。
属性 | 描述 |
---|---|
| 视图的边框矩形,位于父视图的坐标系中。 |
| 视图的边界矩形,位于视图的坐标系中。 |
| 边框的中心,位于父视图的坐标系中。 |
| 视图上的转换矩阵,相对于视图边界的中心。 |
| 视图的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
函数来取得当前图形上下文的指针,实现对它的访问。
程序清单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
的一个子类,因此能够接收用户和视图内容交互时产生的触摸事件。触摸事件从发生触摸的视图开始,沿着响应者链进行传递,直到最后被处理。视图本身就是响应者,是响应者链的参与者,因此可以收到所有关联子视图派发给它们的触摸事件。
处理触摸事件的视图通常需要实现下面的所有方法,更多细节请参见“事件处理”部分:
请记住,在缺省情况下,视图每次只响应一个触摸动作。如果用户将第二个手指放在屏幕上,系统会忽略该触摸事件,而不会将它报告给视图对象。如果您希望在视图的事件处理器方法中跟踪多点触摸手势,则需要重新激活多点触摸事件,具体方法是将视图的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]; |
} |
最后更新日期: 2009-06-17
![](http://www.apple.com.cn/developer/iphone/library/Resources/59/Images/QuickLook/closebox.png)