本文主要记录了Android中的事件分发机制。通过对源码进行分析和实例测试,对Android事件分发机制有了更深的了解。主要为学习Android时的笔记。

更多文章欢迎访问我的个人博客–>幻境云图

1. 触发过程

1.1 点击控件

1.2 dispatchTouchEven

一定会执行dispatchTouchEvent方法,若当前类没有该方法,则向上往父类查找。

1
2
3
4
5
6
7
public boolean dispatchTouchEvent(MotionEvent event) {
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}
  • 条件1 mOnTouchListener != null
1
2
3
public void setOnTouchListener(OnTouchListener l) {
    mOnTouchListener = l;
}

给控件设置监听就会给mOnTouchListener赋值,则条件1成立。

  • 条件2 (mViewFlags & ENABLED_MASK) == ENABLED 控件是否是可点击的

  • 条件3 mOnTouchListener.onTouch(this, event) 回调onTouch方法,返回true 则成立

**小结:**dispatchTouchEvent 方法中一定会执行onTouch方法,如果onTouch方法返回true 则dispatchTouchEvent方法直接返回true 不会执行if外的 return onTouchEvent(event)

1
2
3
4
5
6
7
       title_bar.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
			Log.v("Az","onTouch");
                return false;
            }
        });

setOnTouchListener时,onTouch方法默认返回false,所以才会执行后面的onTouchEvent方法;

1.3 onTouchEvent

 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
public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
                if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
                    // take focus if we don't have it already and we should in
                    // touch mode.
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }
                    if (!mHasPerformedLongPress) {
                        // This is a tap, so remove the longpress check
                        removeLongPressCallback();
                        // Only perform take click actions if we were in the pressed state
                        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();
                            }
                        }
                    }
                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }
                    if (prepressed) {
                        mPrivateFlags |= PRESSED;
                        refreshDrawableState();
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        // If the post failed, unpress right now
                        mUnsetPressedState.run();
                    }
                    removeTapCallback();
                }
                break;
            case MotionEvent.ACTION_DOWN:
                if (mPendingCheckForTap == null) {
                    mPendingCheckForTap = new CheckForTap();
                }
                mPrivateFlags |= PREPRESSED;
                mHasPerformedLongPress = false;
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                break;
            case MotionEvent.ACTION_CANCEL:
                mPrivateFlags &= ~PRESSED;
                refreshDrawableState();
                removeTapCallback();
                break;
            case MotionEvent.ACTION_MOVE:
                final int x = (int) event.getX();
                final int y = (int) event.getY();
                // Be lenient about moving outside of buttons
                int slop = mTouchSlop;
                if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                        (y < 0 - slop) || (y >= getHeight() + slop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();
                        // Need to switch from pressed to not pressed
                        mPrivateFlags &= ~PRESSED;
                        refreshDrawableState();
                    }
                }
                break;
        }
        return true;
    }
    return false;
}

首先在第14行我们可以看出,如果该控件是可以点击的就会进入到第16行的switch判断中去,而如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case当中。在经过种种判断之后,会执行到第38行的performClick()方法,那我们进入到这个方法里瞧一瞧:

若当前事件为抬手,则进入performClick方法

1
2
3
4
5
6
7
8
9
public boolean performClick() {
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    if (mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        mOnClickListener.onClick(this);
        return true;
    }
    return false;
}

如果 mOnClickListener != null 则会执行onClick方法,

1
2
3
4
5
6
public void setOnClickListener(OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    mOnClickListener = l;
}

所以只要给控件设置了点击监听,setOnClickListener就会给mOnClickListener赋值,上面条件就成立,然后回调onClick方法。

到这儿差不多就清楚了分发流程。

这样View的整个事件分发的流程就让我们搞清楚了!不过别高兴的太早,现在还没结束,还有一个很重要的知识点需要说明,就是touch事件的层级传递。我们都知道如果给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。这里需要注意,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public boolean onTouchEvent(MotionEvent event) { 
    //省略...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    break;
                case MotionEvent.ACTION_DOWN:      
                    break;
                case MotionEvent.ACTION_CANCEL:             
                    break;
                case MotionEvent.ACTION_MOVE:
                    break;
            }
            return true;
        }
}

可以看出在dispatchTouchEvent方法中,onTouch方法返回false,然后执行onTouchEvent方法,在进入if判断后,不管进入那个case,最后都会return true,所以才会执行后续的action.

**1. onTouch和onTouchEvent有什么区别,又该如何使用? **

都是dispatchTouchEvent中的方法,onTouch优先级高,若onTouch返回true,就会消费掉当前事件,onTouchEvent就不会执行。

要执行onTouch也需要两个条件,1 给控件设置了触摸监听OnTouchListener ,2该控件是可以点击的。

若控件是非enable的,则不会执行onTouch方法,会执行onTouchEvent,所以想要监听ouTouch事件只能重写onTouchEvent方法来实现。

小结:

控件被点击或触摸后一定会执行dispatchTouchEvent方法(当前类没有则去父类找),如果设置了触摸监听且控件是enable的,就执行onTouch方法 。OnTouch方法返回true则消耗掉本次事件,不执行后面的方法,返回false则执行onTouchEvent方法,如果设置了点击监听且控件是enable的,就在抬手的时候执行onClick方法。

给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

2. ViewGroup

Android中touch事件的传递,绝对是先传递到ViewGroup,再传递到View的

上边说只要你触摸了任何控件,就一定会调用该控件的dispatchTouchEvent方法。这个说法没错,只不过还不完整而已。实际情况是,当你点击了某个控件,首先会去调用该控件所在布局的dispatchTouchEvent方法,然后在布局的dispatchTouchEvent方法中找到被点击的相应控件,再去调用该控件的dispatchTouchEvent方法。

ViewGroupdispatchTouchEvent方法

 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
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 (disallowIntercept || !onInterceptTouchEvent(ev)) {
            ev.setAction(MotionEvent.ACTION_DOWN);
            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)) {
                        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))  {
                            mMotionTarget = child;
                            return true;
                        }
                    }
                }
            }
        }
    }
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
            (action == MotionEvent.ACTION_CANCEL);
    if (isUpOrCancel) {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    final View target = mMotionTarget;
    if (target == null) {
        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 (!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)) {
        }
        mMotionTarget = null;
        return true;
    }
    if (isUpOrCancel) {
        mMotionTarget = null;
    }
    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);
}

第二个if语句 if (disallowIntercept || !onInterceptTouchEvent(ev)

第一个条件disallowIntercept是否禁用掉事件拦截的功能,默认是false。所以是否进入if内部就由第二个条件决定了。

ViewGroup中有一个onInterceptTouchEvent方法 是否拦截触摸事件 默认返回false 即不拦截

1
2
3
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return false;
}

第二个条件 !onInterceptTouchEvent(ev)对返回值取反 即返回false不拦截触摸事件时进入if内部,返回true拦截时不进入if内部

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//省略。。。
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            for (int i = count - 1; i >= 0; i--) {//遍历当前ViewGroup下的所有子View
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    if (frame.contains(scrolledXInt, scrolledYInt)) {//判断当前遍历的View是不是正在点击的View
                        if (child.dispatchTouchEvent(ev))  {//是则调用子View的dispatchTouchEvent
                            mMotionTarget = child;
                            return true;
                        }
                    }
                }
            }
        }

if内部对子View进行了遍历,最终调用子View的dispatchTouchEvent,然后控件可点击那么dispatchTouchEvent一定会返回true,所以后面的代码就执行不了。

ViewGrouponInterceptTouchEvent返回false,不拦截触摸事件时,最终会执行子View的dispatchTouchEvent

ViewGrouponInterceptTouchEvent返回true,拦截触摸事件,就不会进入if内部,则会执行到后面的程序

1
2
3
4
5
6
7
8
if (target == null) {
        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);
    }

可以看到,最后会执行super.dispatchTouchEvent(ev),执行父类即View的dispatchTouchEvent

ViewdispatchTouchEvent如下:

1
2
3
4
5
6
7
public boolean dispatchTouchEvent(MotionEvent event) {
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}

然后又和前面的一样了。执行onTouch或者onTouchEvent。。

3. 总结

传递顺序

Activity -> PhoneWindow -> DecorView -> ViewGroup -> … -> View

通俗语言总结一下,事件来的时候,

Activity会询问Window,Window这个事件你能不能消耗,

Window一看,你先等等,我去问问DecorView他能不能消耗,

DecorView一看,onInterceptTouchEvent返回false啊,不让我拦截啊,

(DecorView继承自FrameLayout,FrameLayout是ViewGroup的子类,所以DecorView也是ViewGroup的子类,事件从Activity传到了ViewGroup)

遍历一下子View吧,问问他们能不能消耗,那个谁,事件按在你的身上了,你看看你能不能消耗,

假如子View为RelativeLayout

RelativeLayout一看,也没有让我拦截啊,我也得遍历看看这个事件发生在那个子View上面,

到这儿事件从ViewGroup传到View上了

那个TextView,事件在你身上,你能不能消耗了他。TextView一看,消耗不了啊,

RelativeLayout一看TextView消耗不了啊,mFirstTouchTarget==null啊,得,我自己消耗吧,嗯!一看自己的onTouchEvent也消耗不了啊!那个DecorView事件我消耗不了,

DecorView一看自己,我也消耗不了,继续往上传,那个Window啊。事件我消耗不了啊,

Window再告诉Activity事件消耗不了啊。

Activity还得我自己来啊。调用自己的onTouchEvent,还是消耗不了,算了,不要了。

最后Activity的onTouchEvent无论返回什么,事件分发都结束。(如果事件在边界范围外默认会返回false)

参考

https://blog.csdn.net/guolin_blog/article/details/9097463

https://blog.csdn.net/guolin_blog/article/details/9153747