Android View事件传递机制

最近在开发中遇到view滑动冲突的问题,由于一开始就知道这个问题与view事件分发有关,之后在网上看了几篇关于事件分发的资料后,开发中遇到的问题很快便得到解决。 
在这里总结一下我对view事件分发的理解。

首先,看下事件分发流程图: 
这里写图片描述

Button事件演示

在对view的事件分发机制进行分析前,我们可以通过一个demo看看Button事件处理的流程。

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        button = (Button) findViewById(R.id.button);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e("zhangke", "button->onClick");
            }
        });

        button.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {

                Log.e("zhangke", "button->onTouch " + event.getAction());

                return false;
            }
        });

    }
  • 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
  • 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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.touchdemo.MainActivity">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button事件演示" />

</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

当OnTouchListener.onTouch方法 return false,日志如下:

11-09 09:02:52.811 2241-2241/com.example.touchdemo E/zhangke: button->onTouch 0
11-09 09:02:52.811 2241-2241/com.example.touchdemo E/zhangke: button->onTouch 1
11-09 09:02:52.818 2241-2241/com.example.touchdemo E/zhangke: button->onClick
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

当OnTouchListener.onTouch方法 return true:日志如下:

11-09 09:05:02.931 2340-2340/com.example.touchdemo E/zhangke: button->onTouch 0
11-09 09:05:02.936 2340-2340/com.example.touchdemo E/zhangke: button->onTouch 1
  • 1
  • 2
  • 1
  • 2

这里可以得出一下结论:

  • 如果view为disenable,则:onTouchListener里面不会执行,但是会执行onTouchEvent(event)方法
  • 如果onTouchListener的onTouch方法返回了true,那么view里面的onTouchEvent就不会被调用了。
  • onTouchEvent方法中的ACTION_UP分支中触发onclick事件监听

View事件传递源码解析

在分析View的事件传递时,我们可以明确一下View事件传递的顺序:

  1. dispatchTouchEvent()
  2. onTouchListener–>onTouch方法
  3. onTouchEvent()
  4. onClickListener–>onClick方法

所以我们首先通过dispatchTouchEvent方法开始分析(代码给予sdk22)

dispatchTouchEvent方法解析

以下是View中dispatchTouchEvent方法代码:

   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()) {
                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)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;

            /**
             * 1、View的事件机制
             *      mOnTouchListener != null :是否设置了OnTouchListener监听事件
             *      (mViewFlags & ENABLED_MASK) == ENABLED :判断控件是否是enable,默认true
             *      li.mOnTouchListener.onTouch(this, event):OnTouchListener.onTouch方法返回值是否为true
             * 
             *   当这三个条件成立时,onTouchEvent方法不会被执行。
             *      
             *   总结:
             *      1、OnTouchListener的优先级别高于onTouchEvent(便于开发者自己通过OnTouchListener处理事件)
             *      2、与demo中的结论相符合,View首先执行OnTouchListener.onTouch方法,如果该方法return false,
             *         那么将执行onClick方法,但是在dispatchTouchEvent方法中并没有onClick方法相关的代码,
             *         这说明onClick方法是在onTouchEvent方法中执行的。
             *       
             */
            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;
    }
  • 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
  • 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

onTouchEvent源码解析:

    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        /**
         * 1、首先判断View是否是可用的
         *    如果View是不可用,同样会响应事件,但是此时是否消耗事件有View是否可点击决定
         */
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // 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)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        /**
         * 2、当View的CLICKABLE或LONG_CLICKABLE为true时,View将消耗此事件,该逻辑判断内return true;
         *    当View不能点击时,View将不消耗此事件,return false
         *
         *
         */
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_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 (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // 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)) {
                                    /**
                                     * 3、ACTION_UP事件发生时,会调用performClick方法,该方法内部执行onClick方法
                                     */
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
    }
  • 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
  • 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
    public boolean performClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        if (mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            mOnClickListener.onClick(this);
            return true;
        }

        return false;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在该方法中可以看到mOnClickListener.onClick(this)被调用了,到此处我们可以确认Button的click事件就是在onTouchEvent方法中调用的

通过onTouchEvent方法,我们可以得出结论:

  1. View的onClick方法是在ACTION_UP事件中被执行的。
  2. 当View为可点击时,onTouchEvent方法才会返回true,这表示该View会消耗此事件,否则返回false,表示View不消耗此事件。

TextView事件传递分析

在上面的源码解析中,我们反复提到事件被消耗了,那么应该怎么理解这个消耗的概念呢?

其实通过上面的分析,我们不难得出结论只有当dispatchTouchEvent方法的返回值为true时,就表示事件被View消耗了。 
而决定dispatchTouchEvent的返回值有两种情况:

  • OnTouchListener.onTouch的返回值
  • onTouchEvent方法的返回值

OnTouchListener.onTouch的返回值我们可以手动控制,而onTouchEvent方法的返回值我们经过上面分析,我们可以狭义的理解成是由View的CLICKABLE和LONG_CLICKABLE决定的。

因为TextView默认是不可点击的,这里我们通过TextView来详细说明一下这个问题。

        TextView textView = (TextView) findViewById(R.id.textview);

        textView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e("zhangke", "textview->onTouch " + event.getAction());

                return true;
            }
        });

        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e("zhangke", "textview->onClick ");
            }
        });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

首先,给TextView设置OnTouchListener方法(未设置OnClickListener监听,此时默认TextView不可点击),并改变onTouch方法的返回值,我们可以看下Log的具体输出:

    // 1、当onTouch方法return true,未设置onClick事件,日志如下
    11-10 10:46:24.128 23049-23049/? E/zhangke: textview->onTouch 0
    11-10 11:02:20.450 30674-30674/? E/zhangke: textview->onTouch 2
    11-10 10:46:24.195 23049-23049/? E/zhangke: textview->onTouch 1

    // 2、当onTouch方法return false,未设置onClick事件,日志如下
    11-10 10:47:27.093 23849-23849/? E/zhangke: textview->onTouch 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

给TextView设置OnClickListener监听,并改变并改变onTouch方法的返回值,我们可以看下Log的具体输出:

    // 3、当onTouch方法return true,设置onClick事件后,日志如下
    11-10 11:03:23.596 31065-31065/? E/zhangke: textview->onTouch 0
    11-10 11:02:20.450 30674-30674/? E/zhangke: textview->onTouch 2
    11-10 11:03:23.642 31065-31065/? E/zhangke: textview->onTouch 1

    // 4、当onTouch方法return false,设置onClick事件后,日志如下
    11-10 11:02:20.387 30674-30674/? E/zhangke: textview->onTouch 0
    11-10 11:02:20.450 30674-30674/? E/zhangke: textview->onTouch 2
    11-10 11:02:20.450 30674-30674/? E/zhangke: textview->onTouch 1
    11-10 11:02:20.451 30674-30674/? E/zhangke: textview->onClick 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

现在我们对上面日志的四种情况具体分析一下:

  1. onTouch 返回 true,未设置onClick事件,此时会执行DOWN、MOVE、UP事件–>dispatchTouchEvent返回true
  2. onTouch 返回 false,未设置onClick事件,此时会执行DOWN事件–>dispatchTouchEvent返回false
  3. onTouch 返回 true,设置onClick事件,此时会执行DOWN、MOVE、UP事件–>dispatchTouchEvent返回true
  4. onTouch 返回 true,设置onClick事件,此时会先执行DOWN、UP事件,然后执行onClick事件–>dispatchTouchEvent返回true

对于1、3、4这三种情况我们通过源码分析都能理解,但是对于情况2 仅仅响应了DOWN,而没有响应MOVE和UP事件,是不是感觉很奇怪?

这里可以给大家一个结论: 
如果View响应了DOWN事件,那么它必须要消耗该事件(dispatchTouchEvent返回true),否则它将不会接收到MOVE和UP事件。

具体原因: 
这是因为dispatchTouchEvent的返回值引起的,具体就是当View在响应DOWN事件时,dispatchTouchEvent的返回值将决定View的父控件是否拦截MOVE、UP事件。

  • 返回值为 true,不拦截MOVE、UP事件。
  • 返回值为 false,拦截MOVE、UP事件。

关于ViewGroup的事件传递机制,将在下一节中讲解,这里不做过多讨论。

posted @ 2017-03-12 19:02  天涯海角路  阅读(1626)  评论(0)    收藏  举报