厦门市网站建设_网站建设公司_HTML_seo优化
2025/12/28 2:32:33 网站建设 项目流程

1.7 解决方案:针对位图的触摸测试

解决方案1-5所用的触摸判定方式非常直观,它只做了一些简单的几何运算,但不巧的是,大部分视图都不是解决方案1-5所演示的样子。比方说,对于图1-1中的花朵,其边界就是不规则的,而且其透明度也有变化。如果图形比较复杂,我们就必须针对位图来做测试,才能判断是否发生了触摸。对基于图像的视图(imagebased view)来说,位图是一种按字节排列的信息,它描述了该视图的内容,从而使开发者可以判断出用户到底是触摸了图中不透明的部分(solid portion),还是触摸了透明的部分,如果是后者,就应该转而判断位图下方的视图是否受到触摸。

解决方案1-6从UIImageView里面提取了一幅图像的位图。它假定受测的视图是以像素来描述其中的图像的。假如你要扭曲视图的话(一般可以通过调整框架(frame)的大小或运用坐标变换来实现扭曲效果),那么需要修改判定触摸所用的算式。开发者可以通过CGPointApplyAffineTransform()来对CGPoint执行变换,以处理由于缩放及旋转而带来的坐标变更。为了简化判断过程,也为了避免繁杂的数学运算,我们把视图的图像与其实际像素之比定为1∶1。你可以把待测试的像素取出来,测试其透明程度,并据此判断用户是否点击了视图中不透明的部分。

解决方案1-6 根据位图像素的不透明程度来测试触摸

本例所使用的分界值是85。就是说,受测像素的不透明程度至少是33%(也就是大约85/255),我们才能认定用户点击了这个视图。我们所编写的pointInside:方法会将不透明度低于33%的像素视为透明。这个值是笔者随意选取的。你可以根据自己GUI的实际需求来调整该值(当然也可以换用另外的判定算法)。

提示 除非你要实现完美的像素级触摸检测,否则可以先把位图缩小,然后再用修改过的算式去判断,这样占用的内存会少一些。

1.8 解决方案:根据触摸情况在屏幕上绘制内容

开发者可以在UIView的范围内实现直接屏幕绘制(direct onscreen drawing)。它的drawRect:方法提供了一种低阶的方式,令我们能够使用Quartz 2D API来创建及显示任意元件,并直接向屏幕中绘制内容。将触摸与绘制结合起来,就能构建出既直观而又容易操控的界面。

解决方案1-7把手势与drawRect相结合,实现出了基于触摸的绘画功能。用户触摸屏幕时,TouchTrackerView类会随着用户的手指而构建贝塞尔曲线。为了在用户触摸屏幕的过程中实现绘画功能,我们令程序的touchesMoved:withEvent:方法调用setNeedsDisplay,而setNeedsDisplay又会触发drawRect:,后者将根据收集到的贝塞尔曲线信息在视图中绘制相关图形。图1-3中的界面演示了用此方式创建出的一条路径。

解决方案1-7 在UIView中实现基于触摸的绘画功能

虽说我们可以用手势识别器来改写本例,但这样做没有实际意义。本程序中的触摸信息并没有太大的用途,我们只不过是用其来创建一条平滑的轨迹。基本的响应者方法(也就是touchesBegan、touchesMoved等方法)完全可以应付路径的创建及管理等事宜。

本例创建出来的是连续轨迹。该程序并不响应静止的触摸事件。假如读者想扩充这个范例程序,使其具备画点或画符号的能力,需要自己去添加相关的行为代码。

1.9 解决方案:令绘制效果变得平滑

用户所使用的设备各有不同,其设备在同一时间所能处理的运算量也不一样,这就导致捕捉到的触摸事件可能要比预期的粗略一些。与生活中轮番握手时的情况类似,触摸事件的采样率通常受限于CPU的能力。我们可以在点之间进行插值(interpolating),用平滑算法(smoothing algorithm)来缓解这些限制。通过图1-4我们可以看到,用采集到的触摸点直接绘制出来的曲线与经过平滑算法处理过的曲线其圆润程度是不同的。

Catmull-Rom样条插值法(Catmull-Rom spline)可以在关键点(key point)之间创建连续曲线(continuous curve)。该算法可以确保一开始所提供的每个点都出现在最后绘制好的曲线上面。运算好的路径能够保持原始路径的形状,而开发者则可以决定在每一对参考点(reference point)之间插入多少个中间点。我们应该在算法的计算量和所能达到的平滑程度之间进行权衡。加入的点越多,CPU的资源消耗量就越大。运行本章附带的范例代码之后你就会发现,只要稍微提升一下平滑程度,就能明显改善绘制效果,即便在新款设备上也是如此。最新版iPad的响应能力很强,如果刚开始就想绘制一条有棱有角的曲线,还真不是那么容易。

解决方案1-8演示了如何从现有的贝塞尔曲线中提取一些点,以便运用样条插值法创建出平滑的效果。Catmull-Rom算法每次使用四个点计算第二点和第三点之间的中间值[1],而开发者可以通过granularity参数来指定两个点之间到底应该插入多少个中间点。

解决方案1-8 用Catmull-Rom样条插值法创建平滑的贝塞尔曲线

解决方案1-8只不过演示了一种实时几何处理算法,实际上,计算几何学里面还有很多算法都能以类似的方式运用到应用程序中。

提示 Erica Sadun所著的《iOS Drawing:Practical UIKit Solutions》(Addison-Wesley,2013年)一书里还讲了很多与getPointsFromBezier相似的UIBezierPath工具。若想了解更多优秀的图形算法,请访问www.graphicsgems.org网站,查看由Academic Press所出版的《Graphics Gems》系列图书,其中也讲了很多先进的平滑算法。

[1] 这里的第二点和第三点是Catmull-Rom样条插值法所使用的特殊称谓,对应于代码中的p1和p2。——译者注

1.10 解决方案:启用多点触摸

在UIView实例中启用了多点触摸式的交互之后,当用户以几根手指同时触摸屏幕时,iOS就可以侦测到这些触摸操作并对其进行响应。把UIView的multipleTouchEnabled属性设为YES,或在自己的视图类中覆写isMultipleTouchEnabled方法,即可启用多点触摸。启用之后,系统在传给触摸回调方法的触摸事件里面,就会放入多个触摸对象。在触摸事件的设置中,如果有多于一个的元素,就说明需要处理多点触摸。

从理论上来说,iOS支持任意多个点的同时触摸。你可以在iPad上面运行解决方案1-9,尽可能用多根手指同时触摸屏幕,看看它的上限是多少。实际的上限以后可能会有变化,所以笔者不打算在这个解决方案里面给出具体的个数。

当年iPhone首次支持多点触摸的时候,开发者们还没有预见到把多点触摸同多用户操作结合起来其实可以实现出一些自由而灵活的功能。将多点触摸添加到游戏及其他应用程序里面,不仅可以扩大手势的种类,而且还能创造出新颖而优秀的多用户操作体验,在配有大屏幕的iPad上面更是如此。只要多点触摸符合应用程序的需求,并且对程序有帮助,就可以考虑引入该特性。

多点触摸并不会按照用户的手来分组。比方说,当用户左右两手各用一根手指触摸屏幕时,系统无法判断出某个触摸点对应于哪只手。而且触摸对象的顺序也是随意排列的。就单个的触摸事件来说,在按下手指、移动直至松开的过程中,触摸对象与手指之间的对应次序保持不变(说得更具体些,同一个触摸对象在内存中的地址保持不变),虽说如此,但是用户下次触摸屏幕时,触摸对象与手指之间不一定还能保持这套对应关系。如果需要把这些触摸对象分辨清楚,那么可以像本条解决方案这样,构建一份以触摸对象为索引的字典。

你要是知道iOS还支持多于两个手指的多点触摸,是不是会觉得这个功能还不错呢?实际上,如果一次用三根或三根以上的手指来触摸屏幕,那么系统很有可能就会漏掉某些手指的触摸操作。假如用两根以上的手指来操作程序,那么很难以编程的方式追踪到平滑的手势。因此,在处理多点触摸这种操作方式时,不要执著于对手势的解析,而是应该把它理解成一系列在限定的时间内发生且各自独立的交互操作。你应该把每一个触摸对象都当成各自独立的东西来看待,并分别处理它们。

解决方案1-9通过设定multipleTouchEnabled属性来为UIView添加多点触摸功能,并记录下每根手指所画的线条。该程序遵照苹果公司的开发建议,记录下每个触摸对象在内存中的物理地址,并且不使用指向这些触摸对象的指针,也不对其做保留(retain)。

解决方案1-9 把用户所画的各条线收集起来,以实现叠加式绘图

本例所采用的做法显然是比较奇怪的,不过这套办法在各个版本的SDK中都能照常运作。这是因为在触摸-移动-释放这个生命期中,每个UITouch对象都驻留在其各自的内存地址处。苹果公司并不建议对UITouch实例做保留,所以,在本例中,我们把这些触摸对象转为整数值,并用这些整数值来作为字典的键。

请注意,每当发生新的触摸操作时,系统就会调用touchesBegan:withEvent:来开启一段新的生命期,这与目前仍在进行中的触摸操作是互不干扰的,这些操作依然可以进入其各自的移动、结束及取消等阶段。开发者在编写代码时应该考虑到这一点。

这条解决方案扩充了原有的解决方案1-7。每次触摸操作都能独自产生一条贝塞尔路径,而视图的drawRect方法则会把这些路径绘制出来。解决方案1-7在每次触摸操作的生命期结束之后,如果发现有新的触摸操作,那么就会清除原有的屏幕内容,并开始绘制新的曲线。这对于一款仅用于演示的应用程序来说,是合适的,但若想创建一款标准的绘图程序,就不能这么做了,而是应该把新绘制的内容叠放到屏幕里原有的内容之上。

解决方案1-9不会擦除原有的内容,新的需要来清空此数组。这个解决方案采用稍微淡一些的颜色来绘制用户正在画的线,以便与strokes数组中已经画好的路径相区别。

提示 在苹果公司的开发者网站上,可以找到很多Core Graphics/Quartz 2D资源。另外还有许多论坛、邮件列表及源代码范例,它们虽然不专门针对iOS,但也都是非常有价值的资源,你可以由此来扩充自己的iOS Core Graphics知识。

1.11 解决方案:检测圆圈手势

对于iOS这种直接操纵界面来说,开发者可能会觉得大部分用户都只是简单地点击屏幕上的物件。但是,有非常多的人希望程序能够支持圆圈手势,开发者实现了这种手势之后,用户就可以拿手指圈选屏幕上的物件了。有读者希望本书给出相关的解决方案,于是,笔者编写了解决方案1-10,实现了一个相对比较简单的圆圈手势检测器,其效果如图1-5所示。

在实现该程序的时候,笔者采用了几个测试步骤。首先做时间测试(time test),以确保手势不能执行得太慢。圆圈手势应该是一种必须快速画出的手势。接下来做弯曲测试(inflection test),以确保方向变化不能太过频繁。一般来说,圆圈手势的方向改变次数是四次,本程序最多允许在画圆过程中变五次方向。第三种测试是覆盖度测试(convergence test),圆的起点与终点必须足够接近,这两个点必须要有某种程度的联系。由于程序并不会向用户提供直接的视觉反馈,所以用户可能会画得稍微偏一点,于是,我们应该在检测时留有一些余地。本程序所能容忍的像素偏差比较大,这个距离大约是视图尺寸的1/3。

解决方案1-10 检测圆圈手势

最后还要执行一种测试,以判断用户的手指围绕手势的中心点旋转了多少度。程序会根据手指划过的弧来统计这个度数,标准的圆应该是360度。为了使用户在画圆的时候能把手势做得自然一些,我们扩大了对角度的容忍范围,用户最少可以少画45度,最多可以多画180度。

如果这些测试全都通过了,那么该算法就生成一个最小外接矩形(least bounding rectangle),并对原手势上的各点求几何平均值,然后把矩形中心点与均值点对齐。程序会把检测结果赋给circle变量。这套检测系统虽然不完美(读者可以运行范例代码,并想办法骗过这套检测系统),但它却能为许多iOS应用程序提供足够健壮而且相当好用的圆圈手势检测能力。

1.12 解决方案:创建自定义手势识别器

只需稍加修改,就能把解决方案1-10中的代码转换成一款可以辨识自定义手势的识别器,解决方案1-11实现了这种识别器。从UIGestureRecognizer中继承子类,即可构建出自己的圆圈手势识别器,然后,可以将其添加到自己的应用程序中。

你应该在自己的新类里面引入UIKit的UIGestureRecognizerSubclass.h。实现UIGestureRecognizer的子类时,可能需要覆写或自定义一些方法,而这些方法都声明在UIGestureRecognizerSubclass.h头文件里。在覆写某个方法时,你应该先调用该方法的原始版本,然后再执行自己的新代码,也就是说,应该先调用超类的同名方法。

手势可以分为两大类:连续手势(continuous gesture)和不连续手势(discrete gesture)。圆圈手势识别器就是不连续的。它要么可以识别出圆圈手势,要么识别不出来。而双指聚拢及拖动则属于连续手势,手势识别器会在手势的整个生命期里面不断发送相关的更新。手势识别器通过设置state属性来产生更新。

手势识别器其实就是个描述指尖状态的状态机。每个识别器刚开始都处在possible状态(UIGestureRecognizerStatePossible),对于连续手势来说,识别器会历经一系列的changed状态(UIGestureRecognizerStateChanged),而对于不连续手势来说,如果能够识别出这样的手势,那么识别器就会进入UIGestureRecognizerStateRecognized状态,若识别不出来,则进入UIGestureRecognizerStateFailed状态,如解决方案1-11所示。除非我们将状态(state)设为possible或failed,否则每次更新状态的时候,识别器都会向其目标发送动作消息。

解决方案1-11 创建UIGestureRecognizer的子类

解决方案1-11里面有几段相当长的注释,那是笔者从UIGestureRecognizerSubclass.h头文件里面拷贝过来的,感谢苹果公司撰写这些注释。它们分别解释了几个关键的方法所扮演的角色,我们在UIGestureRecognizer子类里覆写了这几个方法。reset方法会令识别器回到沉寂(quiescent)状态,以便为识别下一轮手势做准备。

与UIResponder中的相关方法类似,识别器也会在相应的时机调用touchesBegan:withEvent:等方法,这使得开发者可以在识别器里面编写代码,在触摸操作生命期中的相关时间点上执行测试。本范例程序会一直等到系统调用touchesEnded:withEvent:回调方法时,再去判断对手势的识别是成功了还是失败了,它所用的testForCircle方法与解决方案1-10中定义的方法相同。

提示 作为覆写超类方法时所应提倡的一条原则,一旦发现无法辨识出手势,我们就应该令手势识别器尽快“失败”。若是能够辨识出手势,就应当把与手势

有关的信息存储在程序的属性里面。圆圈手势识别器应该把检测到的圆圈保存起来,使用户能够知道手势出现在何处。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询