在使用UGUI实现拖动功能时,常常会遇到ScrollRect内的放置接收点击事件的Item,最常用的方式就是给Item添加Button组件或继承IPointerClickHandler
的脚本,同时保证ScrollRect的Content内所有RaycastTarget不能拦截拖动相关事件,这样基本就完成了这个功能,运行时“看起来”也没什么问题。
但有时我们会发现在高分辨率触摸设备上ScrollRect内的Item对于点击事件的响应有些不灵敏,也就是用户原本认为是点击的操作被识别为拖动,这是UGUI事件系统所造成的,因为InputModule无法根据当前一帧的数据准确判断触发哪个事件,需要综合考虑之前的状态数据来触发事件。所以当InputModule识别到拖动操作后,后续的触摸都会走到拖动事件的判定流程中,而Item本身的点击事件就无法触发了,下面是UGUI中关于PointerClick事件判定的源码。
if (released) {
...
// PointerClick and Drop events
if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick) {
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
}
else if (pointerEvent.pointerDrag != null && pointerEvent.dragging) {
ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
}
...
}
可以看到如果当前是拖动状态,就不会触发Click事件而是会触发Drop事件。下面是UGUI拖动事件判定的源码。
if (!pointerEvent.dragging && ShouldStartDrag(pointerEvent.pressPosition, pointerEvent.position, eventSystem.pixelDragThreshold, pointerEvent.useDragThreshold))
{
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.beginDragHandler);
pointerEvent.dragging = true;
}
...
private static bool ShouldStartDrag(Vector2 pressPos, Vector2 currentPos, float threshold, bool useDragThreshold)
{
if (!useDragThreshold)
return true;
return (pressPos - currentPos).sqrMagnitude >= threshold * threshold;
}
我们发现只有当当前帧与上一帧的位移的绝对值大于一个给定阈值时才会触发拖动事件的流程,若低于该值,停止触摸时则触发点击事件,该阈值在EventSystem.pixelDragThreshold
字段中定义,我发现在Unity 2017版本中该值默认为5,Unity 2018版本中该值默认为10,但是为了保证不同设备的体验,该阈值在分辨率越高的设备上应当越高,经测试在1080P设备上设为20比较合适,所以根据比例可以使用如下代码进行全局设定:
EventSystem.current.pixelDragThreshold = Screen.height / 50;
这样就完美解决了本文开头提到的问题,在用户点击ScrollRect内的Item时,在触摸的随机抖动范围低于给定的阈值时触发Item的点击事件,否则触发ScrollRect的拖动事件。