Unity3D UGUI 源码学习 InputModule

BaseInputModule是Unity GUI事件系统的一部分,负责产生事件并发送事件。

几个InputModule的继承关系:

1
2
3
4
5
6
public abstract class BaseInputModule : UIBehaviour

public abstract class PointerInputModule : BaseInputModule

public class TouchInputModule : PointerInputModule
public class StandaloneInputModule : PointerInputModule
  • BaseInputModule:输入模块的基类,持有一个BaseInput对象,提供了GetAxisEventData()GetBaseEventData()方法获取(产生)一个新的事件,除此之外还有一些静态的工具方法和基类的虚方法

  • PointerInputModule:通用的方法 多为protected 给StandaloneInputModule调用

  • TouchInputModule:Obsolete 已弃用,改由StandaloneInputModule处理
  • StandaloneInputModule: 实现了ActivateModule DeactivateModule UpdateModule Process

BaseInput

开始BaseInputModule之前,先看BaseInput类,它是对UnityEngine.Input类的一层封装,获取输入的状态,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ...
public virtual bool GetMouseButtonDown(int button)
{ return Input.GetMouseButtonDown(button);}

public virtual bool GetMouseButtonUp(int button)
{ return Input.GetMouseButtonUp(button);}

public virtual bool GetMouseButton(int button)
{ return Input.GetMouseButton(button);}

public virtual Vector2 mousePosition
{ get { return Input.mousePosition; }}

public virtual int touchCount
{ get { return Input.touchCount; }}

public virtual Touch GetTouch(int index)
{ return Input.GetTouch(index);}

public virtual bool GetButtonDown(string buttonName)
{ return Input.GetButtonDown(buttonName);}
// ...

EventData

事件数据,记录当前事件相关的信息。有抽象基类AbstractEventData,衍生出的有BaseEventData,后者又有衍生类PointerEventDataAxisEventData。EventData是UI系统总事件的承载者,其中包含有与该事件相关的各种信息。

AbstractEventData

有两个方法和一个属性。用于处理和获取该事件是否被使用的状态。这些方法后边在说到StandaloneInputModuleProcess()时会用到。

1
2
3
4
5
6
7
8
public void Reset()
{ m_Used = false;}

public void Use()
{ m_Used = true;}

public bool used
{ get { return m_Used; }}

BaseEventData

相比于抽象基类,多了两个属性,即当前的输入模块currentInputModule和当前选中的对象selectedObject

PointerEventData

相比Base增加了很多东西,都是和点击相关的。

按键或触摸的id,其中InputButton是它定义的一个枚举类型:

  • InputButton button:鼠标按键枚举值(左键、中键、右键)
  • int pointerId:触摸的id

事件相关的对象

  • GameObject pointerEnter:接收OnPointerEnter事件的对象

  • GameObject m_PointerPress:接收OnPointerDown事件的对象

  • GameObject lastPress:上一次响应按下事件的对象
  • GameObject rawPointerPress:发生按下事件的对象,即使它不能响应该事件
  • GameObject pointerDrag:接收OnDrag事件的对象
  • List<GameObject> hovered:存储的是一组对象,这些对象都接收过OnPointerEnter事件,详见后边HandlePointerExitAndEnter()

射线投射结果相关:

  • RaycastResult pointerCurrentRaycast:当前事件关联的射线投射结果
  • RaycastResult pointerPressRaycast:按下(点击)事件关联的射线投射结果

状态标记:

  • bool eligibleForClick:当前事件用于点击

点击位置信息:

  • Vector2 position:当前点击(鼠标或触摸)位置
  • Vector2 delta:与上一帧点击的变化量
  • Vector2 pressPosition:按下的位置

点击的其它信息:

  • float clickTime:上一次点击的时间
  • int clickCount:点击的数量

与拖动和滑动相关的信息:

  • Vector2 scrollDelta:与上一帧相比的滑动量
  • bool useDragThreshold:是否使用拖拽阈值
  • bool dragging:是否正在拖动

定义了一个枚举类型FramePressState:某一帧内按键的状态。

除了上边之外还有一些方法,也都是用来获取状态信息的。

AxisEventData

包含两个属性,移动方向moveDir及移动矢量moveVector,前者是一个枚举类型,后者是Vector2类型。

BaseInputModule

重要的成员

  • m_AxisEventData:在GetAxisEventData()中会反复使用的一个AxisEventData对象

  • m_BaseEventData:在GetBaseEventData()中会反复使用的一个BaseEventData对象

  • BaseInput input:获取BaseInput对象,在BaseInputModule及其衍生类中很多动作都是由其持有的BaseInput对象来完成的。在获取input时,先判断是否有m_InputOverride(衍生类可以通过设置m_InputOverride来实现对input的覆盖)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public BaseInput input
    {
    get
    {
    if (m_InputOverride != null)
    return m_InputOverride;

    if (m_DefaultInput == null)
    {
    var inputs = GetComponents<BaseInput>();
    foreach (var baseInput in inputs)
    {
    // We dont want to use any classes that derrive from BaseInput for default.
    if (baseInput != null && baseInput.GetType() == typeof(BaseInput))
    {
    m_DefaultInput = baseInput;
    break;
    }
    }

    if (m_DefaultInput == null)
    m_DefaultInput = gameObject.AddComponent<BaseInput>();
    }

    return m_DefaultInput;
    }
    }

重要的方法

覆写了OnEnable和OnDisable,在被调用时调用EventSystem的UpdateModules()方法。定义了抽象方法Process(),虚方法DeactivateModule()DeactivateModule()由其衍生类来实现。

每帧调用UpdateModule()Process()

除此之外有一些静态工具方法:

  • RaycastResult FindFirstRaycast():在传入的RaycastResult的列表中找到第一个有效的RaycastResult
  • MoveDirection DetermineMoveDirection():根据传入的坐标x和y,转化为MoveDirection枚举类型
  • GameObject FindCommonRoot():传入两个GameObject返回其共有的父对象

还有一个是实例方法HandlePointerExitAndEnter,用于处理Pointer(鼠标或点击)离开某个对象及新进入某个对象时对两者的一些操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// walk up the tree till a common root between the last entered and the current entered is foung
// send exit events up to (but not inluding) the common root. Then send enter events up to
// (but not including the common root).
protected void HandlePointerExitAndEnter(PointerEventData currentPointerData, GameObject newEnterTarget)
{
// if we have no target / pointerEnter has been deleted
// just send exit events to anything we are tracking
// then exit
if (newEnterTarget == null || currentPointerData.pointerEnter == null)
{
for (var i = 0; i < currentPointerData.hovered.Count; ++i)
ExecuteEvents.Execute(currentPointerData.hovered[i], currentPointerData, ExecuteEvents.pointerExitHandler);

currentPointerData.hovered.Clear();

if (newEnterTarget == null)
{
currentPointerData.pointerEnter = newEnterTarget;
return;
}
}

// if we have not changed hover target
if (currentPointerData.pointerEnter == newEnterTarget && newEnterTarget)
return;

GameObject commonRoot = FindCommonRoot(currentPointerData.pointerEnter, newEnterTarget);

// and we already an entered object from last time
if (currentPointerData.pointerEnter != null)
{
// send exit handler call to all elements in the chain
// until we reach the new target, or null!
Transform t = currentPointerData.pointerEnter.transform;

while (t != null)
{
// if we reach the common root break out!
if (commonRoot != null && commonRoot.transform == t)
break;

ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerExitHandler);
currentPointerData.hovered.Remove(t.gameObject);
t = t.parent;
}
}

// now issue the enter call up to but not including the common root
currentPointerData.pointerEnter = newEnterTarget;
if (newEnterTarget != null)
{
Transform t = newEnterTarget.transform;

while (t != null && t.gameObject != commonRoot)
{
ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerEnterHandler);
currentPointerData.hovered.Add(t.gameObject);
t = t.parent;
}
}
}

其实注释已经很清楚了,寻找之前最后一次进入的对象,以及当前进入的对象的共同父节点,然后对从离开的对象到共同父节点(不含共同的父节点)发送“离开(Exit)”事件,同理对从进入的对象到共同父节点(不含共同的父节点)发送“进入(Enter)”事件。

ExecuteEvents是与事件执行相关的类,ExecuteEvents.Execute(...)是对某个对象(GameObject)执行某事件的方法,后边的章节会专门说这个类。

整个过程分为几步:

  • 如果没有新进入的对象,或者currentPointerData.pointerEnter是null,那么就不处理进入(Enter),只处理离开(Exit),即对currentPointerData.hovered中的各个对象都执行离开的事件,并在最后确保将currentPointerData.pointerEnter设为null
  • 如果对象没有变化,就是说即将进入的对象和当前事件数据中的进入对象是同一个对象,直接返回。
  • 寻找当前事件数据中的进入对象currentPointerData.pointerEnter和即将要进入的对象newEnterTarget二者的共同父节点。然后如前边说的那样,从旧的进入对象到共有父节点,逐个执行离开事件,并从hovered中移除,此操作不含共有父节点;接下来从新的进入对象到共有父节点,逐个执行进入事件,并将其加入hovered,此操作不包含共有父节点。

PointerInputModule

由于在旧的代码中,PointerInputModule衍生出StandaloneInputModuleTouchInputModule两个类,因此很多公用的方法都放在PointerInputModule中处理,同时给两个子类使用。后来的版本TouchInputModule已弃用,且原本有TouchInputModule承担的逻辑也都合并到StandaloneInputModule中处理(将触摸点击同样视为鼠标按键,二者有不同的id类型,逻辑基本一致)。这么一来,PointerInputModule的内容基本是只服务于StandaloneInputModule。我们选择从StandaloneInputModule入手,二者一起研究,当使用到PointerInputModule中的方法时,我们再跳回来看。此处只是提一下其中的一些成员定义:

  • Dictionary<int, PointerEventData> m_PointerData:键是pointerId或者鼠标按键的枚举值InputButton 值是PointerEventData。它是用来产生并记录与按键或点击相对应的EventData的字典。

除此之外,还有三个嵌套类,逻辑层层封装:

  • MouseButtonEventData:鼠标按键的事件及状态,内包含了一个PointerEventData 和一个FramePressState值,及一组判断鼠标按键是否按下或松开的方法。
  • ButtonState:按键的状态,是对MouseButtonEventData更外一层的封装,内部包含了一个鼠标按键的idInputButton 和一个MouseButtonEventData
  • MouseState:封装了一组ButtonState对象(一个List),提供了一组ButtonState的set和get方法,以及判断鼠标的所有按键中,这一帧内是否有按下或松开的方法。

StandaloneInputModule

内容也很多,我们直接从UpdateModule()Process()入手,顺藤摸瓜,捋一捋它的这些成员和方法:

UpdateModule

UpdateModule()是在EventSystem中每帧调用的方法:

1
2
3
4
5
public override void UpdateModule()
{
m_LastMousePosition = m_MousePosition;
m_MousePosition = input.mousePosition;
}

涉及到了它的两个成员字段,根据字面意思很明确了:更新和获取鼠标位置。

Process

重点是Process(),也是在每帧调用,这其中包含了绝大多数的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public override void Process()
{
bool usedEvent = SendUpdateEventToSelectedObject();

if (eventSystem.sendNavigationEvents)
{
if (!usedEvent)
usedEvent |= SendMoveEventToSelectedObject();

if (!usedEvent)
SendSubmitEventToSelectedObject();
}

// touch needs to take precedence because of the mouse emulation layer
if (!ProcessTouchEvents() && input.mousePresent)
ProcessMouseEvent();
}

结合上边的代码:首先是SendUpdateEventToSelectedObject(),处理update更新事件。并把返回值赋给了usedEvent,即事件是否已使用(或者说能否被下一个步骤使用)。接下来如果eventSystem.sendNavigationEvents为真,判断和更新usedEvent的值,发送move和submit的事件。最后一步,处理触摸事件ProcessTouchEvents(),如果返回false且有鼠标显示,则处理鼠标事件ProcessMouseEvent()

SendUpdateEventToSelectedObject

SendUpdateEventToSelectedObject()定义如下:

1
2
3
4
5
6
7
8
9
protected bool SendUpdateEventToSelectedObject()
{
if (eventSystem.currentSelectedGameObject == null)
return false;

var data = GetBaseEventData();
ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.updateSelectedHandler);
return data.used;
}

发送其它事件的函数定义与此大同小异(可能会包含更多的逻辑判断,但是基本框架步骤都是如此),首先获取(产生)事件GetBaseEventData(),执行事件ExecuteEvents.Execute(...),如果当前选中的对象eventSystem.currentSelectedGameObject实现了IUpdateSelectedHandle,则会调用更新的方法OnUpdateSelected()。最后返回事件是否已被使用。

SendMoveEventToSelectedObject(),会根据按键判断移动方向GetRawMoveVector(),根据连续移动的状态ConsecutiveMoveCount进行一些判断和过滤,最后在传递事件时使用的是AxisEventData。发送完成之后会更新事件used的状态。

SendSubmitEventToSelectedObject(),对应提交submit和取消cancel事件,没有任何更多的复杂逻辑,发送完成后也会更新事件used的状态,注意move和submit都是当eventSystem.sendNavigationEvents为真时才会发送。

最后来重点看一下关于触摸和鼠标点击的处理:

ProcessTouchEvents

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private bool ProcessTouchEvents()
{
for (int i = 0; i < input.touchCount; ++i)
{
Touch touch = input.GetTouch(i);

if (touch.type == TouchType.Indirect)
continue;

bool released;
bool pressed;
var pointer = GetTouchPointerEventData(touch, out pressed, out released);

ProcessTouchPress(pointer, pressed, released);

if (!released)
{
ProcessMove(pointer);
ProcessDrag(pointer);
}
else
RemovePointerData(pointer);
}
return input.touchCount > 0;
}

遍历所有的触摸,如果touch.type不为Indirect(这一状态是由更底层的api获取到的),则继续处理,做一些事情,最后返回当前点击数。

我们来看对每个点击所做的事情,GetTouchPointerEventData获取对应的PointerEventData,并且同时会取到其是否是按下或释放的状态。其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released)
{
PointerEventData pointerData;
var created = GetPointerData(input.fingerId, out pointerData, true);

pointerData.Reset();

pressed = created || (input.phase == TouchPhase.Began);
released = (input.phase == TouchPhase.Canceled) || (input.phase == TouchPhase.Ended);

if (created)
pointerData.position = input.position;

if (pressed)
pointerData.delta = Vector2.zero;
else
pointerData.delta = input.position - pointerData.position;

pointerData.position = input.position;

pointerData.button = PointerEventData.InputButton.Left;

eventSystem.RaycastAll(pointerData, m_RaycastResultCache);

var raycast = FindFirstRaycast(m_RaycastResultCache);
pointerData.pointerCurrentRaycast = raycast;
m_RaycastResultCache.Clear();
return pointerData;
}

获取(产生)PointerEventData,判断其状态,并且要判断它是否是新创建的createdinput.phase表示当前触摸事件位于的阶段(BeganMovedStationaryEndedCanceled ,也是由底层的api获取的)。根据是否新建以及触摸的阶段来对其进行的一些初始化工作或值的更新。最后是一些射线投射的内容,使用当前事件做射线投射,获取第一个投射结果并更新pointerData.pointerCurrentRaycast。关于射线投射的内容会在后边的章节展开讨论。

回到刚才的ProcessTouchEvents,获取了点击事件数据后,紧接着会调用ProcessTouchPress,处理按下和释放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
protected void ProcessTouchPress(PointerEventData pointerEvent, bool pressed, bool released)
{
var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;

// PointerDown notification
if (pressed)
{
pointerEvent.eligibleForClick = true;
pointerEvent.delta = Vector2.zero;
pointerEvent.dragging = false;
pointerEvent.useDragThreshold = true;
pointerEvent.pressPosition = pointerEvent.position;
pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;

DeselectIfSelectionChanged(currentOverGo, pointerEvent);

if (pointerEvent.pointerEnter != currentOverGo)
{
// send a pointer enter to the touched element if it isn't the one to select...
HandlePointerExitAndEnter(pointerEvent, currentOverGo);
pointerEvent.pointerEnter = currentOverGo;
}

// search for the control that will receive the press
// if we can't find a press handler set the press
// handler to be what would receive a click.
var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);

// didnt find a press handler... search for a click handler
if (newPressed == null)
newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

// Debug.Log("Pressed: " + newPressed);

float time = Time.unscaledTime;

if (newPressed == pointerEvent.lastPress)
{
var diffTime = time - pointerEvent.clickTime;
if (diffTime < 0.3f)
++pointerEvent.clickCount;
else
pointerEvent.clickCount = 1;

pointerEvent.clickTime = time;
}
else
{
pointerEvent.clickCount = 1;
}

pointerEvent.pointerPress = newPressed;
pointerEvent.rawPointerPress = currentOverGo;

pointerEvent.clickTime = time;

// Save the drag handler as well
pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);

if (pointerEvent.pointerDrag != null)
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);
}

// PointerUp notification
if (released)
{
// Debug.Log("Executing pressup on: " + pointer.pointerPress);
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);

// Debug.Log("KeyCode: " + pointer.eventData.keyCode);

// see if we mouse up on the same element that we clicked on...
var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

// 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);
}

pointerEvent.eligibleForClick = false;
pointerEvent.pointerPress = null;
pointerEvent.rawPointerPress = null;

if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);

pointerEvent.dragging = false;
pointerEvent.pointerDrag = null;

if (pointerEvent.pointerDrag != null)
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);

pointerEvent.pointerDrag = null;

// send exit events as we need to simulate this on touch up on touch device
ExecuteEvents.ExecuteHierarchy(pointerEvent.pointerEnter, pointerEvent, ExecuteEvents.pointerExitHandler);
pointerEvent.pointerEnter = null;
}
}

代码比较长,可以划分为两部分:处理按下和处理释放。当判断点击状态为按下时,做了以下的事情:

  • 初始化pointerEvent
  • 处理enter和exit事件,清除旧的选择对象selected,更新新的选择对象,并在HandlePointerExitAndEnter(...)中触发enter和exit的事件;
  • 处理pointerDown的事件,这里使用的是ExecuteHierarchy,即会对从当前对象所在的树状结构中自下而上第一个可以响应该事件的对象执行该事件,后边讲到ExecuteEvents时还会再提到;
  • 如果没有对象响应pointerDown,则会尝试执行click事件,也是会自下而上找一遍;
  • 如果响应pointerDown的对象newPressed与上一帧的是同一对象,则处理连续点击的逻辑,竟然有一个写死的0.3f,然后是更新数据;
  • 最后执行一个初始化潜在拖拽的事件;

在处理松开时,主要处理的是以下的逻辑:

  • 处理pointerUp的事件;

  • 如果响应pointerUp的对象pointerUpHandler与按下对象相同且当前事件可用于点击,则执行click事件

  • 如果没有click事件,则判断是否有drag的对象以及当前是否是dragging,如有则执行drop事件;

  • 更新一波数据,主要是清除;

  • 执行endDrag并清除一些数据;

  • 执行exit并清除一些数据;

再回到ProcessTouchEvents,如果不是松开状态(released)则处理移动move和拖动drag,否则清除数据。以下是ProcessMoveProcessDrag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
protected virtual void ProcessMove(PointerEventData pointerEvent)
{
var targetGO = (Cursor.lockState == CursorLockMode.Locked ? null : pointerEvent.pointerCurrentRaycast.gameObject);
HandlePointerExitAndEnter(pointerEvent, targetGO);
}

protected virtual void ProcessDrag(PointerEventData pointerEvent)
{
if (!pointerEvent.IsPointerMoving() ||
Cursor.lockState == CursorLockMode.Locked ||
pointerEvent.pointerDrag == null)
return;

if (!pointerEvent.dragging
&& ShouldStartDrag(pointerEvent.pressPosition, pointerEvent.position, eventSystem.pixelDragThreshold, pointerEvent.useDragThreshold))
{
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.beginDragHandler);
pointerEvent.dragging = true;
}

// Drag notification
if (pointerEvent.dragging)
{
// Before doing drag we should cancel any pointer down state
// And clear selection!
if (pointerEvent.pointerPress != pointerEvent.pointerDrag)
{
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);

pointerEvent.eligibleForClick = false;
pointerEvent.pointerPress = null;
pointerEvent.rawPointerPress = null;
}
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.dragHandler);
}
}

move处理鼠标指针未按下时的状态,逻辑很简单,光标lockState如果是锁定状态,则以null为目标对象,否则是当前click的对象为目标对象。然后执行一次HandlePointerExitAndEnter

drag处理按下后移动的逻辑,首先判断如果不符合拖动,如点击点没有移动等则直接返回;接下来判断是否要开始拖动,触发的事件是beginDrag;最后如果是dragging状态,如果按下对象和拖动的对象不同,则需要为拖动进行一次初始化,然后执行onDrag的事件。

ProcessMoveProcessDrag是触摸点击和鼠标点击公用的逻辑。

现在需要回到Process(),当ProcessTouchEvents()返回了falseinput.mousePresenttrue时,处理鼠标事件ProcessMouseEvent()

ProcessMouseEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protected void ProcessMouseEvent(int id)
{
var mouseData = GetMousePointerEventData(id);
var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;

if (ForceAutoSelect())
eventSystem.SetSelectedGameObject(leftButtonData.buttonData.pointerCurrentRaycast.gameObject, leftButtonData.buttonData);

// Process the first mouse button fully
ProcessMousePress(leftButtonData);
ProcessMove(leftButtonData.buttonData);
ProcessDrag(leftButtonData.buttonData);

// Now process right / middle clicks
ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData);
ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData.buttonData);
ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData);
ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData.buttonData);

if (!Mathf.Approximately(leftButtonData.buttonData.scrollDelta.sqrMagnitude, 0.0f))
{
var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(leftButtonData.buttonData.pointerCurrentRaycast.gameObject);
ExecuteEvents.ExecuteHierarchy(scrollHandler, leftButtonData.buttonData, ExecuteEvents.scrollHandler);
}
}

与触摸点击的处理相似,需要依次处理左键、右键和中键。首先用id(默认参数0)获取MouseState mouseData,然后从中获取ButtonState leftButtonData ,这两个都是之前说到的内嵌类。处理左键,首先是鼠标按下/松开ProcessMousePress()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
protected void ProcessMousePress(MouseButtonEventData data)
{
var pointerEvent = data.buttonData;
var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;

// PointerDown notification
if (data.PressedThisFrame())
{
pointerEvent.eligibleForClick = true;
pointerEvent.delta = Vector2.zero;
pointerEvent.dragging = false;
pointerEvent.useDragThreshold = true;
pointerEvent.pressPosition = pointerEvent.position;
pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;

DeselectIfSelectionChanged(currentOverGo, pointerEvent);

// search for the control that will receive the press
// if we can't find a press handler set the press
// handler to be what would receive a click.
var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);

// didnt find a press handler... search for a click handler
if (newPressed == null)
newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

// Debug.Log("Pressed: " + newPressed);

float time = Time.unscaledTime;

if (newPressed == pointerEvent.lastPress)
{
var diffTime = time - pointerEvent.clickTime;
if (diffTime < 0.3f)
++pointerEvent.clickCount;
else
pointerEvent.clickCount = 1;

pointerEvent.clickTime = time;
}
else
{
pointerEvent.clickCount = 1;
}

pointerEvent.pointerPress = newPressed;
pointerEvent.rawPointerPress = currentOverGo;

pointerEvent.clickTime = time;

// Save the drag handler as well
pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);

if (pointerEvent.pointerDrag != null)
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);
}

// PointerUp notification
if (data.ReleasedThisFrame())
{
// Debug.Log("Executing pressup on: " + pointer.pointerPress);
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);

// Debug.Log("KeyCode: " + pointer.eventData.keyCode);

// see if we mouse up on the same element that we clicked on...
var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

// 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);
}

pointerEvent.eligibleForClick = false;
pointerEvent.pointerPress = null;
pointerEvent.rawPointerPress = null;

if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);

pointerEvent.dragging = false;
pointerEvent.pointerDrag = null;

// redo pointer enter / exit to refresh state
// so that if we moused over somethign that ignored it before
// due to having pressed on something else
// it now gets it.
if (currentOverGo != pointerEvent.pointerEnter)
{
HandlePointerExitAndEnter(pointerEvent, null);
HandlePointerExitAndEnter(pointerEvent, currentOverGo);
}
}
}

ProcessTouchEvents()很相似,不再赘述。接下来是move和drag,与触摸处理完全一致。接下来是处理右键和中键。三个键都处理之后,如果scrollDelta的值大于0,执行scroll事件。


本系列其它文章详见Unity3D UGUI 源码学习

REFERENCE