UI事件系统设计
手指基础事件
基础事件由操作系统提供
- TouchBegan
- TouchMoved
- TouchEnded
- TouchCanceled
手势优先级
- TapDownEvent TapUpEvent TapCancelEvent
- TapEvent(Clicked)
- LongTapEvent(LongPress)
- Drag
- MultiDrag
手势识别器类图
手势识别过程
假设按钮按如下顺序摆放
当手指在在Button3按下后。Button3的TapGestureRecognizer添加到Touch的识别列表中,再将DragGestureRecognizer添加到Touch的识别列表中;Button2的TapGestureRecognizer添加到Touch的识别列表中,再将DragGestureRecognizer添加到Touch的识别列表中;Button1的TapGestureRecognizer添加到Touch的识别列表中,再将DragGestureRecognizer添加到Touch的识别列表中。
Touch的识别列表如下,顶部为队列头部,底部为队列尾端。
手指基础事件会经过这些识别器进行识别。按照List的顺序,每个识别器都会去识别手势。
TouchBegan发生后。Button3.TapGestureRecognizer识别到TapDownEvent。Button3.DragGestureRecognizer觉得手势可能会在将来触发。接着Button2.TapGestureRecognizer识别到TapDownEvent。Button2.DragGestureRecognizer觉得手势可能会在将来触发。接着Button1.TapGestureRecognizer识别到TapDownEvent。Button1.DragGestureRecognizer觉得手势可能会在将来触发。识别器识别完成后,Touch再挨个调用Recognizer的Accept函数。挨个发射Event。最后UI上的3个按钮都触发了TapDownEvent。
接着,玩家在Button3上抬起了手指,系统会触发TouchEnded。Button3.TapGestureRecognizer识别到TapUpEvent,也识别到了TapEvent。Button3.DragGestureRecognizer没有识别到手势。接着Button2.TapGestureRecognizer识别到TapUpEvent,也识别到了TapEvent。Button2.DragGestureRecognizer没有识别到手势。接着Button1.TapGestureRecognizer识别到TapUpEvent,也识别到了TapEvent。Button1.DragGestureRecognizer没有识别到手势。
现在3个识别器都识别到了TapUpEvent和TapEvent。TapUpEvent比较好处理。每个识别器都发射TapUpEvent就好了。但是TapEvent就不一样了。正常逻辑来讲,3个按钮不可能都触发TapEvent,只能有1个TapEvent被触发,其余的都应该当作没发生过。
这里需要引入手指竞争概念。TapEvent算是高级手势,如果TapEvent被识别到,识别器可以独占手指。其它识别器将放弃识别。我们可以在Recognizer的返回值上做文章。识别器之间需要竞争。
EGestureRecognizerResult(Recognizer返回值)
- Ignore(识别器忽略手指,退出手势竞争)
- MayBeGesture(识别器认为手指将来会触发手势,告诉调用者保留自己继续识别的权力)
- TriggerGesture(识别器识别到手势,如果自己没有被淘汰,则应该触发手势)
- CancelGesture(识别器识放弃竞争,调用者将自己淘汰。比如控件被禁用了)
下面两个是抢占手势的两种情况,会与上面的4个枚举或运算后返回
- ConsumeEventHint
- ConsumeGestureHint
这两个都是通知调用者识别器将占用手指,让调用者淘汰掉其它的识别器。区别是ConsumeEventHint表示自己识别成功。而ConsumeGestureHint表示自己是因为特殊情况,一定要占用手指,在多点触控的时候用得上。举个例子,按钮已经触发DragEvent了,这时玩家把新的手指放到了按钮上,新的手指应该直接被Drag识别器捕获,并且竞争成功。
再次讨论手势识别过程
当Tap或Drag或LongTap被识别后,他们都会返回 TriggerGesture|ConsumeEventHint 或者 TriggerGesture|ConsumeGestureHint。Touch得到每个识别器的结果后,会判断哪一个识别器竞争成功。识别列表可以当作优先级。列表从前往后,如果返回值包含ConsumeGestureHint,第一个返回ConsumeGestureHint会直接成功,否则第一个返回包含ConsumeEventHint会竞争成功。竞争成功后,其余所有识别器调用Reset函数,将刚才的识别取消掉。识别完成后,再调用Accept函数将最终的识别结果执行下去。
在本文的例子中,由于Button3的Tap手势被识别。Button1和Button2的TapGestureRecognizer竞争失败而退出。他们调用Reset后将原来的TapUpEvent转变为TapCancelEvent。在Accept调用里,将发出TapCancelEvent。为什么这么设计呢,主要时考虑到这样的方式可以顺便通知别的控件,手指被别人占有了,起到提示作用。这些控件也确实彻底丧失了这个手指,但是手指并没有离开屏幕,所以TapCancelEvent看上去更合适一点。
读到这里,细心的人会发现一个问题。如果TapEvent被识别后,该控件的TapUpEvent将来会不会触发?在有些框架里,TapUpEvent不会再触发了。但我觉得这样的设计并不好。大家都作用与统一个控件,TapUpEvent应该触发。所以我修改了竞争机制,最后会保留竞争成功的识别器和TapGestureRecognizer。以此保证TapUpEvent被正确触发。
关于多指操作
ConsumeGestureHint就是处理多指操作的,如果识别器是单指识别器,可以将多余的手指直接占有,避免其他控件触发手势导致奇怪的逻辑表现。
如果是双指缩放这类手势事件,当单个手指进入识别器时,返回MayBeGesture。当第二个手指进入识别器时,返回TriggerGesture|ConsumeEventHint。系统下一帧遍历到第一个手指时,由于识别器已经是Trigger状态,所以第一个手指的识别器返回TriggerGesture|ConsumeGestureHint,尝试让自己竞争成功,不会被别的控件占有。当有2个手指都竞争成功后,就可以在Accept中发射双指手势事件了。为什么一定要等到这个时候才能发射手势事件呢?毕竟能不能占有到手指还是Touch说了算,识别器不能保证一定能竞争成功,所以要等到确实占有两个手指时,才能发射手势事件。
多指情况示例
按钮按如下摆放
用户的第一次按下的手指,简称Touch1。用户的第二次按下的手指,简称Touch2。
例1:假设我们在Button3上我们绑定了LongTap,并且在Button1上我们绑定了MultiDrag。我们假设Touch1在Button3上按下,并且之后没有移动。2秒后,Touch2在Button3上按下。接着两根手指都抬起。Touch1在按下0.5秒后,就会触发LongTap,识别器返回TriggerGesture|ConsumeEventHint。当Touch2按下后,Touch2会按正常流程对让每一个识别器进行识别。Button3的LongTap识别器返回TriggerGesture|ConsumeGestureHint,表示自己已经触发手势了,希望能够直接占有该手指。这时候Touch2经过优先级判断后,也确实会将手指分给LongTap识别器。我们用这样的方式解决多手指在单手指手势中的冲突。
例2:我们假设Touch1在Button3上按下,并且之后没有移动。0.1秒后,Touch2在Button3上按下。接着两根手指都抬起。按照流程,Button3的LongTapGestureRecognizer并没有识别到任何手势,所有手势识别器返回的都是MayBeGesture。当第二个手指按下时,Button3的识别器依然返回MayBeGesture。Button1的MultiDragGestureRecognizer会返回TriggerGesture|ConsumeEventHint,这是由于识别器列表中已经有一个手指处于未占有状态,加上这根手指,就满足多指拖拽的手势识别。Touch会将Touch2分配给MultiDragGestureRecognizer。但这时候,还不能触发MultiDrag,因为Touch1没有被识别器占有。等到下一帧,我们遍历到Touch1时,Button1的MultiDragGestureRecognizer会返回TriggerGesture|ConsumeGestureHint,用最高优先级来占有手指。占有到后,触发MultiDrag。一定要用ConsumeGestureHint来占有手指,这样就就算Touch1刚好触发了别的手势,也会被Button3的MultiDragGestureRecognizer识别器占有。
多指不完美的地方
在上边的示例中,两个手指都是从Button3的位置按下的。如果Touch1在Button3上按下,2秒后,Touch2在Button2上按下。Touch2的识别列表中并不会有Button3的任何识别器。这时候Touch2就不会被Button3的LongTapGestureRecognizer占有。假如Button1也绑定了LongTap。就会出现这样的情况,Button3的LongTap在Touch1按下0.5秒后触发。Button1的LongTap在Touch2按下0.5秒后触发。会显得混乱。不过实际应用中,这样摆放按钮的情况几乎不存在,可以不用顾虑。
贴一下我的知乎地址,欢迎大家来讨论。点击前往