Android View事件分发机制源码解析

对于View的事件分发,涉及的有dispatchTouchEvent、onTouchEvent、onTouch、onClick
为了更好的查看View的事件转发,我们先来看个demo
首先定义一个自定义View

public class CustomView extends AppCompatButton {

    private static final String TAG = "CustomView";

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                LogUtils.e(TAG,"dispatchTouchEvent=ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                LogUtils.e(TAG,"dispatchTouchEvent=ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                LogUtils.e(TAG,"dispatchTouchEvent=ACTION_UP");
                break;
        }
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                LogUtils.e(TAG,"onTouchEvent=ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                LogUtils.e(TAG,"onTouchEvent=ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                LogUtils.e(TAG,"onTouchEvent=ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }
}

把自定义的按钮添加到布局中:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.xiaoma.restudy.customviews.CustomView
            android:id="@+id/mbt_cmb"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Custom View" />
    </LinearLayout>

</android.support.constraint.ConstraintLayout>

最后看CustomActivity代码

public class CustomActivity extends BaseActivity {
    private CustomView mMbtCmb;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_custom);
        initView();
    }

    @SuppressLint("ClickableViewAccessibility")
    private void initView() {
        mMbtCmb = (CustomView) findViewById(R.id.mbt_cmb);
        mMbtCmb.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        LogUtils.e(TAG, "onTouch=ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        LogUtils.e(TAG, "onTouch=ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        LogUtils.e(TAG, "onTouch=ACTION_UP");
                        break;
                }
                return false;
            }
        });
        mMbtCmb.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LogUtils.e(TAG, "onClick");
            }
        });

        mMbtCmb.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                LogUtils.e(TAG, "onLongClick");
                return false;
            }
        });
    }
}

点击按钮后,运行的结果如下

 E/CustomView: dispatchTouchEvent=ACTION_DOWN
E/.customviews.CustomActivity: onTouch=ACTION_DOWN
 E/CustomView: onTouchEvent=ACTION_DOWN
 E/CustomView: dispatchTouchEvent=ACTION_MOVE
E/.customviews.CustomActivity: onTouch=ACTION_MOVE
 E/CustomView: onTouchEvent=ACTION_MOVE
E/.customviews.CustomActivity: onLongClick
 E/CustomView: dispatchTouchEvent=ACTION_MOVE
E/.customviews.CustomActivity: onTouch=ACTION_MOVE
 E/CustomView: onTouchEvent=ACTION_MOVE
 E/CustomView: dispatchTouchEvent=ACTION_UP
E/.customviews.CustomActivity: onTouch=ACTION_UP
 E/CustomView: onTouchEvent=ACTION_UP
E/.customviews.CustomActivity: onClick

点击下就可以出现上面日志,否则出现一系列ACTION_MOVE事件;但总结来看是ACTION_DOWN、ACTION_MOVE、ACTION_UP三个事件的传递是dispatchTouchEvent-->onTouch-->onTouchEvent-->onLongClick-->onClick
查看源码从View的dispatchTouchEvent方法开始,摘抄代码如下

  if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
           //上面没有没有消费,在onTouchEvent中返回为true进行消费。
            if (!result && onTouchEvent(event)) {
                result = true;
            }

1、如果onTouchListener不等于null,并且是enabled,且onTouch返回true,则消费掉事件
2、上面如果没有消费,则执行onTouchEvent,如果返回true,则消费掉事件

总结:也就是说如果onTouch方法返回ture,则不执行onTouchEvent方法

View的onTouchEvent方法如下,摘要主要功能如下

 final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        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;
        }
   if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
     switch (action) {
                case MotionEvent.ACTION_UP:
      break;
                case MotionEvent.ACTION_DOWN:
      break;
                case MotionEvent.ACTION_CANCEL:
      break;
                case MotionEvent.ACTION_MOVE:
      break;
    return  true;
}

1.clickable变量标识如果view是可点击或者长按的状态就是true;
2.如果view是disabled状态,不处理事件,返回clickable变量;意味着一个disabled状态的view,如果可点击或者长按,还是消费事件,但是不响应
3.如果mTouchDlegate不为空,则当前view不消费事件,交给触摸代理消费事件;
4.如果clickable返回true或者view可以在悬停或者长按时显示工具,则view消耗此事件;

查看MotionEvent.ACTION_DOWN事件,源码如下

  case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }
                    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, x, y);
                    }
                    break;
  1. mHasPerformedLongPress = false; 长按事件还未触发
  2. boolean isInScrollingContainer = isInScrollingContainer(); 判断是否是个滑动容器
  3. 如果是滑动容器,设置 mPrivateFlags |= PFLAG_PREPRESSED; 设置为预按状态;发送一个延迟为ViewConfiguration.getTapTimeout()=100的消息;如果非滑动容器,则直接设置为按压状态,且发送延迟500后执行CheckForLongPress()
  4. 到达延迟时间后,执行CheckForTap()里的run()方法,取消预按状态,且在setPressed(true, x, y)方法里设置按压状态 mPrivateFlags |= PFLAG_PRESSED;且检查是否有长按事件,如果有再发送一个延迟为ViewConfiguration.getLongPressTimeout()=500的消息(减去上面的延迟时间),到达延迟时间后,执行CheckForLongPress()

总结:100用来检测是否按压了,500时长用来检测是否长按

  1. 如果设置了长按的回调;如果长按的返回值为true,则设置 mHasPerformedLongPress = true;如果没有设置长按回调或者返回false,则mHasPerformedLongPress依旧等于false;
 private final class CheckForTap implements Runnable {
        public float x;
        public float y;

        @Override
        public void run() {
            mPrivateFlags &= ~PFLAG_PREPRESSED;
            setPressed(true, x, y);
            checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
        }
    }
##################################
  private void checkForLongClick(int delayOffset, float x, float y) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            mPendingCheckForLongPress.setAnchor(x, y);
            mPendingCheckForLongPress.rememberWindowAttachCount();
            mPendingCheckForLongPress.rememberPressedState();
            postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
        }
    }
#################
 private final class CheckForLongPress implements Runnable {
        private int mOriginalWindowAttachCount;
        private float mX;
        private float mY;
        private boolean mOriginalPressedState;

        @Override
        public void run() {
            if ((mOriginalPressedState == isPressed()) && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                if (performLongClick(mX, mY)) {
                    mHasPerformedLongPress = true;
                }
            }
        }

查看MotionEvent.ACTION_MOVE事件,源码如下:

 case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
判断当前坐标是否移除了当前的view,pointInView(x, y, mTouchSlop),如果移出,则
  1. 执行removeTapCallback(),移出是滑动容易内,则设置 mPrivateFlags &= ~PFLAG_PREPRESSED且不到100移除回调
  2. 执行 removeLongPressCallback(),移除长按回调
  3. 如果是按压状态,则设置 mPrivateFlags &= ~PFLAG_PRESSED

总结:只要用户移出了我们的控件:则将mPrivateFlags取出PRESSED标识,且移除所有在DOWN中设置的检测,长按等;

查看MotionEvent.ACTION_UP事件,源码如下:

case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    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)) {
                                    performClickInternal();
                                }
                            }
                        }

                        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;
  1. 如果mPrivateFlags包含PFLAG_PREPRESSED或者PFLAG_PRESSED则进入if执行语句,也就是无论100ms内或者之后抬起都会进入if语句执行
  2. 如果mHasPerformedLongPress是false,即长按没有执行,则进入方法体,移除长按回调
  3. 如果mPerformClick为null,则初始化一个实例;然后立即通过Handler添加到消息队列尾部,如果添加失败,直接执行performClick()
    public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        notifyEnterOrExitForAutoFillIfNeeded(true);
        return result;
    }
  1. 如果mOnClickListener!=null,则回调onClick()方法
  2. 如果prepressed为true,则执行mUnsetPressedState.run()方法。我们的mPrivateFlags中的PRESSED取消,如果mPendingCheckForTap不为null,移除;
总结

1.整个view的事件分发流程是:
View.dispatchTouchEvent->View.setOnTouchListener->View.onTouchEvent
在dispatchTouchEvent中会进行onTouchListener的判断,如果不为null且返回ture,则整个事件被消费,onTouchEvent不会执行,否则执行onTouchEvent

  1. onTouchEvent中的DOWN、MOVE、UP
DOWN时

1、首先设置mHasPerformedLongPress=false,代表长按是false;
2、如果在滑动容器内,首先设置mPrivateFlags=PFLAG_PREPRESSED且发送一个延迟100ms的mPendingCheckForTap,则将标识位mPrivateFlags=PFLAG_PRESSED,清除PFLAG_PREPRESSED标识,同时发送一个延迟500-100ms的mPendingCheckForLongPress,检测长按任务消息
3、如果不在滑动容器内容,则直接设置mPrivateFlags=PFLAG_PRESSED且发送一个延迟500ms的mPendingCheckForLongPress的检测长按任务消息
4、如果时间超过500ms,则触发mOnLongClickListener;如果mOnLongClickListener不为null,且mOnLongClickListener.onLongClick返回true,则mHasPerformedLongPress=true;否则mHasPerformedLongPress还是false

MOVE时

主要就是检测是否划出了View控件,如果划出了
直接移除mPendingCheckForTap,mPendingCheckForLongPress;如果100ms后mPrivateFlags==PFLAG_PRESSED,则清除mPrivateFlags值

UP时
  1. 如果mPrivateFlags=PFLAG_PREPRESSED,即100ms内触发up,则依次执行 setPressed(true, x, y)、mUnsetPressedState
    2、如果mHasPerformedLongPress==false,代表长按未触发,移除长按检测,执行onClick回调
    3.500ms后,如果设置了onLongClickListener,且onLongClickListener.onClick返回true,则点击事件OnClick无法触发;如果没有设置onLongClickListener或者onLongClickListener.onClick返回false,则点击事件OnClick依旧可以触发
  2. 所以onLongClickListener是在move触发,onClick是在up触发
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350