Android Touch事件传递机制(三) -- Touch事件分发(dispatchTouchEvent)


(本文基于android-2.3.3_r1代码研究)

Android Touch事件传递机制(一)Android Touch事件传递机制(二)这两篇文章中研究了Android触屏事件的分发机制;本文从源码角度继续深入研究。


一. ViewRoot

Android的所有触屏事件、按键事件、界面刷新等事件都是通过ViewRoot进行分发的。

ViewRoot实际就是一个Handler,接收native层传来的事件并进行分发。

下面看看ViewRoot对触屏事件处理的关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public final class ViewRoot extends Handler implements ViewParent,
        View.AttachInfo.Callbacks {

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {

        case DISPATCH_POINTER: {
            MotionEvent event = (MotionEvent) msg.obj;
            try {
                deliverPointerEvent(event);
            } finally {
                event.recycle();
                if (msg.arg1 != 0) {
                    finishInputEvent();
                }

            }
        } break;

    }

}

ViewRoot接收到触屏事件,调用deliverPointerEvent方法。deliverPointerEvent方法关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void deliverPointerEvent(MotionEvent event) {

    boolean handled;

    boolean isDown = event.getAction() == MotionEvent.ACTION_DOWN;

    handled = mView.dispatchTouchEvent(event);

    if (!handled && isDown) {

        final int edgeFlags = event.getEdgeFlags();

        if (edgeFlags != 0 && mView instanceof ViewGroup) {
            View nearest = FocusFinder.getInstance().findNearestTouchable(
                    ((ViewGroup) mView), x, y, direction, deltas);
            if (nearest != null) {

                mView.dispatchTouchEvent(event);
            }
        }
    }

}

可以看到最终事件会分发给mView的dispatchTouchEvent方法进行处理。mView是一个窗口关联的View树的最顶层View,通过ViewRoot的setView方法可以设置mView的值。

下面我们看下View和ViewGroup的dispatchTouchEvent方法。


二. View.dispatchTouchEvent()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    if (!onFilterTouchEventForSecurity(event)) {
        return false;
    }

    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}

View的dispatchTouchEvent方法处理逻辑很简单:

当View可点击且设置了OnTouchListener时,首先回调OnTouchListener接口的onTouch方法;

当onTouch方法返回true时,dispatchTouchEvent方法直接返回true;

当onTouch方法返回false时,dispatchTouchEvent方法再调用View的onTouchEvent方法,并返回onTouchEvent方法的返回值

这个逻辑对应了文章2中Demo的演示结果


三. ViewGroup.dispatchTouchEvent()

ViewGroup中的dispatchTouchEvent方法就要复杂的多了,关键代码如下:

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
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    final int action = ev.getAction();
    final float xf = ev.getX();
    final float yf = ev.getY();
    final float scrolledXFloat = xf + mScrollX;
    final float scrolledYFloat = yf + mScrollY;
    final Rect frame = mTempRect;

    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

    if (action == MotionEvent.ACTION_DOWN) {
        if (mMotionTarget != null) {
            mMotionTarget = null;
        }
        // If we're disallowing intercept or if we're allowing and we didn't
        // intercept
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            // reset this event's action (just to protect ourselves)
            ev.setAction(MotionEvent.ACTION_DOWN);
            // We know we want to dispatch the event down, find a child
            // who can handle it, start with the front-most child.
            final int scrolledXInt = (int) scrolledXFloat;
            final int scrolledYInt = (int) scrolledYFloat;
            final View[] children = mChildren;
            final int count = mChildrenCount;

            for (int i = count - 1; i >= 0; i--) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    child.getHitRect(frame);
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        // offset the event to the view's coordinate system
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                        if (child.dispatchTouchEvent(ev))  {
                            // Event handled, we have a target now.
                            mMotionTarget = child;
                            return true;
                        }
                        // The event didn't get handled, try the next view.
                        // Don't reset the event's location, it's not
                        // necessary here.
                    }
                }
            }
        }
    }

    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
            (action == MotionEvent.ACTION_CANCEL);

    if (isUpOrCancel) {
        // Note, we've already copied the previous state to our local
        // variable, so this takes effect on the next event
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }

    // The event wasn't an ACTION_DOWN, dispatch it to our target if
    // we have one.
    final View target = mMotionTarget;
    if (target == null) {
        // We don't have a target, this means we're handling the
        // event as a regular view.
        ev.setLocation(xf, yf);
        if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        }
        return super.dispatchTouchEvent(ev);
    }

    // if have a target, see if we're allowed to and want to intercept its
    // events
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        ev.setAction(MotionEvent.ACTION_CANCEL);
        ev.setLocation(xc, yc);
        if (!target.dispatchTouchEvent(ev)) {
            // target didn't handle ACTION_CANCEL. not much we can do
            // but they should have.
        }
        // clear the target
        mMotionTarget = null;
        // Don't dispatch this event to our own view, because we already
        // saw it when intercepting; we just want to give the following
        // event to the normal onTouchEvent().
        return true;
    }

    if (isUpOrCancel) {
        mMotionTarget = null;
    }

    // finally offset the event to the target's coordinate system and
    // dispatch the event.
    final float xc = scrolledXFloat - (float) target.mLeft;
    final float yc = scrolledYFloat - (float) target.mTop;
    ev.setLocation(xc, yc);

    if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
        ev.setAction(MotionEvent.ACTION_CANCEL);
        target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        mMotionTarget = null;
    }

    return target.dispatchTouchEvent(ev);
}

11行代码判断是否允许当前ViewGroup拦截touch事件;当允许拦截touch事件时,onInterceptTouchEvent方法才会起到拦截touch事件的作用;使用requestDisallowInterceptTouchEvent方法可以设置是否允许拦截touch事件

19行代码处,down事件在允许ViewGroup拦截touch事件时,先将down事件分发给ViewGroup的onInterceptTouchEvent方法处理;

当ViewGroup的onInterceptTouchEvent方法返回false时,再将down事件分发给子View;从最前方的子View到最底层的子View,依次调用子View的dispatchTouchEvent方法;若子View的dispatchEvent方法返回true,表示这个子View消费掉这次down事件,设置这个View为目标View(mMotionTarget),并返回true,不再继续分发;若所有的子View都不消费掉这次down事件,则mMotionTarget为null,将该ViewGroup当做普通View,调用super.dispatchTouchEvent方法。

若事件不是down事件,或不允许该ViewGroup拦截touch事件,或ViewGroup的onInterceptTouchEvent方法返回了true,则从54行代码处执行。

66行代码处,mMotionTarget为null,表明没有子View消费掉down事件,则将该ViewGroup当做普通View,调用super.dispatchTouchEvent方法。

执行到79行代码处表明有子View消费了之前的down事件;后续的事件交由目标View处理;79行代码处,当允许ViewGroup拦截touch事件时,首先将后续事件分发到onInterceptTouchEvent方法中,如果onInterceptTouchEvent方法返回true,表示ViewGroup拦截后续事件,则mMotionTarget将接收到cancel事件,mMotionTarget同时被设置为null,这样后续事件不再分发给mMotionTarget;onInterceptTouchEvent返回false,事件继续传递;

97行代码处,接收到up事件或cancel事件,将事件分发给目标View,同时清除mMotionTarget,一次完整触屏操作就此结束。


四. Activity.dispatchTouchEvent

前面分析了View以及ViewGroup对touch事件的分发处理机制,下面我们继续研究下Activity的dispatchTouchEvent方法。

Activity中提供了dispatchTouchEvent方法,这个方法又是什么时候被调用的呢?

我们知道在ViewRoot中touch首先分发给mView来处理的,那么mView到底是什么呢?

我们在创建一个Activity时,经常调用Activity的setContentView方法来显示自定义的布局文件:

1
2
3
4
5
6
7
8
9
/**
 * Set the activity content from a layout resource.  The resource will be
 * inflated, adding all top-level views to the activity.
 * 
 * @param layoutResID Resource ID to be inflated.
 */
public void setContentView(int layoutResID) {
    getWindow().setContentView(layoutResID);
}

这里getWindow获取的是PhoneWindow的实例,其setContentView如下:

1
2
3
4
5
6
7
8
9
10
11
@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();
    } else {
        mContentParent.removeAllViews();
    }
    mLayoutInflater.inflate(layoutResID, mContentParent);

    ......
}

可以看到传入的布局文件最终会展开并作为mContentParent的子View。那么mContentParent又是怎么产生的?看下installDecor方法:

1
2
3
4
5
6
7
8
9
10
11
12
private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor();
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);

        ......
    }
}

在installDecor方法中,创建了一个DecorView的实例并赋值给mDecor变量。然后在第8行调用generateLayout并传入mDecor变量:

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
protected ViewGroup generateLayout(DecorView decor) {
    // Apply data from current theme.

    ......

    // Inflate the window decor.

    int layoutResource;
    int features = getLocalFeatures();

    if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        if (mIsFloating) {
            layoutResource = com.android.internal.R.layout.dialog_title_icons;
        } else {
            layoutResource = com.android.internal.R.layout.screen_title_icons;
        }
        // System.out.println("Title Icons!");
    } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0) {
        // Special case for a window with only a progress bar (and title).
        // XXX Need to have a no-title version of embedded windows.
        layoutResource = com.android.internal.R.layout.screen_progress;
        // System.out.println("Progress!");
    } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
        // Special case for a window with a custom title.
        // If the window is floating, we need a dialog layout
        if (mIsFloating) {
            layoutResource = com.android.internal.R.layout.dialog_custom_title;
        } else {
            layoutResource = com.android.internal.R.layout.screen_custom_title;
        }
    } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
        // If no other features and not embedded, only need a title.
        // If the window is floating, we need a dialog layout
        if (mIsFloating) {
            layoutResource = com.android.internal.R.layout.dialog_title;
        } else {
            layoutResource = com.android.internal.R.layout.screen_title;
        }
        // System.out.println("Title!");
    } else {
        // Embedded, so no decoration is needed.
        layoutResource = com.android.internal.R.layout.screen_simple;
        // System.out.println("Simple!");
    }

    mDecor.startChanging();

    View in = mLayoutInflater.inflate(layoutResource, null);
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

    ......

    mDecor.finishChanging();

    return contentParent;
}

11行到44行是根据当前的主题(theme)来展开不同的布局文件,然后在48、49行将展开的布局添加到传入的mDecorView中。然后在51行找到mDecorView中id为Window.ID_ANDROID_CONTENT(com.android.internal.R.id.content)的View并返回,最终设置为mContentParent变量。

可以看出,Activity中,最上层为DecorView,mContentParent为DecorView中id为Window.ID_ANDROID_CONTENT的子View,而我们通过setContentView方法传入的布局文件则是mContentParent的子View。Activity在显示时,会设置ViewRoot中的mView变量:

1
2
3
4
5
6
7
8
void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

getWindowManager方法返回的是LocalWindowManager类的实例,其内部封装了一个WindowManagerImpl实例。LocalWindowManager的addView方法最终调用了WindowManagerImpl的addView方法:

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
private void addView(View view, ViewGroup.LayoutParams params, boolean nest)
{

    final WindowManager.LayoutParams wparams
            = (WindowManager.LayoutParams)params;
    
    ViewRoot root;
    View panelParentView = null;
    
    synchronized (this) {

        int index = findViewLocked(view, false);
        if (index >= 0) {
        ......

            return;
        }

        root = new ViewRoot(view.getContext());
        root.mAddNesting = 1;

        view.setLayoutParams(wparams);

        if (mViews == null) {
            index = 1;
            mViews = new View[1];
            mRoots = new ViewRoot[1];
            mParams = new WindowManager.LayoutParams[1];
        } else {
            index = mViews.length + 1;
            Object[] old = mViews;
            mViews = new View[index];
            System.arraycopy(old, 0, mViews, 0, index-1);
            old = mRoots;
            mRoots = new ViewRoot[index];
            System.arraycopy(old, 0, mRoots, 0, index-1);
            old = mParams;
            mParams = new WindowManager.LayoutParams[index];
            System.arraycopy(old, 0, mParams, 0, index-1);
        }
        index--;

        mViews[index] = view;
        mRoots[index] = root;
        mParams[index] = wparams;
    }
    // do this last because it fires off messages to start doing things
    root.setView(view, wparams, panelParentView);
}

在19行处创建了ViewRoot的实例,然后在48行处调用了ViewRoot的setView方法设置了ViewRoot的mView变量为Activity的mDecorView。

因此对应Activity而言,touch事件首先分发给DecorView的dispatchTouchEvent方法。看下DecorView的dispatchTouchEvent方法:

1
2
3
4
5
6
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    final Callback cb = getCallback();
    return cb != null && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super
            .dispatchTouchEvent(ev);
}

这里的Callback就是Activity自身,因此touch事件从DecorView的dispatchTouchEvent方法传递到Activity的dispatchTouchEvent方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * Called to process touch screen events.  You can override this to
 * intercept all touch screen events before they are dispatched to the
 * window.  Be sure to call this implementation for touch screen events
 * that should be handled normally.
 * 
 * @param ev The touch screen event.
 * 
 * @return boolean Return true if this event was consumed.
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

Activity的dispatchTouchEvent方法接收到touch事件,在15行处分发给PhoneWindow的superDispatchTouchEvent方法;PhoneWindow再将touch事件分发给DecorView的superDispatchTouchEvent方法;而DecorView则调用super.dispatchTouchEvent方法处理touch事件;

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

DecorView是FrameLayout的子类,这样最终touch事件分发给ViewGroup的dispatchTouchEvent方法来处理。


相关文章

Android Touch事件传递机制(一) – onInterceptTouchEvent & onTouchEvent

Android Touch事件传递机制(二) – OnTouchListener & OnClickListener & OnLongClickListener

Android Touch事件传递机制(四) – Touch事件处理(onTouchEvent)


参考

Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

Android事件分发机制完全解析,带你从源码的角度彻底理解(下)



—  我的个人空间 |   —