1.13 解决方案:把滚动视图中的内容拖曳到外面
iOS所提供的手势识别器的功能确实很丰富,但并不总是能够满足开发者的需要。比方说,有个可以水平滚动的视图,里面包含许多相邻的图像视图ImageView,用户可以左右滚动这个大视图来查看其中的全部内容。现在,假设我们要实现一个功能,令用户可以把视图中的ImageView拖曳到滚动区域下方的空白区域里。想实现此功能,我们需要辨识发生在每个子视图身上的向下触摸操作(downward touch,其移动的方向与大视图的滚动方向相垂直)。
开发者Alex Hosgrove在构建一款应用程序时,就遇到了这个问题,那款程序里面的东西有点像吸附在冰箱门上的一套磁铁字母。用户可以把字母向下拖放到工作区中,然后可以操作并排列其所选的字母。实现这种功能的时候,要解决两个问题:一是触摸操作属于谁来管,二是识别出了向下触摸之后应该怎样处理。
滚动视图(Scroll View)中的子视图都需要关注触摸操作。如果检测到了向下的手势,那么程序就应该产生新对象,若是检测到横向的手势,则应该水平滚动滚动视图中的内容。为了使滚动视图及其子视图都能响应用户的操作,我们应该在程序内部共享触摸信息。经由UIGestureRecognizerDelegate即可实现这一点。
开发者通过UIGestureRecognizerDelegate可以实现同步识别,也就是说,两个手势识别器可以同时运作。要想实现这一点,只需令相关的类遵从UIGestureRecognizerDelegate协议,并向其中添加下面这个简单的委托方法:
由于我们不能重新设置滚动视图的委托,所以必须令滚动视图里面的子视图所对应的类遵循UIGestureRecognizerDelegate协议,并在其中实现上述方法。
要想解决上面所提的第二个问题,也就是如何令滑动这个动作产生出拖曳效果,需要从整个触摸生命期的角度来考虑。凡是能产生新对象的触摸操作,一开始都是一个垂直方向上的拖曳,但只要新对象创建出来了,它就变成了拖动手势。所以,此处使用拖动手势识别器更为合适,因为假如使用滑动手势识别器的话,那么等识别出来的时候,触摸操作的生命期已经结束了。
为了解决第二个问题,解决方案1-12在内置的手势探测代码中实现了对方位移动[1]的检测。从最后的结果来看,这种打破常规的做法收到了实效。这是因为程序检测到滑动之后,底层的拖动手势识别器依然会继续运作。如此一来,用户就可以继续移动刚才拖曳出来的物件,而无须先把手指抬起来,然后再重新触摸它。
解决方案1-12 把滚动视图中的物件拖曳到外面
解决方案1-12中的实现代码在认定滑动操作时,所用的标准是垂直方向至少扫过16个像素,而且左右两侧的偏离不能超过12个像素。如果代码检测到了这种向下的滑动,那么它就把新建的DragView对象(本章前面曾经用过DragView类)添加到屏幕中,使该对象可以随着用户的触摸而完成接下来的拖动手势交互过程。
一旦识别出向下的滑动操作,PullView类就把自己的gestureWasHandled标注成TRUE,意思是说,自己已经把这个滑动处理过了,同时,它还会在继续处理本次拖动事件的过程中禁用ScrollView。这样的话,用户所拖曳的子视图就可以完全掌控当前拖动手势的交互过程,而Scroll View也就不用再处理接下来的触摸移动了。
[1] directional-movement,也就是上、下、左、右四个方向上的移动。——译者注
1.14 解决方案:实时的触摸反馈
你有没有给iOS应用程序录制过demo呢?如果要录制的话,总会遇到个两难的问题。一种办法是对着手机屏幕来录制,这样做的缺点是可能会把手机屏幕上反射的影像录进去,而且用户的手还可能会挡住屏幕。另一种做法是使用Reflection(http://reflectionapp.com)之类的工具,但是这些工具只能把直接发生在iOS设备屏幕上的内容录下来,没有办法录下用户触摸应用程序的情况,也没办法专门对某个部分进行特写。
解决方案1-13提供了一套简单的类(它们统称为TOUCHkit),使程序可以具备实时的触摸反馈层,以供开发者向他人演示该程序的用法。有了这项功能之后,你既可以看到要录制的屏幕,也可以看到引发互动效果的触摸操作,而那些互动效果正是你想要展示给大家看的。TOUCHkit提供了一种方式,使开发者可以编译出两个版本的程序,一种供普通场合使用,另一种供演示用。无论制作哪个版本,都不用改变核心的应用程序。你只需简单切换一下,就能构建出这两种不同用途的版本。
为了说明TOUCHkit的用法,笔者找了一款由苹果公司所制作的标准演示程序,并把它的范例代码连同本节的范例代码一并放在解决方案1-13之中。学会这套工具包的用法之后,你基本上就可以把它运用到各种标准的应用程序上面了。
1.14.1 启用触摸反馈效果
若想为现有程序添加触摸反馈效果,只需切换TOUCHkit中的相关特性,这不会影响到普通的应用程序代码。设定好相关标志之后,就可以编译并构建应用程序了,用户在触摸这种程序时,屏幕上会出现叠加效果,开发者可以拿这种程序做演示用。部署到App Store的时候,则应该将该标志禁用。禁用之后,应用程序的行为就恢复正常了,而且开发者也无须担心程序会执行App Store所认定的不安全调用:
本条解决方案假定开发者所构建的程序是只有一个主窗口的标准应用程序。编译的时候,TOUCHkit会用一个自定义的类来取代窗口类,而自定义的类可以捕获并复制所有的触摸操作,这就使得应用程序能够用气泡标志来表示用户的触摸点。
另外,开发者还需要做一次非常重要的代码修改操作,不过所涉及的代码量非常少。在应用程序委托类中,需要定义WINDOW_CLASS,构建iOS应用程序的窗口时会用到它
1.14.2 拦截并转发触摸事件
TOUCHkit之所以能在屏幕上实现触摸反馈效果,是因为它拦截了触摸事件,并据此在应用程序通常的界面上方创建了叠加图样,然后又把事件转发给了应用程序。TOUCHkit的视图位于程序原来的界面之上,而自定义的窗口类则可以抓取用户的触摸事件,并将其以圆圈的形式展示到TOUCHkit的视图上面。然后,它会把事件转发给程序,这样看上去就好像是用户直接在和普通的UIWindow交互一样。本条解决方案将使用事件转发来实现这一点。
事件转发是通过调用另外一个事件处理程序来完成的。TOUCHOverlayWindow类覆写了UIWindow的sendEvent:方法,以便把触摸效果绘制到屏幕上面,然后,该方法会调用超类的同名方法,以便将控制权交还给普通的响应者链。
下面的实现代码是根据苹果公司的《Event Handling Guide for iOS》而编写的。它会把与当前事件有关的全部UITouch都收集起来,这样一来,无论是多点触摸还是单点触摸,我们都能够应对。然后,该方法会将其派发给TOUCHkit的TOUCHkitView,最后,它调用通常的UIWindow sendEvent:实现代码,将事件转给视窗。
1.14.3 实现TOUCHkit的TOUCHkitView类
TOUCHkit的TOUCHkitView是个简单而清晰的UIView单例。当应用程序首次请求访问该类的共享实例时,它才会创建sharedInstance,创建的时候,该类会把sharedInstance添加到程序的key window之中。由于TOUCHkitView的userInteractionEnabled标志是NO,所以即便我们通过标准的touchesBegan、touchesMoved、touchesEnded及touchesCancelled等事件回调方法处理了这些事件,它们也依然能够继续向后传播,并到达TOUCHkitView下方的界面里,使得
系统可以将其纳入响应者链。
处理触摸事件的方法会在每个触摸点上绘制一个圆圈,并创建一个指向theTouches的强指针(strong pointer),待绘制完成之后,再清空该指针。解决方案1- 13详细介绍了处理该功能的回调和绘制方法。
解决方案1-13 创建TOUCHkitView类,以绘制触摸反馈效果
1.15 解决方案:向视图中添加菜单
UIMenuController类使得开发者可以向身为第一响应者的任何物件之中添加弹出式菜单。一般来说,菜单是与文本视图(Text View)及文本框(Text Field)结合起来使用的,它使得用户能够执行选取、拷贝及粘贴等操作。另外,开发者也可通过菜单来提供与互动式屏幕元件有关的操作,例如,可以像本章这样,通过菜单来操作程序里的小DragView。图1-6演示了一套自定义的菜单。在解决方案1-14中,如果用户长按某一朵花,程序就会展示出一组菜单。用户可以经由其中的菜单项对当前的DragView执行缩放、旋转或隐藏等操作。
本条解决方案将要演示如何获取共享的UIMenu-Controller,以及如何向其中添加菜单项。开发者需要设置菜单的目标矩形(一般来说,需要传入自己的bounds以及展示该菜单的视图),并调整菜单的箭头方向,然后调用菜单的update方法,使我们对菜单所做的修改能够生效。执行完这些操作之后,就可以把菜单设为可见状态了。
菜单项也有其标准的目标-动作回调机制,但我们并不需要直接设置目标。目标总是身为第一响应者的视图。本条解决方案没有在响应者上面执行canPerformAction:withSender:检测,不过,若是你的程序里面某些视图可以支持特定的动作,而另外一些视图不能,那么就需要添加这种检测了。是否能支持某项菜单所对应的操作一般和视图的状态有关。比方说,假如视图里面没有内容可供拷贝,那么我们就不应该提供拷贝(copy)命令。
解决方案1-14 向互动式的视图中添加菜单
1.16 小结
UIView及其底层的CALayer为用户能够在屏幕上看到的各种组件提供了支持。触摸功能使得用户可通过UITouch类及手势识别器来直接操作视图。正如本章范例
程序所示,即便在最为基本的形态之下,基于触摸的界面也提供了一套既容易实现又很灵活的接口。读者学会了怎样在屏幕中移动视图,也学会了如何对其移动行为施加限制。我们还讲了怎样通过对触摸操作的检测来判断视图是否应该响应这些操作。此外,大家还知道了如何在视图上面实现绘画功能,以及如何把手势识别器与视图相关联,以解读并响应手势。笔者把本章各个解决方案所体现出来的设计思路总结起来,读者在继续学习下一章之前,可以先思考一下这些问题。
·程序要直观。iOS设备的屏幕对触摸支持得非常好。所以,你应该考虑令用户可以来回拖曳屏幕上面的物件,并且使程序能够随着用户手指的移动而画线。这样的话,程序就显得更加真实了,而且也把iOS平台的互动性质体现出来了。
·用户通常是可以十指并用的。iPad提供了相当强大的屏幕操作能力。所以,不要总是设计只能用一个手指来操作的界面,而是应该在屏幕大小容许的前提下,为程序添加多点触摸式的互动操作。
·要扎实地掌握Quartz图形绘制技术以及Core Animation API,这对你很有帮助。开发者可以用drawRect:方法在UIView里面绘制出各式各样的内容,包括文本、贝塞尔曲线以及涂鸦效果等。
·如果Cocoa Touch所提供的手势识别器不能满足你的特定需求,就自己编写一个。这不是非常困难的事情,你应该尽量把代码写得完备一些,以便将自定义识别器可能历经的各种状态都涵盖在内。
·尽可能使程序支持多点触摸,如果你想令多位用户能够同时触摸程序的话,就更应该如此了。不要把程序局限在单人单指触摸这种交互方式上面,有时只需多用一点编程技巧,就可以使多位用户同时操作应用程序。
·多多探索。在应用程序中实现直接操纵是有很多方式的,而本章只讲了有限的几种。请以本章为出发点,探索UITouch类的其他各种能力。
第2章 构建并使用控件
UIControl类是很多iOS互动式元件的基础,比如按钮、文本框、滑杆和开关等。这些视图对象都是从同一个祖先类继承下来的,除此以外,它们之间还有许多共同点。所有控件都使用类似的布局范式(layout paradigm)及目标-动作触发器。只学会创建一种控件就够了:无论这种控件多么特殊,我们都能由此明白所有控件的工作原理。控件的外观及功能也许各有不同,但设计这些控件时所依循的模式却是相同的。本章要讲解各种控件及其用途。你将学会以多种方式来构建并定制控件。笔者会给出与控件有关的各种解决方案,供你在自己的程序里复用,这些解决方案有的比较简单,有的比较复杂。
2.1 UIControl类
iOS的控件是程序库中预先构建好的一批对象,它们是为了实现用户交互功能而设计的。这些控件包括按钮、文本框、滑杆、开关以及其他一些由苹果公司所提供的对象。控件所扮演的角色就是把用户的操作转化成回调。用户通过触摸及操纵控件来与应用程序交互。
在控件类的继承树里,UIControl类是树根。控件类都是UIView的子类,它从UIView中继承了与显示及布局有关的全部属性。UIView的子类添加了一套响应机制,该机制可以强化视图,使其能够具备交互性。
当用户操作控件的界面时,每个控件都有办法实现消息派发。控件使用目标-动作模式来发送消息。定义新控件的时候,开发者需要指明消息的接收方(也就是目标)以及所发送的消息(也就是动作),还要指定发送这些消息的时机(也就是触发条件,比方说,当用户在某个控件范围内完成触摸操作时,就触发某消息)。
2.1.1 目标-动作模式
目标-动作设计模式提供了一条可以响应用户交互的底层途经。我们基本上只会在UIControl类的子类中碰到它。经由目标-动作模式,开发者可以在发生了特定的用户事件时,命令控件给指定的对象发送消息。比方说,你可以指明当用户按下按钮或调整滑杆时,哪个对象应该接收相关的选择子(selector)。
开发者可以随意提供选择子。由于系统并不在运行期检查它,所以编写代码时需要小心。假如指定的选择子还未声明,那么编译器会发出警告,有时这能够提醒开发者注意选择子中的拼写错误,从而防止程序在运行时崩溃。下面这段代码设置了一对目标-动作组合,当用户在按钮内松开手指时,程序就会调用playSound:这个选择子。假如目标(也就是self)没有实现那个方法,程序在运行的时候就会因为未定义的方法调用错误而崩溃:
目标-动作模式并不像委托那样去遵循一套预先建立好的方法规范。与委托及其所需的协议不同,系统并不强迫目标对象一定要实现playSound:。开发者应该自
己来确保回调操作一定能够执行某个已经实现好了的方法。在设置目标-动作组合之前,谨慎的程序员会先检查目标对象到底有没有实现指定的选择子。例如:
对于标准的UIControl来说,在设定了目标-动作组合之后,系统一般会给选择子传递0个、1个或2个参数。假如传递了参数的话,那么参数就是交互对象(interaction object,也就是用户所操作的按钮、滑杆或开关等)以及表示用户输入的UIEvent对象。系统可以单单把交互对象传递给选择子,也可以把交互对象和相关事件一并传过去。在前述范例代码中,选择子只接收一个参数,就是用户正在点击的UIButton实例。这种自我引用式的写法使得系统在回调选择子的时候,可以把用户所触发的对象以参数的形式传过去,从而令开发者能够编写出更为通用的动作代码,这种代码知道究竟是哪个控件引发了回调。
2.1.2 控件的种类
在UIControl家族中,系统所提供的成员有按钮、分段选择控件(segmented control)、开关、滑杆、页面控制控件以及文本框。这些控件都可以在Interface Builder(简称IB)的Object Library里面找到,如图2-1所示(按下Command-Control-Option-3组合键,或者点击View>Utilities>Show Object Library菜单项)。
2.1.3 控件事件
控件主要响应三类事件:基于触摸的事件、基于值的事件,以及基于编辑的事件。表2-1列出了控件所能响应的每种事件。
在大多数情况下,事件的用法都可以概括如下。按钮使用触摸事件,所有与按钮相关的操作几乎都可以通过UIControlEventTouchUpInside这种事件类型来处理,而这也正是IB创建连接(connection)时所采用的默认事件类型。如果用户调整了分段选择控件、开关、滑杆或页面控制控件,那么就会引发值事件(比方说UIControlEventValueChanged)。刷新表格内容所用的刷新控件也能触发值事件。当用户切换、滑动或点击那些控件的时候,控件的值就会改变。UITextField对象会引发编辑事件。当用户在文本框范围内或范围外点击以及修改文本框中的内容时,就会引发这类事件。
与iOS的所有GUI元件一样,你既可以通过Xcode的IB界面来排布控件,也可以用代码的方式来实例化它们。本章会讨论一些与IB有关的方案,不过我们主要还是关注基于代码的解决方案。只要学会了用IB来排布控件,以后无论遇到什么控件,都可以用相同的办法来操作。我们可以把控件拖放到界面里,通过检查器(inspector)及Auto Layout(自动布局)约束对其进行定制,并将它与其他IB对象相连。