View的事件分发机制

Posted by Chejdj Blog on February 19, 2018

再过几天就面试了,现在整理一下View的事件分发机制的整个过程。首先,我们看一下Android UI界面的构架图

ViewGroup的事件分发

每个Activity都包含有一个Window对象,在Android中window的实现类是PhoneWindow来实现,PhoneWindow又将一个DecorView设置为整个应用的窗口的根View,DecorView作为窗口界面的顶层视图,可以理解整个手机屏幕就是一个PhoneWindow,而DecorView就是这个屏幕的画板,来显示该有的内容。 DecorView下面又有TitleView和ContentView(嗯,没错就是在onCreate中调用setContentVIew来显示它的内容)。

首先接受到点击事件的是Activity,我们看一下Activity的dispatchTouchEvent(MotionEvent ev)(注意,我们需要了解当事件传递到某个View的时候,该控件的dispatchTouchEvent方法就会调用)

 public boolean dispatchTouchEvent(MotionEvent ev) {
       if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction(); //一般都是DOWN开始
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
  public void onUserInteraction(){ //调用的为空方法??可能自己覆盖它,达到更好的控制效果??
    }

继续向下执行我们看一下getWindow().superDispatchTouchEvent(ev)方法,getWindow()返回的是Activity自己的一个成员变量 private Window mWindow;,说明事件由Activity->Window,但是我们看到Window为抽象类,但是我们在代码中找到了它的实现类
mWindow = new PhoneWindow(this, window, activityConfigCallback);
由这一句,我们看到是PhoneWindow,我们看一下PhoneWindow实现的方法

public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

它又调用了mDecor.superDispatchTouchEvent(Event),我想不用看就知道mDecor就是DecorView,这又实现了Window->DecorView的传递
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks
这里的DecorView是一个FrameLayout类,FrameLayout又继承自ViewGroup,所以说DecorView就是一个ViewGroup类,看我们看一下DecorView的实现方法

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

调用父类也就是Framelayout的dispatchTouchEvent,FrameLayout并没有复写ViewGroup的dispatchTouchEvent方法,所以直接调用ViewGroup的方法

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
....
if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);//每次传递过来,询问一下是否拦截
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } 


...
public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;   
        }
        return false;
    }  //只有动作来自鼠标,动作是ACTION_DOWN,并且是鼠标左键,最后一个函数没有找到,简单就有鼠标左击事件才会被ViewGroup拦截,其他都不拦截
...
看一下关于BUTTON_PRIMARY事件
 /**
     * Button constant: Primary button (left mouse button).
     *
     * This button constant is not set in response to simple touches with a finger
     * or stylus tip.  The user must actually push a button.
     *
     * @see #getButtonState
     */
....


final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

//比较重要的cancel条件
...
 /**
     * Constant for {@link #getActionMasked}: The current gesture has been aborted.
     * You will not receive any more points in it.  You should treat this as
     * an up event, but not perform any action that you normally would.
     */

    public static final int ACTION_CANCEL           = 3;
.... 
// 检查触摸事件是否被取消
if (!canceled && !intercepted) {  //如果该事件没有被取消,并且不拦截

final int childrenCount = mChildrenCount;//获取孩子的数量
//这里分配条件注意,就是当是ACTION_DOWN的时候
if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {


      if (newTouchTarget == null && childrenCount != 0) {
                  final float x = ev.getX(actionIndex);//触摸事件的坐标
                  final float y = ev.getY(actionIndex);
                 // Find a child that can receive the event.
                 // Scan children from front to back.
                  final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                  final boolean customOrder = preorderedList == null
                          && isChildrenDrawingOrderEnabled();
                  final View[] children = mChildren;//遍历该ViewGroup的孩子
                   for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                               childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                  preorderedList, children, childIndex);
        // If there is a view that has accessibility focus we want it
        // to get the event first and if not handled we will perform a
        // normal dispatch. We may do a double iteration but this is
        // safer given the timeframe.
         if (childWithAccessibilityFocus != null) {
                if (childWithAccessibilityFocus != child) {
                               continue;
                    }
                    childWithAccessibilityFocus = null;
                    i = childrenCount - 1;
                }
           //如果这个孩子可以接受触摸事件,并且事件在事件在孩子的可视范围内,就会向下
          //执行,这里否则跳过
               if (!canViewReceivePointerEvents(child)
                       || !isTransformedTouchPointInView(x, y, child, null)) {
                   ev.setTargetAccessibilityFocus(false);
                                continue;
                 }
 .... 
//调用dispatchTransformedTouchEvent把事件分发给这个孩子
  if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
              // Child wants to receive touch within its bounds.
             //孩子想要接收该事件
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                              mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                           //把这个View添加到mFirstTouchTarget表头
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                 // The accessibility focus didn't handle the event, so clear
                 // the flag and do a normal dispatch to all children.
                 //如果不处理的话
                            ev.setTargetAccessibilityFocus(false);
            }
                 if (preorderedList != null) preorderedList.clear();
  			   if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                 //如果没有找到孩子可以接受事件,把指针指向最近添加的target
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
   //进一步事件分发
   //如果没有找到孩子可以接受事件,就把自己当成View去处理事件
   //如果不是空的话,就说明有View可以接受事件 就把事件分发给他们
      if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
}
}  

这里已经看蒙了吧,我们梳理一下,当点击事件为ACTION_DOWN的时候,ViewGroup去找自己的子孩子能够处理该事件的View,并且把他添加到mFirstTouchTarget链表当中,其他别的事件直接就搜索该链表,把事件分发给之前能够接受Down事件的子View

如果看了前面的话,我们有两处调用了dispatchTransformedTouchEvent函数,一次用于判断是否该child能否接受ACTION_DOWN事件,一处是当没有找到子View的时候,传入child参数为null,我们现在看一下这个函数

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
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
       //对于ACTION_CANCEL事件我们不做过滤,直接分发
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) { 
                handled = super.dispatchTouchEvent(event);//ViewGroup的父类就是View
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        // Calculate the number of pointers to deliver.
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

        // If for some reason we ended up in an inconsistent state where it looks like we
        // might produce a motion event with no pointers in it, then drop the event.
        if (newPointerIdBits == 0) {
            return false;
        }

        // If the number of pointers is the same and we don't need to perform any fancy
        // irreversible transformations, then we can reuse the motion event for this
        // dispatch as long as we are careful to revert any changes we make.
        // Otherwise we need to make a copy.
        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }

        // Perform any necessary transformations and dispatch.
        if (child == null) {  
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

再梳理一下: dispatchTransformedTouchEvent函数就是一个简单的对于事件的过滤,
看是不是和之前是同一个事件,是的就进行分发,如果孩子是NULL的话,
则把消息发送给自己,ViewGroup继承自View,调用View的dispatchTouchEvent,如果child是ViewGroup就递归调用在分发给自己的孩子,最终递归到View的dispatchTouchEvent(MotionEvent ev)

View的dispatchTouchEvent(MotionEvent ev)

直接上源代码

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
public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {//是否有焦点
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) { //没有View直接返回false
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) { //判断是否被遮住
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement

下面是判断的重点

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
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

我们可以看到,判断 li!=NULL,li.mOnTouchListerner!=NULL ` public void setOnTouchListener(OnTouchListener l) { getListenerInfo().mOnTouchListener = l; } `
通过这一句我们可以看到,该View我们需要设置setOnTouchListener,设置了onTouch就不为空
(mViewFlags & ENABLED_MASK) == ENABLED这一句我们是设置View的是否是Enable,默认是enable
li.mOnTouchListener.onTouch(this, event)这个需要我们自己重写,如果不重写就默认返回false.
如果没有设置OnTouchListener,就调用onTouchEvent()方法,下面我们看一下onTouchEvent方法

if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }

通过注释可以知道,一个可以点击的View即使处于不可用状态依然可以消耗事件,只是不能够产生回应,就是对于一个不可用状态的View,如果是可点击就返回true,否则返回false

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
     switch (action) {
   .....
      } 
   return true;
  }
return false;
}

当这个View是可以点击,TOOLTIP 暗示该View是长按显示工具提示,也就是可以点击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
    if (mPerformClick == null) {
          mPerformClick = new PerformClick();
       }
    if (!post(mPerformClick)) {
                        performClick();
 }
...
 public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this); //这里就是我们的OnClickListenrer的方法
.....

好这里再次总结一下: View的dispatchTouchEvent(MotionEvent ev) ,是否设置了onTouchListener,设置了就调用onTouchListener中的onTouch方法,返回true,就把result置为true,如果onTouchlistener.onTouch返回True的话,TouchEvent()就不会调用,如果是false,就调用onTouchEvent方法,在里面判断,如果这个View是不可用状态,就看这个View就返回点击状态,可用状态的就进行一些处理,还是看是否可以点击。

总结

当ACTION_DOWN按下时,由Activity->Window->DecorView->ViewGroup的dispatchTouchEvent,在ViewGroup进行分发,看自己onInterceptTouchEvent是否拦截,如果拦截就调用自己的onTouchEvent()处理,如果不拦截,就遍历自己的子View,看事件的发生位置,在View的可视范围dispatchTransformedTouchEvent()尝试把事件传递给该View,dispatchTransformedTouchEvent函数就是一个简单的对于事件的过滤,如果孩子是NULL的话,则把消息发送给自己,ViewGroup继承自View,调用View的dispatchTouchEvent,如果child是ViewGroup就递归调用在分发给自己的孩子.
最终递归到View的dispatchTouchEvent(MotionEvent ev),子View的dispatchTouchevent,就会调用onTouchEvent方法,只要该View可点击就代表消耗,可以重写它。之后其他的事件,就会直接传给接受了ACTION_DOWN事件的子View.

(哇格式全乱了,现在有点讨厌起了MarkDown)