手势识别以及手势冲突处理

概述

手势识别器是处理视图中的触摸或者按压事件最简单的方法,我们可以在任意视图上附加一个或多个手势识别器。手势识别器封装了处理和解释该视图的触摸事件所需的所有逻辑,并将其与已知模式进行匹配。当触摸事件与已知模式匹配时,手势识别器会通知其关联的目标对象进行处理。手势识别器使用target-action设计模式去发送通知,如下图所示。当UITapGestureRecognizer对象在视图中检测到单指轻敲时,会调用视图所属的视图控制器的操作方法来回应。

图1-1 手势识别器通知其关联到目标对象

手势识别器有两种类型:离散型和连续型。当识别出手势后,离散型手势识别器只会调用一次其关联目标对象的操作方法。连续型手势识别器则可能会多次调用其关联目标对象的操作方法,包括手势的开始和结束以及跟踪手势细节的变化。例如,使用拖拽手势识别器时,只要触摸事件的位置发生变化,UIPanGestureRecognizer对象就会调用其关联对象的操作方法。

配置手势识别器

配置手势识别器分为以下三步:

  • 使用代码创建一个手势识别器对象,并将其附加到视图上。或者,使用storyboard,直接将手势识别器拖动到视图上。
  • 实现识别手势后目标对象要调用的操作方法。
  • 调用手势识别器对象的addTarget:action:方法将目标对象和目标对象要调用的操作方法与手势识别器关联起来。或者,使用storyboard,右键单击手势识别器将其Sent Action selector连接到目标对象来创建关联操作方法。

回应手势

与手势识别器关联的操作方法为应用程序提供了对该手势的响应。对于离散型手势,其关联的操作方法与按钮的操作方法类似。只有手势识别成功后,才会调用其关联对象的操作方法作出响应。对于连续型手势,其关联的操作方法不仅可以对手势识别成功后作出响应,还可以在识别手势之前跟踪手势细节的变化。

手势识别器的state属性可以反映当前的手势识别状态。对于连续型手势,手势识别器会在手势识别过程中将此属性值从UIGestureRecognizerStateBegan变更为UIGestureRecognizerStateChanged,手势被识别后变更为UIGestureRecognizerStateEnded

点击手势

点击手势识别器UITapGestureRecognizer对象会简要地检测一个或多个手指点击屏幕。在点击手势被识别成功前,涉及手势的手指不能从初始触摸点显著移动到其他地方,但可以配置手指必须触摸屏幕的次数。例如,可以配置点击手势识别器来检测单击,双击或者三击。

图3-1 Tap gestures

点击手势属于离散型手势,只有当点击手势识别成功后才会调用其关联对象的操作方法。由于手势可以被取消的原因有很多,所以在操作方法中回应手势前,检查state属性可以确保不会出错。

如果触摸屏幕后没有调用与手势识别器关联的对象的操作方法,请检查下列条件是否成立:

  • 视图的userInteractionEnabled属性设置为YESUIImageViewUILabel类默认将此属性设为NO
  • 点击次数等于numberOfTapsRequired属性中指定的次数。
  • 手指个数等于numberOfTouchesRequired属性中指定的个数。

长按手势

长按手势识别器UILongPressGestureRecognizer对象检测一个或者多个手指长时间触摸屏幕。在长按手势被识别成功前,涉及手势的手指不能从初始触摸点显著移动到其他地方,但可以配置手指必须触摸屏幕的次数以及长按手势的最短持续时间。手势识别器仅由触摸的持续时间而不是与其相关的力触发。

图3-2 Long-press gesture

长按手势根据触摸的持续时间来确定手势的成功或者失败,它属于连续型手势,手势关联对象的操作方法可能会随着手势状态的变化而被多次调用。长按手势识别器在用户手指停留在屏幕上一定时间后(用户手指仍旧停留在屏幕上)进入UIGestureRecognizerStateBegan状态,当触摸事件更新时,进入UIGestureRecognizerStateChanged状态,用户手指离开屏幕时,进入UIGestureRecognizerStateEnded状态。

如果触摸屏幕后没有调用与手势识别器关联的对象的操作方法,请检查下列条件是否成立:

  • 视图的userInteractionEnabled属性设置为YESUIImageViewUILabel类默认将此属性设为NO
  • 点击次数等于numberOfTapsRequired属性中指定的次数。
  • 手指个数等于numberOfTouchesRequired属性中指定的个数。
  • 触摸持续时间大于minimumPressDuration属性中指定的时间。

拖拽手势

拖拽手势识别器UIPanGestureRecognizer对象检测一个或者多个手指在屏幕上移动。屏幕边缘拖拽手势是限定触摸位置在屏幕边缘的拖拽手势,使用UIScreenEdgePanGestureRecognizer对象来识别屏幕边缘拖拽手势。

图3-3 Pan gesture

拖拽手势属于连续型手势,在手势识别过程中,会多次调用手势关联对象的操作方法。当手指开始移动时,拖拽手势识别器进入UIGestureRecognizerStateBegan状态,继续移动会导致手势识别器进入UIGestureRecognizerStateChanged状态。当手指离开屏幕时,进入UIGestureRecognizerStateEnded状态。

使用UIPanGestureRecognizer对象的translationInView:方法可以获取手指从初始触摸位置移动的距离。在手势开始时,拖拽手势识别器会存储初始触摸点。如果手势涉及多个手指,则手势识别器会使用多个手指的触摸点的中心点。

如果触摸屏幕后没有调用与手势识别器关联的对象的操作方法,请检查下列条件是否成立:

  • 视图的userInteractionEnabled属性设置为YESUIImageViewUILabel类默认将此属性设为NO
  • 触摸次数在minimumNumberOfTouchesmaximumNumberOfTouches属性中指定的值之间。
  • 如果是屏幕边缘拖拽手势,应确保触摸位置在edges属性中指定的区域中。

轻扫手势

轻扫手势识别器UISwipeGestureRecognizer对象检测屏幕上一个或多个手指在特定的水平或垂直方向上移动。轻扫手势的方向和手指的数量是可以配置的,其属于离散型手势,只有在手势被成功识别后才会调用手势关联对象的操作方法。当我们只关注手势的结果而不关注手指的移动时,轻扫手势是最合适的。

图3-4 Swipe gesture

如果触摸屏幕后没有调用与手势识别器关联的对象的操作方法,请检查下列条件是否成立:

  • 视图的userInteractionEnabled属性设置为YESUIImageViewUILabel类默认将此属性设为NO
  • 触摸次数等于numberOfTouchesRequired属性中指定的值之间。
  • 滑动的方向与direction属性值相匹配。

捏合手势

捏合手势属于连续型手势,其跟踪最先触摸屏幕的两根手指之间的距离,使用UIPinchGestureRecognizer对象来检测捏合手势。

图3-5 Pinch gestures

当两个手指间的距离开始改变时,会更新捏合手势识别器对象的手指间当前距离与初始距离的比例scale属性值,然后调用手势关联目标对象的操作方法。捏合手势常用于更改屏幕上的对象或者内容的大小。在缩放内容大小时,应该取scale值和内容初始大小的积。

如果触摸屏幕后没有调用与手势识别器关联的对象的操作方法,请检查下列条件是否成立:

  • 视图的userInteractionEnabled属性设置为YESUIImageViewUILabel类默认将此属性设为NO
  • 至少两根手指同时触摸屏幕。
  • 正在使用scale值缩放内容大小。

旋转手势

旋转手势属于连续型手势,其跟踪触摸屏幕的两根手指旋转的角度,使用UIRotationGestureRecognizer对象来检测旋转手势。

图3-6 Rotation gesture

当手指开始在屏幕上旋转时,会更新旋转手势识别器对象的从初始到现在已旋转角度rotation属性值,然后调用手势关联目标对象的操作方法。可以使用旋转手势来旋转视图或者更新自定义控件的值。

如果触摸屏幕后没有调用与手势识别器关联的对象的操作方法,请检查下列条件是否成立:

  • 视图的userInteractionEnabled属性设置为YESUIImageViewUILabel类默认将此属性设为NO
  • 至少两根手指同时触摸屏幕。
  • 正在使用rotation旋转内容。

处理手势冲突

在视图上附加多个手势识别器时,多个手势识别器会同时跟踪传入的触摸事件。但默认情况下UIKit只允许在单个视图中一次只识别一个手势,也就是说当某个手势识别器成功识别手势后,其他手势识别器就不会再继续去识别该手势。一次只识别一个手势可以防止用户一次触发多个动作,但这种默认识别行为可能会导致意想不到的副作用。例如,在包含拖拽和轻扫手势识别器的视图中,轻扫手势永远不会被识别。这是因为拖拽手势是连续的,所以它总是在轻扫手势之前被识别。

UIKit通过调用手势识别器的委托对象的代理方法来确定一个手势是否必须在其他手势之前或之后被识别,或者两个手势是否可以被同时识别。对于涉及潜在冲突的两个手势识别器,只需要其中一个手势识别器去关联委托对象,该委托对象需要遵循UIGestureRecognizerDelegate协议。

确定在视图中识别手势的顺序

当视图上附加有单击和双击手势时,单击手势会始终在双击手势之前被识别,但是可以通过实现单击手势委托对象的gestureRecognizer:shouldRequireFailureOfGestureRecognizer:方法去延后单击手势的识别直到双击手势被识别失败,代码如下:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if (gestureRecognizer == self.singleTap && otherGestureRecognizer == self.doubleTap)
    {
        return YES;
    }
    return NO;
}

当视图上附加有轻扫和拖拽手势时,拖拽手势会始终在轻扫手势之前被识别。在这种情况下,可以通过实现轻扫手势委托对象的gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:方法实现在识别轻扫手势失败后才识别拖拽手势,代码如下:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if (gestureRecognizer == self.swip && otherGestureRecognizer == self.pan)
    {
        return YES;
    }
    return NO;
}

也可以通过实现拖拽手势委托对象的gestureRecognizer:shouldRequireFailureOfGestureRecognizer:方法去延后拖拽手势的识别直到轻扫手势被识别失败,代码如下:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if (gestureRecognizer == self.pan && otherGestureRecognizer == self.swip)
    {
        return YES;
    }
    return NO;
}

允许同时识别多个手势

在默认情况下,UIkit在单个视图中一次只允许识别一个手势,但如果有需要,也可以在单个视图中同时识别多个手势。要允许在单个视图中同时识别多个手势,需要手势委托对象实现gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:方法。UIKit会调用此方法来判断是否允许同时识别两个手势,默认返回NO

当一个视图上同时附加有拖拽、 缩放和旋转手势时,允许用户在屏幕上可以同时拖动、 缩放和旋转视图,代码如下:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    // 允许同时识别在同一视图上的特定手势
    if (gestureRecognizer.view == self.targetView && otherGestureRecognizer.view == self.targetView)
    {
        // 排除长按手势
        if (![gestureRecognizer isMemberOfClass:[UILongPressGestureRecognizer class]] && ![otherGestureRecognizer isMemberOfClass:[UILongPressGestureRecognizer class]])
        {
            return YES;
        }
    }
    return NO;
}

要同时识别多个手势,它们的委托对象都要实现gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:方法,如果只有其中一个手势的委托对象实现了,但其它手势的委托对象没有实现,那么UIKit也不会同时识别它们。

自定义手势识别器

当UIKit定义的手势类型不能满足我们需求时,我们也可以自定义手势识别器来处理特定的触摸事件。自定义手势识别器的详情可以参看Implementing a Custom Gesture Recognizer

Demo

示例代码下载地址:https://github.com/Jen668/GesturesDemo.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容