视图层次中的坐标转换
很多时候,特别是处理事件的时候,应用程序可能需要将一个相对于某边框的坐标值转换为相对于另一个边框的值。例如,触摸事件通常使用基于窗口指标系统的坐标值来报告事件发生的位置,但是视图对象需要的是相对于视图本地坐标的位置信息,两者可能是不一样的。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:
方法来设置动画的重复次数。 -
用
setAnimationRepeatAutore
方法来指定动画在到达目标值时是否自动反向播放。您可以结合使用这个方法和verses: setAnimationRepeatCount:
方法,使各个属性在初始值和目标值之间平滑切换一段时间。
commitAnimations
类方法在调用之后和动画开始之前立刻返回。UIKit在一个独立的、和应用程序的主事件循环分离的线程中执行动画。commitAnimations
方法将动画发送到该线程,然后动画就进入线程中的队列,直到被执行。缺省情况下,只有在当前正在运行的动画块执行完成后,Core Animation才会启动队列中的动画。但是,您可以通过向动画块中的setAnimationBeginsFromCu
类方法传入YES
来重载这个行为,使动画立即启动。这样做会停止当前正在执行的动画,而使新动画在当前状态下开始执行。
缺省情况下,所有支持动画的属性在动画块中发生的变化都会形成动画。如果您希望让动画块中发生的某些变化不产生动画效果,可以通过setAnimationsEnabled:
方法来暂时禁止动画,在完成修改后才重新激活动画。在调用setAnimationsEnabled:
方法并传入NO
值之后,所有的改变都不会产生动画效果,直到用YES
值再次调用这个方法或者提交整个动画块时,动画才会恢复。您可以用areAnimationsEnabled
方法来确定当前是否激活动画。
配置动画的委托
您可以为动画块分配一个委托,并通过该委托接收动画开始和结束的消息。当您需要在动画开始前和结束后立即执行其它任务时,可能就需要这样做。您可以通过UIView
的setAnimationDelegate:
类方法来设置委托,并通过setAnimationWillStartSel
和setAnimationDidStopSelec
方法来指定接收消息的选择器方法。消息处理方法的形式如下:
- (void)animationWillStart:(NSString *)animationID context:(void *)context; |
- (void)animationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context; |
上面两个方法的animationID和context参数和动画块开始时传给beginAnimations:context:
方法的参数相同:
-
animationID
- 应用程序提供的字符串,用于标识一个动画块中的动画。 -
context
- 也是应用程序提供的对象,用于向委托对象传递额外的信息。
setAnimationDidStopSelec
选择器方法还有一个参数—即一个布尔值。如果动画顺利完成,没有被其它动画取消或停止,则该值为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的函数、或者使用两者相结合的方法来直接进行描画。需要的话,您可以通过UIGraphicsGetCurrentCont
函数来取得当前图形上下文的指针,实现对它的访问。
程序清单2-3显示了drawRect:
方法的一个简单实现,即在视图边界描画一个10像素宽的红色边界。由于UIKit描画操作的实现也是基于Quartz,所以您可以像下面这样混合使用不同的描画调用来得到期望的结果。
程序清单2-3
- (void)drawRect:(CGRect)rect { |
CGContextRef context = UIGraphicsGetCurrentCont |
CGRect myFrame = self.bounds; |
|
CGContextSetLineWidth(context, 10); |
|
[[UIColor redColor] set]; |
UIRectFrame(myFrame); |
} |
如果您能确定自己的描画代码总是以不透明的内容覆盖整个视图的表面,则可以将视图的opaque
属性声明设置为YES
,以提高描画代码的总体效率。当您将视图标识为不透明时,UIKit会避免对该视图正下方的内容进行描画。这不仅减少了描画开销的时间,而且减少内容合成需要的工作。然而,只有当您能确定视图提供的内容为不透明时,才能将这个属性设置为YES
;如果您不能保证视图内容总是不透明,则应该将它设置为NO
。
提高描画性能(特别是在滚动过程)的另一个方法是将视图的clearsContextBeforeDrawi
属性设置为NO
。当这个属性被设置为YES
时,UIKIt会在调用drawRect:
方法之前,把即将被该方法更新的区域填充为透明的黑色。将这个属性设置为NO
可以取消相应的填充操作,而由应用程序负责完全重画传给drawRect:
方法的更新矩形中的部分。这样的优化在滚动过程中通常是一个好的折衷。
响应事件
UIView
类是UIResponder
的一个子类,因此能够接收用户和视图内容交互时产生的触摸事件。触摸事件从发生触摸的视图开始,沿着响应者链进行传递,直到最后被处理。视图本身就是响应者,是响应者链的参与者,因此可以收到所有关联子视图派发给它们的触摸事件。
处理触摸事件的视图通常需要实现下面的所有方法,更多细节请参见“事件处理”部分:
请记住,在缺省情况下,视图每次只响应一个触摸动作。如果用户将第二个手指放在屏幕上,系统会忽略该触摸事件,而不会将它报告给视图对象。如果您希望在视图的事件处理器方法中跟踪多点触摸手势,则需要重新激活多点触摸事件,具体方法是将视图的multipleTouchEnabled
属性声明设置为YES
。
某些视图,比如标签和图像视图,在初始状态下完全禁止事件处理。您可以通过改变视图的userInteractionEnabled
属性值来控制视图是否可以对事件进行处理。当某个耗时很长的操作被挂起时,您可以暂时将这个属性设置为NO
,使用户无法对视图的内容进行操作。为了阻止事件到达您的视图,还可以使用UIApplication
对象的beginIgnoringInteraction
和endIgnoringInteractionEv
方法。这些方法影响的是整个应用程序的事件分发,而不仅仅是某个视图。
在处理触摸事件时,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]; |
} |
事件处理
本章将描述iPhone OS系统中的事件类型,并解释如何处理这些事件。文中还将讨论如何在应用程序内部或不同应用程序间通过UIPasteboard
类提供的设施进行数据的拷贝和粘贴,该类是iPhone OS 3.0引入的。
iPhone OS支持两种类型的事件:即触摸事件或运动事件。在iPhone OS 3.0中,UIEvent
类已经被扩展为不仅可以包含触摸事件和运动事件,还可以容纳将来可能引入的其它事件类型。每个事件都有一个与之关联的事件类型和子类型,可以通过UIEvent
的type
和subtype
属性声明进行访问,类型既包括触摸事件,也包括运动事件。在iPhone OS 3.0上,子类型只有一种,即摇摆-运动子类型(UIEventSubtypeMotionShak
)。
触摸事件
iPhone OS中的触摸事件基于多点触摸模型。用户不是通过鼠标和键盘,而是通过触摸设备的屏幕来操作对象、输入数据、以及指示自己的意图。iPhone OS将一个或多个和屏幕接触的手指识别为多点触摸序列的 一部分,该序列从第一个手指碰到屏幕开始,直到最后一个手指离开屏幕结束。iPhone OS通过一个多点触摸序列来跟踪与屏幕接触的手指,记录每个手指的触摸特征,包括手指在屏幕上的位置和发生触摸的时间。应用程序通常将特定组合的触摸识别 为手势,并以用户直觉的方式来进行响应,比如对收缩双指距离的手势,程序的响应是缩小显示的内容;对轻拂屏幕的手势,则响应为滚动显示内容。
请注意:手指在屏幕上能达到的精度和鼠标指针有很大的不同。当用户触击屏幕时,接触区域实际上是椭圆形的,而且比用户 想像的位置更靠下一点。根据触摸屏幕的手指、手指的尺寸、手指接触屏幕的力量、手指的方向、以及其它因素的不同,其“接触部位”的尺寸和形状也有所不同。 底层的多点触摸系统会分析所有的这些信息,为您计算出单一的触点。
很多UIKit类对多点触摸事件的处理方式不同于它的对象实例,特别是像UIButton
和UISlider
这样的UIControl
的子类。这些子类的对象—被称为控件对象—只接收特定类型的手势,比如触击或向特定方向拖拽。控件对象在正确配置之后,会在某种手势发生后将动作消息发送给目标对象。其它的UIKit类则在其它的上下文中处理手势,比如UIScrollView
可以为表格视图和具有很大内容区域的文本视图提供滚动行为。
某些应用程序可能不需要直接处理事件,它们可以依赖UIKit类实现的行为。但是,如果您创建了UIView
的定制子类—这是iPhone OS系统开发的常见模式—且希望该视图响应特定的触摸事件,就需要实现处理该事件所需要的代码。而且,如果您希望一个UIKit对象以不同的方式响应事件,就必须创建框架类的子类,并重载相应的事件处理方法。
事件和触摸
在iPhone OS中,触摸动作是指手指碰到屏幕或在屏幕上移动,它是一个多点触摸序列的 一部分。比如,一个pinch-close手势就包含两个触摸动作:即屏幕上的两个手指从相反方向靠近对方。一些单指手势则比较简单,比如触击、双击、或 轻拂(即用户快速碰擦屏幕)。应用程序也可以识别更为复杂的手势,举例来说,如果一个应用程序使用具有转盘形状的定制控件,用户就需要用多个手指来“转 动”转盘,以便进行某种精调。
事件是当用户手指触击屏幕及在屏幕上移动时,系统不断发送给应用程序的对象。事件对象为一个多点触摸序列中所有触摸动 作提供一个快照,其中最重要的是特定视图中新发生或有变化的触摸动作。一个多点触摸序列从第一个手指碰到屏幕开始,其它手指随后也可能触碰屏幕,所有手指 都可能在屏幕上移动。当最后一个手指离开屏幕时,序列就结束了。在触摸的每个阶段,应用程序都会收到事件对象。
触摸信息有时间和空间两方面,时间方面的信息称为阶段(phrase),表示触摸是否刚刚开始、是否正在移动或处于静止状态,以及何时结束—也就是手指何时从屏幕举起(参见图3-1)。 触摸信息还包括当前在视图或窗口中的位置信息,以及之前的位置信息(如果有的话)。当一个手指接触屏幕时,触摸就和某个窗口或视图关联在一起,这个关联在 事件的整个生命周期都会得到维护。如果有多个触摸同时发生,则只有和同一个视图相关联的触摸会被一起处理。类似地,如果两个触摸事件发生的间隔时间很短, 也只有当它们和同一个视图相关联时,才会被处理为多触击事件。
在iPhone OS中,一个UITouch
对象表示一个触摸,一个UIEvent
对象表示一个事件。事件对象中包含与当前多点触摸序列相对应的所有触摸对象,还可以提供与特定视图或窗口相关联的触摸对象(参见图3-2)。在一个触摸序列发生的过程中,对应于特定手指的触摸对象是持久的,在跟踪手指运动的过程中,UIKit会对其进行修改。发生改变的触摸属性变量有触摸阶段、触摸在视图中的位置、发生变化之前的位置、以及时间戳。事件处理代码通过检查这些属性的值来确定如何响应事件。
系统可能随时取消多点触摸序列,进行事件处理的应用程序必须做好正确响应的准备。事件的取消可能是由于重载系统事件引起的,电话呼入就是这样的例子。