iPhone应用程序编程指南(窗口和视图)

窗口和视图

窗口为内容显示提供背景平台,而视图负责绝大部分的内容描画,并负责响应用户的交互。

什么是窗口和视图?

UIWindow的作用

iPhone应用程序通常只有一个窗口,表示为一个UIWindow类的实例。您的应用程序在启动时创建这个窗口(或者从nib文件进行装载),并往窗口中加入一或多个视图,然后将它显示出来。窗口显示出来之后,您很少需要再次引用它。

iPhone应用程序通常只有一个窗口,表示为一个UIWindow类的实例。您的应用程序在启动时创建这个窗口(或者从nib文件进行装载),并往窗口中加入一或多个视图,然后将它显示出来。窗口显示出来之后,您很少需要再次引用它。

在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的作用

在iPhone的应用程序中,视图在展示用户界面及响应用户界面交互方面发挥关键作用。每个视图对象都要负责渲染视图矩形区域中的内容,并响应该区域中发生的触碰事件。这一双重行为意味着视图是应用程序与用户交互的重要机制。

子视图是指嵌入到另一视图对象边框内部的视图对象,而被嵌入的视图则被称为父视图或超视图。视图的这种布局方式被称为视图层次

每个父视图都负责管理其直接的子视图,即根据需要调整它们的位置和尺寸,以及响应它们没有处理的事件。

视图控制器的作用是处理视图的装载与卸载、处理由于设备旋转导致的界面旋转,以及和用于构建复杂用户界面的高级导航对象进行交互。


UIKit的视图类

图2-1显示了所有UIKit视图类的层次框图。除了UIViewUIControl类是例外,这个框图中的大多数视图都设计为可直接使用,或者和委托对象结合使用。

图2-1  视图的类层次

View class hierarchy
 

视图控制器的作用

运行在iPhone OS上的应用程序在如何组织内容和如何将内容呈现给用户方面有很多选择。含有很多内容的应用程序可以将内容分为多个屏幕。在运行时,每个屏幕的背后都是一组视图对象,负责显示该屏幕的数据。一个屏幕的视图后面是一个视图控制器其作用是管理那些视图上显示的数据,并协调它们和应用程序其它部分的关系。

UIViewController类负责创建其管理的视图及在低内存时将它们从内容中移出。视图控制器还为某些标准的系统行为提供自动响应。比如,在响应设备方向变化时,如果应用程序支持该方向,视图控制器可以对其管理的视图进行尺寸调整,使其适应新的方向。您也可以通过视图控制器来将新的视图以模式框的方式显示在当前视图的上方。

 

视图架构和几何属性

视图交互模型

2-2显示了从用户触击屏幕到图形系统更新屏幕内容这一过程的基本事件序列。以编程方式触发事件的基本步骤与此相同,只是没有最初的用户交互。

图2-2  UIKit和您的视图对象之间的交互

UIKit interactions with your view objects

下面的步骤说明进一步刨析了图2-2中的事件序列,解释了序列的每个阶段都发生了什么,以及应用程序可能如何进行响应。

  1. 用户触击屏幕。

  2. 硬件将触击事件报告给UIKit框架。

  3. UIKit框架将触击信息封装为一个UIEvent对象,并派发给恰当的视图(有关UIKit如何将事件递送给您的视图的详细解释,请参见“事件的传递”部分)。

  4. 视图的事件处理方法可以通过下面的方式来响应事件:

    • 调整视图或其子视图的属性变量(边框、边界、透明度等)。

    • 将视图(或其子视图)标识为需要修改布局。

    • 将视图(或其子视图)标识为布局需要重画。

    • 将数据发生的变化通报给控制器

    当然,上述的哪些事情需要做及调用什么方法来完成是由视图来决定的。

  5. 如果视图被标识为需要重新布局,UIKit就调用视图的layoutSubviews方法。

    您可以在自己的定制视图中重载这个方法,以便调整子视图的尺寸和位置。举例来说,如果一个视图具有很大的滚动区域,就需要使用几个子视图来“平铺”,而不是创建一个内存很可能装不下的大视图。在这个方法的实现中,视图可以隐藏所有不需显示在屏幕上的子视图,或者在重新定位之后将它们用于显示新的内容。作为这个过程的一部分,视图也可以将用于“平铺”的子视图标识为需要重画。

  6. 如果视图的任何部分被标识为需要重画,UIKit就调用该视图的drawRect:方法。

    UIKit只对那些需要重画的视图调用这个方法。在这个方法的实现中,所有视图都应该尽可能快地重画指定的区域,且都应该只重画自己的内容,不应该描画子视图的内容。在这个调用点上,视图不应该尝试进一步改变其属性或布局。

  7. 所有更新过的视图都和其它可视内容进行合成,然后发送给图形硬件进行显示。

  8. 图形硬件将渲染完成的内容转移到屏幕。

请注意:上述的更新模型主要适用于采纳内置视图和描画技术的应用程序。

基于上述的步骤说明可以看出,UIKit为您自己定制的视图提供如下主要的结合点:

  1. 下面这些事件处理方法:

  2. layoutSubviews方法

  3. drawRect:方法

大多数定制视图通过实现这些方法来得到自己期望的行为。

 

视图渲染架构

UIKit中每个视图对象的背后都有一个Core Animation层对象,它是一个CALayer类的实例,该类为视图内容的布局和渲染、以及合成和动画提供基础性的支持。

iPhone OS将Core Animation集成到视图渲染实现的核心。虽然Core Animation发挥核心作用,但是UIKit在Core Animation上面提供一个透明的接口层,使编程体验更为流畅。这个透明的接口使开发者在大多数情况下不必直接访问Core Animation的层,而是通过UIView的方法和属性声明取得类似的行为。然而,当UIView类没有提供您需要的接口时,Core Animation就变得重要了,在那种情况下,您可以深入到Core Animation层,在应用程序中实现一些复杂的渲染。

Core Animation基础

Core Animation利用了硬件加速和架构上的优化来实现快速渲染和实时动画。当视图的drawRect:方法首次被调用时,层会将描画的结果捕捉到一个位图中,并在随后的重画中尽可能使用这个缓存的位图,以避免调用开销很大的drawRect:方法。这个过程使Core Animation得以优化合成操作,取得期望的性能。

Core Animation把和视图对象相关联的层存储在一个被称为层树的层次结构中。和视图一样,层树中的每个层都只有一个父亲,但可以嵌入任意数量的子层。缺省情况下,层树中对象的组织方式和视图在视图层次中的组织方式完全一样。但是,您可以在层树中添加层,而不同时添加相应的视图。当您希望实现某种特殊的视觉效果、而又不需要在视图上保持这种效果时,就可能需要这种技术。

实际上,层对象是iPhone OS渲染和布局系统的推动力,大多数视图属性实际上是其层对象属性的一个很薄的封装。当您(直接使用CALayer对象)修改层树上层对象的属性时,您所做的改变会立即反映在层对象上。但是,如果该变化触发了相应的动画,则可能不会立即反映在屏幕上,而是必须随着时间的变化以动画的形式表现在屏幕上。为了管理这种类型的动画,Core Animation额外维护两组层对象,我们称之为表示树渲染树

表示树反映的是层在展示给用户时的当前状态。假定您对层值的变化实行动画,则在动画开始时,表示层反映的是老的值;随着动画的进行,Core Animation会根据动画的当前帧来更新表示树层的值;然后,渲染树就和表示树一起,将变化渲染在屏幕上。由于渲染树运行在单独的进程或线程上,所以它所做的工作并不影响应用程序的主运行循环。虽然层树和表示树都是公开的,但是渲染树的接口是私有。

在视图后面设置层对象对描画代码的性能有很多重要的影响。使用层的好处在于视图的大多数几何变化都不需要重画。举例来说,改变视图的位置和尺寸并需要重画视图的内容,只需简单地重用层缓存的位图就可以了。对缓存的内容实行动画比每次都重画内容要有效得多。

使用层的缺点在于层是额外的缓存数据,会增加应用程序的内存压力。如果您的应用程序创建太多的视图,或者创建多个很大的视图,则可能很快就会出现内存不够用的情形。您不用担心在应用程序中使用视图,但是,如果有现成的视图可以重用,就不要创建新的视图对象。换句话说,您应该设法使内存中同时存在的视图对象数量最小。

改变视图的层

在iPhone OS系统中,由于视图必须有一个与之关联的层对象,所以UIView类在初始化时会自动创建相应的层。您可以通过视图的layer属性访问这个层,但是不能在视图创建完成后改变层对象。

如果您希望视图使用不同类型的层,必须重载其layerClass类方法,并在该方法中返回您希望使用的层对象。使用不同层类的最常见理由是为了实现一个基于OpenGL的应用程序。为了使用OpenGL描画命令,视图下面的层必须是CAEAGLLayer类的实例,这种类型的层可以和OpenGL渲染调用进行交互,最终在屏幕上显示期望的内容。

重要提示:您永远不应修改视图层的delegate属性,该属性用于存储一个指向视图的指针,应该被认为是私有的。类似地,由于一个视图只能作为一个层的委托,所以您必须避免将它作为其它层对象的委托,否则会导致应用程序崩溃。

 

动画支持

iPhone OS的每个视图后面都有一个层对象,这样做的好处之一是使视图内容更加易于实现动画。

UIView类的很多属性都被设计为可动画的(animatable)。可动画的属性是指当属性从一个值变为另一个值的时候,可以半自动地支持动画。您仍然必须告诉UIKit希望执行什么类型的动画,但是动画一旦开始,Core Animation就会全权负责。UIView对象中支持动画的属性有如下几个:

虽然其它的视图属性不直接支持动画,但是您可以为其中的一部分显式创建动画。显式动画要求您做很多管理动画和渲染内容的工作,通过使用Core Animation提供的基础设施,这些工作仍然可以得到良好的性能。

视图坐标系统

UIKit中的坐标是基于这样的坐标系统:以左上角为坐标的原点,原点向下和向右为坐标轴正向。坐标值由浮点数来表示,内容的布局和定位因此具有更高的精度,还可以支持与分辨率无关的特性。

图2-3  视图坐标系统

View coordinate system

您在编写界面代码时,需要知道当前起作用的坐标系统。每个窗口和视图对象都维护一个自己本地的坐标系统。视图中发生的所有描画都是相对于视图本地的坐标系统。但是,每个视图的边框矩形都是通过其父视图的坐标系统来指定,而事件对象携带的坐标信息则是相对于应用程序窗口的坐标系统。

边框、边界、和中心的关系

视图对象通过framebounds、和center属性声明来跟踪自己的大小和位置。frame属性包含一个矩形,即边框矩形,用于指定视图相对于其父视图坐标系统的位置和大小。bounds属性也包含一个矩形,即边界矩形,负责定义视图相对于本地坐标系统的位置和大小。虽然边界矩形的原点通常被设置为 (0, 0),但这并不是必须的。center属性包含边框矩形的中心点

在代码中,您可以将framebounds、和center属性用于不同的目的。边界矩形代表视图本地的坐标系统,因此,在描画和事件处理代码中,经常借助它来取得视图中发生事件或需要更新的位置。中心点代表视图的中心,改变中心点一直是移动视图位置的最好方法。边框矩形是一个通过boundscenter属性计算得到的便利值,只有当视图的变换属性被设置恒等变换时,边框矩形才是有效的。

图2-4显示了边框矩形和边界矩形之间的关系。右边的整个图像是从视图的(0, 0)开始描画的,但是由于边界的大小和整个图像的尺寸不相匹配,所以位于边界矩形之外的图像部分被自动裁剪。在视图和它的父视图进行合成的时候,视图在其父视图中的位置是由视图边框矩形的原点决定的。在这个例子中,该原点是(5, 5)。结果,视图的内容就相对于父视图的原点向下向右移动相应的尺寸。

图2-4  视图的边框和边界之间的关系

Relationship between a view's frame and bounds

如果没有经过变换,视图的位置和大小就由上述三个互相关联的属性决定的。当您在代码中通过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  改变视图的边界

Altering a view's bounds

请注意:缺省情况下,视图的边框并不会被父视图的边框裁剪。如果您希望让一个视图裁剪其子视图,需要将其clipsToBounds属性设置为YES

坐标系统变换

在视图的drawRect:方法中常常借助坐标系统变换来进行描画。而在iPhone OS系统中,您还可以用它来实现视图的某些视觉效果。举例来说,UIView类中包含一个transform属性声明,您可以通过它来对整个视图实行各种类型的平移、比例缩放、和变焦缩放效果。缺省情况下,这个属性的值是一个恒等变换,不会改变视图的外观。在加入变换之前,首先要得到该属性中存储的CGAffineTransform结构,用相应的Core Graphics函数实行变换,然后再将修改后的变换结构重新赋值给视图的transform属性。

请注意:当您将变换应用到视图时,所有执行的变换都是相对于视图的中心点。

平移一个视图会使其所有的子视图和视图本身的内容一起移动。由于子视图的坐标系统是继承并建立在这些变化的基础上的,所以比例缩放也会影响子视图的描画。有关如何控制视图内容缩放的更多信息,请参见“内容模式和比例缩放”部分。

重要提示:如果transform属性的值不是恒等变换,则frame属性的值就是未定义的,必须被忽略。在设置变换属性之后,请使用boundscenter属性来获取视图的位置和大小。

 

有关如何在drawRect:方法中使用变换的信息,请参见“坐标和坐标变换”部分;有关用于修改CGAffineTransform结构的函数,则请参见CGAffineTransform参考

内容模式与比例缩放

当您改变视图的边界,或者将一个比例因子应用到视图的transform属性声明时,边框矩形会发生等量的变化。根据内容模式的不同,视图的内容也可能被缩放或重新定位,以反映上述的变化。视图的contentMode属性决定了边界变化和缩放操作作用到视图上产生的效果。缺省情况下,这个属性的值被设置为UIViewContentModeScaleToFill,意味着视图内容总是被缩放,以适应新的边框尺寸。作为例子,图2-6显示了当视图的水平缩放因子放大一倍时产生的效果。

图2-6 使用scale-to-fill内容模式缩放视图

View scaled using the scale-to-fill content mode

视图内容的缩放仅在首次显示视图的时候发生,渲染后的内容会被缓存在视图下面的层上。当边界或缩放因子发生变化时,UIKit并不强制视图进行重画,而是根据其内容模式决定如何显示缓存的内容。图2-7比较了在不同的内容模式下,改变视图边界或应用不同的比例缩放因子时产生的结果。

图2-7  内容模式比较

Content mode comparisons

对视图应用一个比例缩放因子总是会使其内容发生缩放,而边界的改变在某些内容模式下则不会发生同样的结果。不同的UIViewContentMode常量(比如UIViewContentModeTopUIViewContentModeBottomRight)可以使当前的内容在视图的不同角落或沿着视图的不同边界显示,还有一种模式可以将内容显示在视图的中心。在这些模式的作用下,改变边界矩形只会简单地将现有的视图内容移动到新的边界矩形中对应的位置上。

当您希望在应用程序中实现尺寸可调整的控件时,请务必考虑使用内容模式。这样做可以避免控件的外观发生变形,以及避免编写定制的描画代码。按键和分段控件(segmented control)特别适合基于内容模式的描画。它们通常使用几个图像来创建控件外观。除了有两个固定尺寸的盖帽图像之外,按键可以通过一个可伸展的、宽度只有一个像素的中心图像来实现水平方向的尺寸调整。它将每个图像显示在自己的图像视图中,而将可伸展的中间图像的内容模式设置为UIViewContentModeScaleToFill,使得在尺寸调整时两端的外观不会变形。更为重要的是,每个图像视图的关联图像都可以由Core Animation来缓存,因此不需要编写描画代码就可以支持动画,从而使大大提高了性能。

内容模式通常有助于避免视图内容的描画,但是当您希望对缩放和尺寸调整过程中的视图外观进行特别的控制时,也可以使用UIViewContentModeRedraw模式。将视图的内容模式设置为这个值可以强制Core Animation使视图的内容失效,并调用视图的drawRect:方法,而不是自动进行缩放或尺寸调整。

自动尺寸调整行为

当您改变视图的边框矩形时,其内嵌子视图的位置和尺寸往往也需要改变,以适应原始视图的新尺寸。如果视图的autoresizesSubviews属性声明被设置为YES,则其子视图会根据autoresizingMask属性的值自动进行尺寸调整。简单配置一下视图的自动尺寸调整掩码常常就能使应用程序得到合适的行为;否则,应用程序就必须通过重载layoutSubviews方法来提供自己的实现。

设置视图的自动尺寸调整行为的方法是通过位OR操作符将期望的自动尺寸调整常量连结起来,并将结果赋值给视图的autoresizingMask属性。表2-1列举了自动尺寸调整常量,并描述这些常量如何影响给定视图的尺寸和位置。举例来说,如果要使一个视图和其父视图左下角的相对位置保持不变,可以加入UIViewAutoresizingFlexibleRightMarginUIViewAutoresizingFlexibleTopMargin常量,并将结果赋值给autoresizingMask属性。当同一个轴向有多个部分被设置为可变时,尺寸调整的裕量会被平均分配到各个部分上。

表2-1  自动尺寸调整掩码常量

自动尺寸调整掩码

描述

UIViewAutoresizingNone

这个常量如果被设置,视图将不进行自动尺寸调整。

UIViewAutoresizingFlexibleHeight

这个常量如果被设置,视图的高度将和父视图的高度一起成比例变化。否则,视图的高度将保持不变。

UIViewAutoresizingFlexibleWidth

这个常量如果被设置,视图的宽度将和父视图的宽度一起成比例变化。否则,视图的宽度将保持不变。

UIViewAutoresizingFlexibleLeftMargin

这个常量如果被设置,视图的左边界将随着父视图宽度的变化而按比例进行调整。否则,视图和其父视图的左边界的相对位置将保持不变。

UIViewAutoresizingFlexibleRightMargin

这个常量如果被设置,视图的右边界将随着父视图宽度的变化而按比例进行调整。否则,视图和其父视图的右边界的相对位置将保持不变。

UIViewAutoresizingFlexibleBottomMargin

这个常量如果被设置,视图的底边界将随着父视图高度的变化而按比例进行调整。否则,视图和其父视图的底边界的相对位置将保持不变。

UIViewAutoresizingFlexibleTopMargin

这个常量如果被设置,视图的上边界将随着父视图高度的变化而按比例进行调整。否则,视图和其父视图的上边界的相对位置将保持不变。

图2-8为这些常量值的位置提供了一个图形表示。如果这些常量之一被省略,则视图在相应方向上的布局就被固定;如果某个常量被包含在掩码中,在该方向的视图布局就就灵活的。

图2-8  视图的自动尺寸调整掩码常量

View autoresizing mask constants

如果视图的autoresizesSubviews属性被设置为NO,则该视图的直接子视图的所有自动尺寸调整行为将被忽略。类似地,如果一个子视图的自动尺寸调整掩码被设置为UIViewAutoresizingNone,则该子视图的尺寸将不会被调整,因而其直接子视图的尺寸也不会被调整。

请注意:为了使自动尺寸调整的行为正确,视图的transform属性必须设置为恒等变换;其它变换下的尺寸自动调整行为是未定义的。

自动尺寸调整行为可以适合一些布局的要求,但是如果您希望更多地控制视图的布局,可以在适当的视图类中重载layoutSubviews方法。

 

创建和管理视图层次

重要提示:在内存管理方面,可以将子视图考虑为其它的集合对象。特别是当您通过addSubview:方法将一个视图作为子视图插入时,父视图会对其进行保持操作。反过来,当您通过removeFromSuperview方法将子视图从父视图移走时,子视图会被自动释放。在将视图加入视图层次之后释放该对象可以避免多余的保持操作,从而避免内存泄露。

当您为某个视图添加子视图时,UIKit会向相应的父子视图发送几个消息,通知它们当前发生的状态变化。您可以在自己的定制视图中对诸如willMoveToSuperview:willMoveToWindow:willRemoveSubview:didAddSubview:didMoveToSuperview、和didMoveToWindow这样的方法进行重载,以便在事件发生的前后进行必要的处理,并根据发生的变化更新视图的状态信息。

对于当前被显示在屏幕上的视图,窗口对象通常是整个视图层次的根视图。

您可以通过视图的window属性来取得指向其父窗口(如果有的话)的指针,如果视图还没有被链接到窗口上,则该属性会被设置为nil

视图层次中的坐标转换

很多时候,特别是处理事件的时候,应用程序可能需要将一个相对于某边框的坐标值转换为相对于另一个边框的值。例如,触摸事件通常使用基于窗口指标系统的坐标值来报告事件发生的位置,但是视图对象需要的是相对于视图本地坐标的位置信息,两者可能是不一样的。UIView类定义了下面这些方法,用于在不同的视图本地坐标系统之间进行坐标转换:

convert...:fromView:方法将指定视图的坐标值转换为视图本地坐标系统的坐标值;convert...:toView:方法则将视图本地坐标系统的坐标值转换为指定视图坐标系统的坐标值。如果传入nil作为视图引用参数的值,则上面这些方法会将视图所在窗口的坐标系统作为转换的源或目标坐标系统。

除了UIView的转换方法之外,UIWindow类也定义了几个转换方法。这些方法和UIView的版本类似,只是UIView定义的方法将视图本地坐标系统作为转换的源或目标坐标系统,而UIWindow的版本则使用窗口坐标系统。

当参与转换的视图没有被旋转,或者被转换的对象仅仅是点的时候,坐标转换相当直接。如果是在旋转之后的视图之间转换矩形或尺寸数据,则其几何结构必须经过合理的改变,才能得到正确的结果坐标。在对矩形结构进行转换时,UIView类假定您希望保证原来的屏幕区域被覆盖,因此转换后的矩形会被放大,其结果是使放大后的矩形(如果放在对应的视图中)可以完全覆盖原来的矩形区域。图2-11显示了将rotatedView对象的坐标系统中的矩形转换到其超类(outerView)坐标系统的结果。

图2-11  对旋转后视图中的值进行转换

Converting values in a rotated view

对于尺寸信息,UIView简单地将它处理为分别相对于源视图和目标视图(0.0, 0.0)点的偏移量。虽然偏移量保持不变,但是相对于坐标轴的差额会随着视图的旋转而移动。在转换尺寸数据时,UIKit总是返回正的数值。

标识视图

您可以通过UIViewviewWithTag:方法来检索标识过的视图。该方法从消息的接收者自身开始,通过深度优先的方法来检索接收者的子视图。

 

在运行时修改视图

实现视图动画

动画块从调用UIViewbeginAnimations:context:类方法开始,而以调用commitAnimations类方法作为结束。在这两个调用之间,您可以配置动画的参数和改变希望实行动画的属性值。一旦调用commitAnimations方法,UIKit就会开始执行动画,即把给定属性从当前值到新值的变化过程用动画表现出来。动画块可以被嵌套,但是在最外层的动画块提交之前,被嵌套的动画不会被执行。

表2-2列举了UIView类中支持动画的属性。

表2-2  支持动画的属性

属性

描述

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方法来确定当前是否激活动画。

配置动画的委托

您可以为动画块分配一个委托,并通过该委托接收动画开始和结束的消息。当您需要在动画开始前和结束后立即执行其它任务时,可能就需要这样做。您可以通过UIViewsetAnimationDelegate:类方法来设置委托,并通过setAnimationWillStartSelector:setAnimationDidStopSelector:方法来指定接收消息的选择器方法。消息处理方法的形式如下:

- (void)animationWillStart:(NSString *)animationID context:(void *)context;
- (void)animationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context;

上面两个方法的animationIDcontext参数和动画块开始时传给beginAnimations:context:方法的参数相同:

  • animationID - 应用程序提供的字符串,用于标识一个动画块中的动画。

  • context - 也是应用程序提供的对象,用于向委托对象传递额外的信息。

setAnimationDidStopSelector:选择器方法还有一个参数—即一个布尔值。如果动画顺利完成,没有被其它动画取消或停止,则该值为YES

响应布局的变化

任何时候,当视图的布局发生改变时,UIKit会激活每个视图的自动尺寸调整行为,然后调用各自的layoutSubviews方法,使您有机会进一步调整子视图的几何尺寸。下面列举的情形都会引起视图布局的变化:

  • 视图边界矩形的尺寸发生变化。

  • 滚动视图的内容偏移量—也就是可视内容区域的原点—发生变化。

  • 和视图关联的转换矩阵发生变化。

  • 和视图层相关联的Core Animation子层组发生变化。

  • 您的应用程序调用视图的setNeedsLayoutlayoutIfNeeded方法来强制进行布局。

  • 您的应用程序调用视图背后的层对象的setNeedsLayout方法来强制进行布局。

子视图的初始布局由视图的自动尺寸调整行为来负责。

有些时候,您可能希望通过layoutSubviews方法来手工调整子视图的布局,而不是完全依赖自动尺寸调整行为。举例来说,如果您要实现一个由几个子视图元素组成的定制控件,则可以通过手工调整子视图来精确控制控件在一定尺寸范围内的外观。还有,如果一个视图表示的滚动内容区域很大,可以选择将内容显示为一组平铺的子视图,在滚动过程中,可以回收离开屏幕边界的视图,并在填充新内容后将它重新定位,使它成为下一个滚入屏幕的视图。

请注意:您也可以用layoutSubviews方法来调整作为子层链接到视图层的定制CALayer对象。您可以通过对隐藏在视图后面的层层次进行管理,实现直接基于Core Animation的高级动画。有关如何通过Core Animation管理层层次的更多信息,请参见Core Animation编程指南

重画视图的内容

有些时候,应用程序数据模型的变化会影响到相应的用户界面。为了反映这些变化,您可以将相应的视图标识为需要刷新(通过调用setNeedsDisplaysetNeedsDisplayInRect:方法)。和简单创建一个图形上下文并进行描画相比,将视图标识为需要刷新的方法使系统有机会更有效地执行描画操作。举例来说,如果您在某个运行周期中将一个视图的几个区域标识为需要刷新,系统就会将这些需要刷新的区域进行合并,并最终形成一个drawRect:方法的调用。结果,只需要创建一个图形上下文就可以描画所有这些受影响的区域。这个做法比连续快速创建几个图形上下文要有效得多。

实现drawRect:方法的视图总是需要检查传入的矩形参数,并用它来限制描画操作的范围。因为描画是开销相对昂贵的操作,以这种方式来限制描画是提高性能的好方法。

缺省情况下,视图在几何上的变化并不自动导致重画。相反,大多数几何变化都由Core Animation来自动处理。具体来说,当您改变视图的frameboundscenter、或transform属性时,Core Animation会将相应的几何变化应用到与视图层相关联的缓存位图上。在很多情况下,这种方法是完全可以接受的,但是如果您发现结果不是您期望得到的,则可以强制UIKit对视图进行重画。为了避免Core Animation自动处理几何变化,您可以将视图的contentMode属性声明设置为UIViewContentModeRedraw。更多有关内容模式的信息,请参见“内容模式和比例缩放”部分。

隐藏视图

您可以通过改变视图的hidden属性声明来隐藏或显示视图。将这个属性设置为YES会隐藏视图,设置为NO则可以显示视图。对一个视图进行隐藏会同时隐藏其内嵌的所有子视图,就好象它们自己的hidden属性也被设置一样。

当您隐藏一个视图时,该视图仍然会保留在视图层次中,但其内容不会被描画,也不会接收任何触摸事件。由于隐藏视图仍然存在于视图层次中,所以会继续参与自动尺寸调整和其它布局操作。如果被隐藏的视图是当前的第一响应者,则该视图会自动放弃其自动响应者的状态,但目标为第一响应者的事件仍然会传递给隐藏视图。有关响应者链的更多信息,请参见“响应者对象和响应者链”部分。

 

创建一个定制视图

初始化您的定制视图

程序清单2-2  初始化一个视图的子类

- (id)initWithFrame:(CGRect)aRect {
    self = [super initWithFrame:aRect];
    if (self) {
          // setup the initial properties of the view
          ...
       }
    return self;
}

 

描画您的视图内容

当您改变视图内容时,可以通过setNeedsDisplaysetNeedsDisplayInRect:方法来将需要重画的部分通知给系统。在应用程序返回运行循环之后,会对所有的描画请求进行合并,计算界面中需要被更新的部分;之后就开始遍历视图层次,向需要更新的视图发送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对象的beginIgnoringInteractionEventsendIgnoringInteractionEvents方法。这些方法影响的是整个应用程序的事件分发,而不仅仅是某个视图。

在处理触摸事件时,UIKit会通过UIViewhitTest: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];
}


posted @ 2012-02-06 00:34  Piosa  阅读(1717)  评论(0编辑  收藏  举报