本章将描述iPhone OS系统中的事件类型,并解释如何处理这些事件。文中还将讨论如何在应用程序内部或不同应用程序间通过
UIPasteboard
类提供的设施进行数据的拷贝和粘贴,该类是iPhone OS 3.0引入的。

iPhone OS支持两种类型的事件:即触摸事件或运动事件。在iPhone OS 3.0中,
UIEvent
类已经被扩展为不仅可以包含触摸事件和运动事件,还可以容纳将来可能引入的其它事件类型。每个事件都有一个与之关联的事件类型和子类型,可以通过
UIEvent

type

subtype
属性声明进行访问,类型既包括触摸事件,也包括运动事件。在iPhone OS 3.0上,子类型只有一种,即摇摆-运动子类型(
UIEventSubtypeMotionShake
)。


触摸事件

iPhone OS中的触摸事件基于多点触摸模型。用户不是通过鼠标和键盘,而是通过触摸设备的屏幕来操作对象、输入数据、以及指示自己的意图。iPhone OS将一个或多个和屏幕接触的手指识别为多点触摸序列的一部分,该序列从第一个手指碰到屏幕开始,直到最后一个手指离开屏幕结束。iPhone OS通过一个多点触摸序列来跟踪与屏幕接触的手指,记录每个手指的触摸特征,包括手指在屏幕上的位置和发生触摸的时间。应用程序通常将特定组合的触摸识别为手势,并以用户直觉的方式来进行响应,比如对收缩双指距离的手势,程序的响应是缩小显示的内容;对轻拂屏幕的手势,则响应为滚动显示内容。


请注意:手指在屏幕上能达到的精度和鼠标指针有很大的不同。当用户触击屏幕时,接触区域实际上是椭圆形的,而且比用户想像的位置更靠下一点。根据触摸屏幕的手指、手指的尺寸、手指接触屏幕的力量、手指的方向、以及其它因素的不同,其“接触部位”的尺寸和形状也有所不同。底层的多点触摸系统会分析所有的这些信息,为您计算出单一的触点。


很多UIKit类对多点触摸事件的处理方式不同于它的对象实例,特别是像
UIButton

UISlider
这样的
UIControl
的子类。这些子类的对象—被称为控件对象—只接收特定类型的手势,比如触击或向特定方向拖拽。控件对象在正确配置之后,会在某种手势发生后将动作消息发送给目标对象。其它的UIKit类则在其它的上下文中处理手势,比如
UIScrollView
可以为表格视图和具有很大内容区域的文本视图提供滚动行为。

某些应用程序可能不需要直接处理事件,它们可以依赖UIKit类实现的行为。但是,如果您创建了
UIView
定制子类—这是iPhone OS系统开发的常见模式—且希望该视图响应特定的触摸事件,就需要实现处理该事件所需要的代码。而且,如果您希望一个UIKit对象以不同的方式响应事件,就必须创建框架类的子类,并重载相应的事件处理方法。


事件和触摸

在iPhone OS中,触摸动作是指手指碰到屏幕或在屏幕上移动,它是一个多点触摸序列的一部分。比如,一个pinch-close手势就包含两个触摸动作:即屏幕上的两个手指从相反方向靠近对方。一些单指手势则比较简单,比如触击、双击、或轻拂(即用户快速碰擦屏幕)。应用程序也可以识别更为复杂的手势,举例来说,如果一个应用程序使用具有转盘形状的定制控件,用户就需要用多个手指来“转动”转盘,以便进行某种精调。

事件是当用户手指触击屏幕及在屏幕上移动时,系统不断发送给应用程序的对象。事件对象为一个多点触摸序列中所有触摸动作提供一个快照,其中最重要的是特定视图中新发生或有变化的触摸动作。一个多点触摸序列从第一个手指碰到屏幕开始,其它手指随后也可能触碰屏幕,所有手指都可能在屏幕上移动。当最后一个手指离开屏幕时,序列就结束了。在触摸的每个阶段,应用程序都会收到事件对象。

触摸信息有时间和空间两方面,时间方面的信息称为阶段(phrase),表示触摸是否刚刚开始、是否正在移动或处于静止状态,以及何时结束—也就是手指何时从屏幕举起(参见图3-1)。触摸信息还包括当前在视图或窗口中的位置信息,以及之前的位置信息(如果有的话)。当一个手指接触屏幕时,触摸就和某个窗口或视图关联在一起,这个关联在事件的整个生命周期都会得到维护。如果有多个触摸同时发生,则只有和同一个视图相关联的触摸会被一起处理。类似地,如果两个触摸事件发生的间隔时间很短,也只有当它们和同一个视图相关联时,才会被处理为多触击事件。


图3-1 多点触摸序列和触摸阶段

A multi-touch sequence and touch phases

在iPhone OS中,一个
UITouch
对象表示一个触摸,一个
UIEvent
对象表示一个事件。事件对象中包含与当前多点触摸序列相对应的所有触摸对象,还可以提供与特定视图或窗口相关联的触摸对象(参见图3-2)。在一个触摸序列发生的过程中,对应于特定手指的触摸对象是持久的,在跟踪手指运动的过程中,UIKit会对其进行修改。发生改变的触摸属性变量有触摸阶段、触摸在视图中的位置、发生变化之前的位置、以及时间戳。事件处理代码通过检查这些属性的值来确定如何响应事件。


图3-2 
UIEvent
对象及其
UITouch
对象间的关系

Relationship of a UIEvent object and its UITouch objects

系统可能随时取消多点触摸序列,进行事件处理的应用程序必须做好正确响应的准备。事件的取消可能是由于重载系统事件引起的,电话呼入就是这样的例子。


事件的传递

系统将事件按照特定的路径传递给可以对其进行处理的对象。如“核心应用程序架构”部分描述的那样,当用户触摸设备屏幕时,iPhone OS会将它识别为一组触摸对象,并将它们封装在一个
UIEvent
对象中,放入当前应用程序的事件队列中。事件对象将特定时刻的多点触摸序列封装为一些触摸对象。负责管理应用程序的
UIApplication
单件对象将事件从队列的顶部取出,然后派发给其它对象进行处理。典型情况下,它会将事件发送给应用程序的键盘焦点窗口—即拥有当前用户事件焦点的窗口,然后代表该窗口的
UIWindow
对象再将它发送给第一响应者进行处理(第一响应者在 “响应者对象和响应者链”部分中描述)。

应用程序通过触碰测试(hit-testing)来寻找事件的第一响应者,即通过递归调用视图层次中视图对象的
hitTest:withEvent:
方法来确认发生触摸的子视图。触摸对象的整个生命周期都和该视图互相关联,即使触摸动作最终移动到该视图区域之外也是如此。“事件处理技巧”部分对触碰测试在编程方面的一些隐含意义进行讨论。


UIApplication
对象和每个
UIWindow
对象都在
sendEvent:
方法(两个类都声明了这个方法)中派发事件。由于这些方法是事件进入应用程序的通道,所以,您可以从
UIApplication

UIWindow
派生出子类,重载
其sendEvent:
方法,实现对事件的监控或执行特殊的事件处理。但是,大多数应用程序都不需要这样做。


响应者对象和响应者链

响应者对象是可以响应事件并对其进行处理的对象。
UIResponder
是所有响应者对象的基类,它不仅为事件处理,而且也为常见的响应者行为定义编程接口。
UIApplication

UIView
、和所有从
UIView
派生出来的UIKit类(包括
UIWindow
)都直接或间接地继承自
UIResponder
类。

第一响应者是应用程序中当前负责接收触摸事件的响应者对象(通常是一个
UIView
对象)。
UIWindow
对象以消息的形式将事件发送给第一响应者,使其有机会首先处理事件。如果第一响应者没有进行处理,系统就将事件(通过消息)传递给响应者链中的下一个响应者,看看它是否可以进行处理。

响应者链是一系列链接在一起的响应者对象,它允许响应者对象将处理事件的责任传递给其它更高级别的对象。随着应用程序寻找能够处理事件的对象,事件就在响应者链中向上传递。响应者链由一系列“下一个响应者”组成,其顺序如下:


第一响应者将事件传递给它的视图控制器(如果有的话),然后是它的父视图。


类似地,视图层次中的每个后续视图都首先传递给它的视图控制器(如果有的话),然后是它的父视图。


最上层的容器视图将事件传递给
UIWindow
对象。


UIWindow
对象将事件传递给
UIApplication
单件对象。


如果应用程序找不到能够处理事件的响应者对象,则丢弃该事件。

响应者链中的所有响应者对象都可以实现
UIResponder
的某个事件处理方法,因此也都可以接收事件消息。但是,它们可能不愿处理或只是部分处理某些事件。如果是那样的话,它们可以将事件消息转送给下一个响应者,方法大致如下:


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch* touch = [touches anyObject];
    NSUInteger numTaps = [touch tapCount];
    if (numTaps < 2) {
        [self.nextResponder touchesBegan:touches withEvent:event];
   } else {
        [self handleDoubleTap:touch];
   }
}

请注意:如果一个响应者对象将一个多点触摸序列的初始阶段的事件处理消息转发给下一个响应者(在
touchesBegan:withEvent:
方法中), 就应该同样转发该序列的其它事件处理消息。


动作消息的处理也使用响应者链。当用户对诸如按键或分页控件这样的
UIControl
对象进行操作时,控件对象(如果正确配置的话)会向目标对象发送动作消息。但是,如果目标对象被指定为
nil
,应用程序就会像处理事件消息那样,把该动作消息路由给第一响应者。如果第一响应者没有进行处理,再发送给其下一个响应者,以此类推,将消息沿着响应者链向上传递。


调整事件的传递

UIKit为应用程序提供了一些简化事件处理、甚至完全关闭事件流的编程接口。下面对这些方法进行总结:


关闭事件的传递。缺省情况下,视图会接收触摸事件。但是,您可以将其
userInteractionEnabled
属性声明设置为
NO
,关闭事件传递的功能。隐藏或透明的视图也不能接收事件。


在一定的时间内关闭事件的传递。应用程序可以调用
UIApplication

beginIgnoringInteractionEvents
方法,并在随后调用
endIgnoringInteractionEvents
方法来实现这个目的。前一个方法使应用程序完全停止接收触摸事件消息,第二个方法则重启消息的接收。某些时候,当您的代码正在执行动画时,可能希望关闭事件的传递。


打开多点触摸的传递。 缺省情况下,视图只接收多点触摸序列的第一个触摸事件,而忽略所有其它事件。如果您希望视图处理多点触摸,就必须使它启用这个功能。在代码或Interface Builder的查看器窗口中将视图的
multipleTouchEnabled
属性设置为
YES
,就可以实现这个目标。


将事件传递限制在某个单独的视图上。 缺省情况下,视图的
exclusiveTouch
属性被设置为
NO
。将这个属性设置为
YES
会使相应的视图具有这样的特性:即当该视图正在跟踪触摸动作时,窗口中的其它视图无法同时进行跟踪,它们不能接收到那些触摸事件。然而,一个标识为“独占触摸”的视图不能接收与同一窗口中其它视图相关联的触摸事件。如果一个手指接触到一个独占触摸的视图,则仅当该视图是窗口中唯一一个跟踪手指的视图时,触摸事件才会被传递。如果一个手指接触到一个非独占触摸的视图,则仅当窗口中没有其它独占触摸视图跟踪手指时,该触摸事件才会被传递。


将事件传递限制在子视图上。一个定制的
UIView
类可以通过重载
hitTest:withEvent:
方法来将多点触摸事件的传递限制在它的子视图上。这个技巧的讨论请参见“事件处理技巧”部分。



处理多点触摸事件

为了处理多点触摸事件,
UIView
的定制子类(比较不常见的还有
UIApplication

UIWindow
的定制子类)必须至少实现一个
UIResponder
的事件处理方法。本文的下面部分将对这些方法进行描述,讨论处理常见手势的方法,并展示一个处理复杂多点触摸事件的响应者对象实例,以及就事件处理的某些技术提出建议。


事件处理方法

在一个多点触摸序列发生的过程中,应用程序会发出一系列事件消息。为了接收和处理这些消息,响应者对象的类必须至少实现下面这些由
UIResponder
类声明的方法之一:


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event

在给定的触摸阶段中,如果发生新的触摸动作或已有的触摸动作发生变化,应用程序就会发送这些消息:


当一个或多个手指触碰屏幕时,发送
touchesBegan:withEvent:
消息。


当一个或多个手指在屏幕上移动时,发送
touchesMoved:withEvent:
消息。


当一个或多个手指离开屏幕时,发送
touchesEnded:withEvent:
消息。


当触摸序列被诸如电话呼入这样的系统事件所取消时,发送
touchesCancelled:withEvent:
消息。


上面这些方法都和特定的触摸阶段(比如
UITouchPhaseBegan
)相关联,该信息存在于
UITouch
对象的
phase
属性声明中。

每个与事件处理方法相关联的消息都有两个参数。第一个参数是一个
UITouch
对象的集合,表示给定阶段中新的或者发生变化的触摸动作;第二个参数是一个
UIEvent
对象,表示这个特定的事件。您可以通过这个事件对象得到与之相关联的所有触摸对象(
allTouches
),或者发生在特定的视图或窗口上的触摸对象子集。其中的某些触摸对象表示自上次事件消息以来没有发生变化,或虽然发生变化但处于不同阶段的触摸动作。

为了处理给定阶段的事件,响应者对象常常从传入的集合参数中取得一或多个
UITouch
对象,然后考察这些对象的属性或取得它们的位置(如果需要处理所有触摸对象,可以向该
NSSet
对象发送
anyObject
消息)。
UITouch
类中有一个名为
locationInView:
的重要方法,如果传入
self
参数值,它会给出触摸动作在响应者坐标系统中的位置(假定该响应者是一个
UIView
对象,且传入的视图参数不为
nil
)。另外,还有一个与之平行的方法,可以给出触摸动作之前位置(
previousLocationInView:
)。
UITouch
实例的属性还可以给出发生多少次触碰(
tapCount
)、触摸对象的创建或最后一次变化发生在什么时间(
timestamp
)、以及触摸处于什么阶段(
phase
)。

响应者类并不是必须实现上面列出的所有三个事件方法。举例来说,如果它只对手指离开屏幕感兴趣,则只需要实现
touchesEnded:withEvent:
方法就可以了。

在一个多点触摸序列中,如果响应者在处理事件时创建了某些持久对象,则应该实现
touchesCancelled:withEvent:
方法,以便当系统取消该序列的时候对其进行清理。多点触摸序列的取消常常发生在应用程序的事件处理遭到外部事件—比如电话呼入—破坏的时候。请注意,响应者对象同样应该在收到多点触摸序列的
touchesEnded:withEvent:
消息时清理之前创建的对象(“事件处理技巧”部分讨论了如何确定一个序列中的最后一个touch-up事件)。


处理单个和多个触碰手势

iPhone应用程序中一个很常见的手势是触击:即用户用手指触碰一个对象。响应者对象可以以一种方式响应单击,而以另外一种方式响应双击,甚至可能以第三种方式响应三次触击。您可以通过考察
UITouch
对象的
tapCount
属性声明值来确定用户在一个响应者对象上的触击次数,

取得这个值的最好地方是
touchesBegan:withEvent:

touchesEnded:withEvent:
方法。在很多情况下,我们更倾向于后者,因为它与用户手指离开屏幕的阶段相对应。在触摸结束阶段(
UITouchPhaseEnded
)考察触击的次数可以确定手指是真的触击,而不是其它动作,比如手指接触屏幕后拖动的动作。

程序清单3-1展示了如何检测某个视图上是否发生双击。

程序清单3-1  检测双击手势


- (void) touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
{
    UITouch       *touch = [touches anyObject];
 
    if ([touch tapCount] == 2) {
        CGPoint tapPoint = [theTouch locationInView:self];
        // Process a double-tap gesture
    }
}

当一个响应者对象希望以不同的方式响应单击和双击事件时,就会出现复杂的情况。举例来说,单击的结果可能是选定一个对象,而双击则可能是显示一个编辑视图,用于编辑被双击的对象。那么,响应者对象如何知道一个单击不是另一个双击的起始部分呢?我们接下来解释响应者对象如何借助上文刚刚描述的事件处理方法来处理这种情况:



touchesEnded:withEvent:
方法中,当触击次数为一时,响应者对象就向自身发送一个
performSelector:withObject:afterDelay:
消息,其中的选择器标识由响应者对象实现的、用于处理单击手势的方法;第二个参数是一个
NSValue

NSDictionary
对象,用于保存相关的
UITouch
对象;时延参数则表示单击和双击手势之间的合理时间间隔。


请注意:使用一个
NSValue
对象或字典来保存触摸对象是因为它们会保持传入的对象。然而,您自己在进行事件处理时,不应该对
UITouch
对象进行保持。



touchesBegan:withEvent:
方法中,如果触击次数为二,响应者对象会向自身发送一个
cancelPreviousPerformRequestsWithTarget:
消息,取消当前被挂起和延期执行的调用。如果触碰次数不为二,则在指定的延时之后,先前步骤中由选择器标识的方法就会被调用,以处理单击手势。



touchesEnded:withEvent:
方法中,如果触碰次数为二,响应者会执行处理双击手势的代码。



检测碰擦手势

水平和垂直的碰擦(Swipe)是简单的手势类型,您可以简单地在自己的代码中进行跟踪,并通过它们执行某些动作。为了检测碰擦手势,您需要跟踪用户手指在期望的坐标轴方向上的运动。碰擦手势如何形成是由您自己来决定的,也就是说,您需要确定用户手指移动的距离是否足够长,移动的轨迹是否足够直,还有移动的速度是否足够快。您可以保存初始的触碰位置,并将它和后续的touch-moved事件报告的位置进行比较,进而做出这些判断。

程序清单3-2展示了一些基本的跟踪方法,可以用于检测某个视图上发生的水平碰擦。在这个例子中,视图将触摸的初始位置存储在名为
startTouchPosition
的成员变量中。随着用户手指的移动,清单中的代码将当前的触摸位置和起始位置进行比较,确定是否为碰擦手势。如果触摸在垂直方向上移动得太远,就会被认为不是碰擦手势,并以不同的方式进行处理。但是,如果手指继续在水平方向上移动,代码就继续将它作为碰擦手势来处理。一旦碰擦手势在水平方向移动得足够远,以至于可以认为是完整的手势时,处理例程就会触发相应的动作。检测垂直方向上的碰擦手势可以用类似的代码,只是把x和y方向的计算互换一下就可以了。

程序清单3-2  在视图中跟踪碰擦手势


#define HORIZ_SWIPE_DRAG_MIN  12
#define VERT_SWIPE_DRAG_MAX    4
 
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    startTouchPosition = [touch locationInView:self];
}
 
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint currentTouchPosition = [touch locationInView:self];
 
    // If the swipe tracks correctly.
    if (fabsf(startTouchPosition.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN &&
        fabsf(startTouchPosition.y - currentTouchPosition.y) <= VERT_SWIPE_DRAG_MAX)
    {
        // It appears to be a swipe.
        if (startTouchPosition.x < currentTouchPosition.x)
            [self myProcessRightSwipe:touches withEvent:event];
        else
            [self myProcessLeftSwipe:touches withEvent:event];
    }
    else
    {
        // Process a non-swipe event.
    }
}


处理复杂的多点触摸序列

触击和碰擦是简单的手势。如何处理更为复杂的多点触摸序列—实际上是解析应用程序特有的手势—取决于应用程序希望完成的具体目标。您可以跟踪所有阶段的所有触摸动作,记录触摸对象中发生变化的属性变量,并正确地改变内部的状态。

说明如何处理复杂的多点触摸序列的最好方法是通过实例。程序清单3-3展示一个定制的
UIView
对象如何通过在屏幕上动画移动“Welcome”标语牌来响应用户手指的移动,以及如何通过改变欢迎标语的语言来响应用户的双击手势(例子中的代码来自一个名为MoveMe的示例工程,进一步考察该工程可以更好地理解事件处理的上下文)。

程序清单3-3  处理复杂的多点触摸序列


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [[event allTouches] anyObject];
    // Only move the placard view if the touch was in the placard view
    if ([touch view] != placardView) {
        // On double tap outside placard view, update placard's display string
        if ([touch tapCount] == 2) {
            [placardView setupNextDisplayString];
        }
        return;
    }
    // "Pulse" the placard view by scaling up then down
    // Use UIView's built-in animation
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.5];
    CGAffineTransform transform = CGAffineTransformMakeScale(1.2, 1.2);
    placardView.transform = transform;
    [UIView commitAnimations];
 
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.5];
    transform = CGAffineTransformMakeScale(1.1, 1.1);
    placardView.transform = transform;
    [UIView commitAnimations];
 
    // Move the placardView to under the touch
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.25];
    placardView.center = [self convertPoint:[touch locationInView:self] fromView:placardView];
    [UIView commitAnimations];
}
 
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [[event allTouches] anyObject];
 
    // If the touch was in the placardView, move the placardView to its location
    if ([touch view] == placardView) {
        CGPoint location = [touch locationInView:self];
        location = [self convertPoint:location fromView:placardView];
        placardView.center = location;
        return;
    }
}
 
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [[event allTouches] anyObject];
 
    // If the touch was in the placardView, bounce it back to the center
    if ([touch view] == placardView) {
        // Disable user interaction so subsequent touches don't interfere with animation
        self.userInteractionEnabled = NO;
        [self animatePlacardViewToCenter];
        return;
    }
}

请注意:对于通过描画自身的外观来响应事件的定制视图,在事件处理方法中通常应该只是设置描画状态,而在
drawRect:
方法中执行所有的描画操作。如果需要了解更多关于描画视图内容的方法,请参见“图形和描画”部分。


 


事件处理技巧

下面是一些事件处理技巧,您可以在自己的代码中使用。


跟踪UITouch对象的变化

在事件处理代码中,您可以将触摸状态的相关位置保存下来,以便在必要时和变化之后的
UITouch
实例进行比较。作为例子,假定您希望将每个触摸对象的最后位置和其初始位置进行比较,则在
touchesBegan:withEvent:
方法中,您可以通过
locationInView:
方法得到每个触摸对象的初始位置,并以
UITouch
对象的地址作为键,将它们存储在
CFDictionaryRef
封装类型中;然后,在
touchesEnded:withEvent:
方法中,可以通过传入
UITouch
对象的地址取得该对象的初始位置,并将它和当前位置进行比较(您应该使用
CFDictionaryRef
类型,而不是
NSDictionary
对象,因为后者需要对其存储的项目进行拷贝,而
UITouch
类并不采纳
NSCopying
协议,该协议在对象拷贝过程中是必须的)。


对子视图或层上的触摸动作进行触碰测试

定制视图可以用
UIView

hitTest:withEvent:
方法或
CALayer

hitTest:
方法来寻找接收触摸事件的子视图或层,进而正确地处理事件。下面的例子用于检测定制视图的层中的“Info” 图像是否被触碰。


- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
    CGPoint location = [[touches anyObject] locationInView:self];
    CALayer *hitLayer = [[self layer] hitTest:[self convertPoint:location fromView:nil]];
 
    if (hitLayer == infoImage) {
        [self displayInfo];
    }
}

如果您有一个携带子视图的定制视图,就需要明确自己是希望在子视图的级别上处理触摸事件,还是在父视图的级别上进行处理。如果子视图没有实现
touchesBegan:withEvent:

touchesEnded:withEvent:
、或者
touchesMoved:withEvent:
方法,则这些消息就会沿着响应者链被传播到父视图。然而,由于多次触碰和多点触摸事件与发生这些动作所在的子视图是互相关联的,所以父视图不会接收到这些事件。为了保证能接收到所有的触摸事件,父视图必须重载
hitTest:withEvent:
方法,并在其中返回其本身,而不是它的子视图。


确定多点触摸序列中最后一个手指何时离开

当您希望知道一个多点触摸序列中的最后一个手指何时从视图离开时,可以将传入的集合参数中包含的
UITouch
对象数量和
UIEvent
参数对象中与该视图关联的触摸对象数量相比较。请看下面的例子:


- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
    if ([touches count] == [[event touchesForView:self] count]) {
        // last finger has lifted....
    }
}

 


运动事件

当用户以特定方式移动设备,比如摇摆设备时,iPhone或者iPod touch会产生运动事件。运动事件源自设备加速计。系统会对加速计的数据进行计算,如果符合某种模式,就将它解释为手势,然后创建一个代表该手势的
UIEvent
对象,并发送给当前活动的应用程序进行处理。


请注意:在iPhone 3.0上,只有摇摆设备的动作会被解释为手势,并形成运动事件。


运动事件比触摸事件简单得多。系统只是告诉应用程序动作何时开始及何时结束,而不包括在这个过程中发生的每个动作的时间。而且,触摸事件中包含一个触摸对象的集合及其相关的状态,而运动事件中除了事件类型、子类型、和时间戳之外,没有其它状态。系统以这种方式来解析运动手势,避免和方向变化事件造成冲突。

为了处理运动事件,
UIResponder
的子类必须实现
motionBegan:withEvent:

motionEnded:withEvent:
方法之一,或者同时实现这两个方法。举例来说,如果用户希望赋以水平摆动和垂直摆动不同的意义,就可以在
motionBegan:withEvent:
方法中将当前加速计轴的值缓存起来,并将它们和
motionEnded:withEvent:
消息传入的值相比较,然后根据不同的结果进行动作。响应者还应该实现
motionCancelled:withEvent:
方法,以便响应系统发出的运动取消的事件。有些时候,这些事件会告诉您整个动作根本不是一个正当的手势。

应用程序及其键盘焦点窗口会将运动事件传递给窗口的第一响应者。如果第一响应者不能处理,事件就沿着响应者链进行传递,直到最终被处理或忽略,这和触摸事件的处理相类似(详细信息请参见“事件的传递”部分)。但是,摆动事件和触摸事件有一个很大的不同,当用户开始摆动设备时,系统就会通过
motionBegan:withEvent:
消息的方式向第一响应者发送一个运动事件,如果第一响应者不能处理,该事件就在响应者链中传递;如果摆动持续的时间小于1秒左右,系统就会向第一响应者发送
motionEnded:withEvent:
消息;但是,如果摆动时间持续更长,如果系统确定当前的动作不是摆动,则第一响应者会收到一个
motionCancelled:withEvent:
消息。

如果摆动事件沿着响应者链传递到窗口而没有被处理,且
UIApplication

applicationSupportsShakeToEdit
属性被设置为
YES
,则iPhone OS会显示一个带有撤消(Undo)和重做(Redo)的命令。缺省情况下,这个属性的值为
NO


拷贝、剪切、和粘贴操作

在iPhone OS 3.0之后,用户可以在一个应用程序上拷贝文本、图像、或其它数据,然后粘贴到当前或其它应用程序的不同位置上。比如,您可以从某个电子邮件中拷贝一个地址,然后粘贴到Contacts程序的地址域中。目前,UIKit框架
UITextView

UITextField
、和
UIWebView
类中实现了拷贝-剪切-粘贴支持。如果您希望在自己的应用程序中得到这个行为,可以使用这些类的对象,或者自行实现。

本文的下面部分将描述UIKit中用于拷贝、剪切、和粘贴操作的编程接口,并解释其用法。


请注意:与拷贝和粘贴操作相关的使用指南,请参见iPhone人机界面指南文档中的“支持拷贝和粘贴”部分。



UIKit中支持拷贝-粘贴操作的设施

UIKit框架提供几个类和一个非正式协议,用于为应用程序中的拷贝、剪切、和粘贴操作提供方法和机制。具体如下:



UIPasteboard
类提供了粘贴板的接口。粘贴板是用于在一个应用程序内或不同应用程序间进行数据共享的受保护区域。该类提供了读写剪贴板上数据项目的方法。



UIMenuController
类可以在选定的拷贝、剪切、和粘贴对象的上下方显示一个编辑菜单。编辑菜单上的命令可以有拷贝、剪切、粘贴、选定、和全部选定。



UIResponder
类声明了
canPerformAction:withSender:
方法。响应者类可以实现这个方法,以根据当前的上下文显示或移除编辑菜单上的命令。



UIResponderStandardEditActions
非正式协议声明了处理拷贝、剪切、粘贴、选定、和全部选定命令的接口。当用户触碰编辑菜单上的某个命令时,相应的
UIResponderStandardEditActions
方法就会被调用。



粘贴板的概念

粘贴板是同一应用程序内或不同应用程序间交换数据的标准化机制。粘贴板最常见的的用途是处理拷贝、剪贴、和粘贴操作:


当用户在一个应用程序中选定数据并选择拷贝(或剪切)菜单命令时,被选择的数据就会被放置在粘贴板上。


当用户选择粘贴命令时(可以在同一或不同应用程序中),粘贴板上的数据就会被拷贝到当前应用程序上。


在iPhone OS中,粘贴板也用于支持查找(Find)操作。此外,还可以用于在不同应用程序间通过定制的URL类型传输数据(而不是通过拷贝、剪切、和粘贴命令,关于这个技巧的信息请参见“和其它应用程序间的通讯”部分。

无论是哪种操作,您通过粘贴板执行的基本任务是读写粘贴板数据。虽然这些任务在概念上很简单,但是它们屏蔽了很多重要的细节。复杂的原因主要在于数据的表现方式可能有很多种,而这个复杂性又引入了效率的考虑。本文的下面部分将对这些以及其它的问题进行讨论。


命名粘贴板

粘贴板可能是公共的,也可能是私有的。公共粘贴板被称为系统粘贴板;私有粘贴板则由应用程序自行创建,因此被称为应用程序粘贴板。粘贴板必须有唯一的名字。
UIPasteboard
定义了两个系统粘贴板,每个都有自己的名字和用途:



UIPasteboardNameGeneral
用于剪切、拷贝、和粘贴操作,涉及到广泛的数据类型。您可以通过该类的
generalPasteboard
类方法来取得代表通用(General)粘贴板的单件对象。



UIPasteboardNameFind
用于检索操作。当前用户在检索条(
UISearchBar
)键入的字符串会被写入到这个粘贴板中,因此可以在不同的应用程序中共享。您可以通过调用
pasteboardWithName:create:
类方法,并在名字参数中传入
UIPasteboardNameFind
值来取得代表检索粘贴板的对象。


典型情况下,您只需使用系统定义的粘贴板就够了。但在必要时,您也可以通过
pasteboardWithName:create:
方法来创建自己的应用程序粘贴板。如果您调用
pasteboardWithUniqueName
方法,
UIPasteboard
会为您提供一个具有唯一名称的应用程序粘贴板。您可以通过其
name
属性声明来取得这个名称。


粘贴板的持久保留

您可以将粘贴板标识为持久保留,使其内容在当前使用的应用程序终止后继续存在。不持久保留的粘贴板在其创建应用程序退出后就会被移除。系统粘贴板是持久保留的,而应用程序粘贴板在缺省情况下是不持久保留的。将其应用程序粘贴板的
persistent
属性设置为
YES
可以使其持久保留。当持久粘贴板的拥有者程序被用户卸载时,其自身也会被移除。


粘贴板的拥有者和数据项

最后将数据放到粘贴板的对象被称为该粘贴板的拥有者。放到粘贴板上的每一片数据都称为一个粘贴板数据项。粘贴板可以保有一个或多个数据项。应用程序可以放入或取得期望数量的数据项。举例来说,假定用户在视图中选择的内容包含一些文本和一个图像,粘贴板允许您将文本和图像作为不同的数据项进行拷贝。从粘贴板读取多个数据项的应用程序可以选择只读取被支持的数据项(比如只是文本,而不支持图像)。


重要提示:当一个应用程序将数据写入粘贴板时,即使只是单一的数据项,该数据也会取代粘贴板的当前内容。虽然您可能使用
UIPasteboard

addItems:
方法来添加项目,但是该写入方法并不会将那些项目加入到粘贴板当前内容之后。

 



数据的表示和UTI

粘贴板操作经常在不同的应用程序间执行。系统并不要求应用程序了解对方的信息,包括对方可以处理的数据种类。为了最大化潜在的数据分享能力,粘贴板可以保留同一个数据项的多种表示。例如,一个富文本编辑器可以提供被拷贝数据的HTML、PDF、和纯文本表示。粘贴板上的一个数据项包括应用程序可为该数据提供的所有表示。

粘贴板数据项的每种表示通常都有一个唯一类型标识符(Unique Type Identifier,缩写为UTI)。UTI简单定义为一个唯一标识特定数据类型的字符串。UTI提供了一个标识数据类型的常用手段。如果您希望支持一个定制的数据类型,就必须为其创建一个唯一的标识符。为此,您可以用反向DNS表示法来定义类型标识字符串,以确保其唯一性。例如,您可以用
com.myCompany.myApp.myType
来表示一个定制的类型标识。更多有关UTI的信息请参见统一类型标识符概述

作为例子,假定一个应用程序支持富文本和图像的选择,它可能希望将富文本和Unicode版本的选定文本,以及选定图像的不同表示放到粘贴板上。在这样的场景下,每个数据项的每种表示都和它自己的数据一起保存,如图3-3所示。


图3-3  粘贴板及其表示

Pasteboard items and representations

一般情况下,为了最大化潜在的共享可能性,粘贴板数据项应该包括尽可能多的表示。

粘贴板的读取程序必须找到最适合自身能力(如果有的话)的数据类型。通常情况下,这意味着选择内涵最丰富的可用类型。举例来说,一个文本编辑器可能为被拷贝的数据提供HTML(富文本)和纯文本表示,支持富文本的应用程序应该选择HTML表示,而只支持纯文本的应用程序则应该选择纯文本的表示。


变化记数

变化记数是每个粘贴板都有的变量,它随着每次粘贴板内容的变化而递增—特别是发生增加、修改、或移除数据项的时候。应用程序可以通过考察变化记数(通过
changeCount
属性)来确定粘贴板的当前数据是否和最后一次取得的数据相同。每次变化记数递增时,粘贴板都会向对此感兴趣的观察者发送通告。


选择和菜单管理

在拷贝或剪切视图中的某些内容之前,必须首先选择“某些内容”。它可能是一些文本、一个图像、一个URL、一种颜色、或者其它类型的数据,包括定制对象。为了在定制视图中实现拷贝-和-粘贴行为,您必须自行管理该视图中对象的选择。如果用户通过特定的触摸手势(比如双击)来选择视图中的对象,您就必须处理该事件,即在程序内部记录该选择(同时取消之前的选择),可能还要在视图中指示新的选择。如果用户可以在视图中选择多个对象,然后进行拷贝-剪切-粘贴操作,您就必须实现多选的行为。


请注意:触摸事件及其处理技巧在“触摸事件”部分进行讨论。


当应用程序确定用户请求了编辑菜单时—可能就是一个选择的动作—您应该执行下面的步骤来显示菜单:


调用
UIMenuController

sharedMenuController
类方法来取得全局的,即菜单控制器实例。


计算选定内容的边界,并用得到的边界矩形调用
setTargetRect:inView:
方法。系统会根据选定内容与屏幕顶部和底部的距离,将编辑菜单显示在该矩形的上方或下方。


调用
setMenuVisible:animated:
方法(两个参数都传入
YES
),在选定内容的上方或下方动画显示编辑菜单。


程序清单3-4演示了如何在
touchesEnded:withEvent:
方法的实现中显示编辑菜单(注意,例子中省略了处理选择的代码)。在这个代码片段中,定制视图还向自己发送一个
becomeFirstResponder
消息,确保自己在随后的拷贝、剪切、和粘贴操作中是第一响应者。

程序清单3-4  显示编辑菜单


- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *theTouch = [touches anyObject];
 
    if ([theTouch tapCount] == 2  && [self becomeFirstResponder]) {
 
        // selection management code goes here...
 
        // bring up editing menu.
        UIMenuController *theMenu = [UIMenuController sharedMenuController];
        CGRect selectionRect = CGRectMake(currentSelection.x, currentSelection.y, SIDE, SIDE);
        [theMenu setTargetRect:selectionRect inView:self];
        [theMenu setMenuVisible:YES animated:YES];
 
    }
}

初始的菜单包含所有的命令,因此第一响应者提供了相应的
UIResponderStandardEditActions
方法的实现(
copy:

paste:
等)。但是在菜单被显示之前,系统会向第一响应者发送一个
canPerformAction:withSender:
消息。在很多情况下,第一响应者就是定制视图的本身。在该方法的实现中,响应者考察给定的命令(由第一个参数传入的选择器表示)是否适合当前的上下文。举例来说,如果该选择器是
paste:
,而粘贴板上没有该视图可以处理的数据,则响应者应该返回
NO
,以便禁止粘贴命令。如果第一响应者没有实现
canPerformAction:withSender:
方法,或者没有处理给定的命令,该消息就会进入响应者链。

程序清单3-5展示了
canPerformAction:withSender:
方法的一个实现。该实现首先寻找和
copy:

copy:
、及
paste:
选择器相匹配的消息,并根据当前选择的上下文激活或禁用拷贝、剪切、和粘贴菜单命令。对于粘贴命令,还考虑了粘贴板的内容。

程序清单3-5  有条件地激活菜单命令


- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
    BOOL retValue = NO;
    ColorTile *theTile = [self colorTileForOrigin:currentSelection];
 
    if (action == @selector(paste:) )
        retValue = (theTile == nil) &&
             [[UIPasteboard generalPasteboard] containsPasteboardTypes:
             [NSArray arrayWithObject:ColorTileUTI]];
    else if ( action == @selector(cut:) || action == @selector(copy:) )
        retValue = (theTile != nil);
    else
        retValue = [super canPerformAction:action withSender:sender];
    return retValue;
}

请注意,这个方法的最后一个
else
子句调用了超类的实现,使超类有机会处理子类忽略的命令。

还要注意,操作一个菜单命令可能会改变其它菜单命令的上下文。比如,当用户选择视图中的所有对象时,拷贝和剪切命令就应该被包含在菜单中。在这种情况下,虽然菜单仍然可见,但是响应者可以调用菜单控制器的
update
方法,使第一响应者的
canPerformAction:withSender:
再次被调用。


拷贝和剪切选定的内容

当用户触碰编辑菜单上的拷贝或剪切命令时,系统会分别调用响应者对象的
copy:

cut:
方法。通常情况下,第一响应者—也就是您的定制视图—会实现这些方法,但如果没有实现的话,该消息会按正常的方式进入响应者链。请注意,
UIResponderStandardEditActions
非正式协议声明了这些方法。


请注意:由于
UIResponderStandardEditActions
是非正式协议,应用程序中的任何类都可以实现它的方法。但是,为了使命令可以按缺省的方式在响应者链上传递,实现这些方法的类应该继承自
UIResponder
类,且应该被安装到响应者链中。



copy:

cut:
消息的响应代码中,您需要把和选定内容相对应的对象或数据以尽可能多的表示形式写入到粘贴板上。这个操作涉及到如下这些步骤(假定只有一个的粘贴板数据项):


标识或取得和选定内容相对应的对象或二进制数据。

二进制数据必须封装在
NSData
对象中。其它可以写入到粘贴板的对象必须是属性列表对象—也就是说,必须是下面这些类的对象:
NSString

NSArray

NSDictionary

NSDate

NSNumber
、或者
NSURL
(有关属性列表对象的更多信息,请参见属性列表编程指南)。


可能的话,请为对象或数据生成一或多个其它的表示。

举例来说,在之前提到的为选定图像创建
UIImage
对象的步骤中,您可以通过
UIImageJPEGRepresentation

UIImagePNGRepresentation
函数将图像转换为不同的表示。


取得粘贴板对象。

在很多情况下,使用通用粘贴板就可以了。您可以通过
generalPasteboard
类方法来取得该对象。


为写入到粘贴板数据项的每个数据表示分配一个合适的UTI。

这个主题的讨论请参见“粘贴板的概念”部分。


将每种表示类型的数据写入到第一个粘贴板数据项中:


向粘贴板对象发送
setData:forPasteboardType:
消息可以写入数据对象。


向粘贴板对象发送
setValue:forPasteboardType:
消息可以写入属性列表对象。


对于剪切(
cut:
方法)命令,需要从应用程序的数据模型中移除选定内容所代表的对象,并更新视图。

程序清单3-6展示了
copy:

cut:
方法的一个实现。
cut:
方法调用了
copy:
方法,然后从视图和数据模型中移除选定的对象。注意,
copy:
方法对定制对象进行归档,目的是得到一个
NSData
对象,以便作为参数传递给粘贴板的
setData:forPasteboardType:
方法。

程序清单3-6  拷贝和剪切操作


- (void)copy:(id)sender {
    UIPasteboard *gpBoard = [UIPasteboard generalPasteboard];
    ColorTile *theTile = [self colorTileForOrigin:currentSelection];
    if (theTile) {
        NSData *tileData = [NSKeyedArchiver archivedDataWithRootObject:theTile];
        if (tileData)
            [gpBoard setData:tileData forPasteboardType:ColorTileUTI];
    }
}
 
- (void)cut:(id)sender {
     [self copy:sender];
     ColorTile *theTile = [self colorTileForOrigin:currentSelection];
 
     if (theTile) {
         CGPoint tilePoint = theTile.tileOrigin;
         [tiles removeObject:theTile];
          CGRect tileRect = [self rectFromOrigin:tilePoint inset:TILE_INSET];
         [self setNeedsDisplayInRect:tileRect];
     }
}


粘贴选定内容

当用户触碰编辑菜单上的粘贴命令时,系统会调用响应者对象的
paste:
方法。通常情况下,第一响应者—也就是您的定制视图—会实现这些方法,但如果没有实现的话,该消息会按正常的方式进入响应者链。
paste:
方法在
UIResponderStandardEditActions
非正式协议中声明。


paste:
 消息的响应代码中,您可以从粘贴板中读取应用程序支持的表示,然后将被粘贴对象加入到应用程序的数据模型中,并将新对象显示在用户指定的视图位置上。这个操作涉及到如下这些步骤(假定只有单一的粘贴板数据项):


取得粘贴板对象。

在很多情况下,使用通用粘贴板就可以了,您可以通过
generalPasteboard
类方法来取得该对象。


确认第一个粘贴板数据项是否包含应用程序可以处理的表示,这可以通过调用
containsPasteboardTypes:
方法,或者调用
pasteboardTypes
方法并考察其返回的类型数组来实现。

请注意,您在
canPerformAction:withSender:
方法的实现中应该已经执行过这个步骤。


如果粘贴板的第一个数据项包含应用程序可以处理的数据,则可以调用下面的方法来读取:



dataForPasteboardType:
,如果要读取的数据被封装为
NSData
对象,就可以使用这个方法。



valueForPasteboardType:
,如果要读取的数据被封装为属性列表对象,请使用这个方法(请参见“拷贝和剪切选定的内容”部分)。


将对象加入到应用程序的数据模型中。


将对象的表示显示在用户界面中用户指定的位置上。

程序清单3-7是
paste:
方法的一个实现实例,该方法执行与
cut:

copy:
方法相反的操作。示例中的视图首先确认粘贴板是否包含自身支持的定制表示数据,如果是的话,就读取该数据并将它加入到应用程序的数据模型中,然后将视图的一部分—当前选定区域—标识为需要重画。

程序清单3-7  将粘贴板的数据粘贴到选定位置上


- (void)paste:(id)sender {
     UIPasteboard *gpBoard = [UIPasteboard generalPasteboard];
     NSArray *pbType = [NSArray arrayWithObject:ColorTileUTI];
     ColorTile *theTile = [self colorTileForOrigin:currentSelection];
     if (theTile == nil && [gpBoard containsPasteboardTypes:pbType]) {
 
        NSData *tileData = [gpBoard dataForPasteboardType:ColorTileUTI];
        ColorTile *theTile = (ColorTile *)[NSKeyedUnarchiver unarchiveObjectWithData:tileData];
         if (theTile) {
             theTile.tileOrigin = self.currentSelection;
             [tiles addObject:theTile];
             CGRect tileRect = [self rectFromOrigin:currentSelection inset:TILE_INSET];
             [self setNeedsDisplayInRect:tileRect];
         }
     }
}


消除编辑菜单

在您实现的
cut:

copy:
、或
paste:
命令返回后,编辑菜单会被自动隐藏。通过下面的代码使它保持可见:


[UIMenuController setMenuController].menuVisible = YES;

系统可能在任何时候隐藏编辑菜单,比如当显示警告信息或用户触碰屏幕其它区域时,编辑菜单就会被隐藏。如果您有某些状态或屏幕显示需要依赖于编辑菜单是否显示的话,就应该侦听
UIMenuControllerWillHideMenuNotification
通告,并执行恰当的动作。


posted on 2011-05-25 16:38  一个人的天空@  阅读(1675)  评论(2编辑  收藏  举报