Android事件分发流程

概述

最近在复习androidUI的相关知识点,在网上看了一些关于事件分发的文章与视频,因此在此记录一下自己的理解过程。

首先思考2个问题:

  1. 什么是事件?
  2. 什么是事件分发机制?

用户与手机的交互会产生一系列的事件 ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL。每一个事件都会封装成一个ACTIONEVENT.

事件分发主要解决由谁来处理事件的问题,当你点击Actiivty上的某一个控件时实际点击了不止一个控件,按照下图的显示。
在这里插入图片描述
当用户点击了view1时,实际上用户也点击到了ViewGroupA、RootView、DecorView,那事件到底交由哪个控件去处理?另外事件是由一系列的事件序列组成的,从DOWN–>MOVE–>UP,无法割裂开来,要么被忽略,要么都被某一个view所消费。

事件分发流程

如果在屏幕点击了一个控件View,最终这个控件View消费了这个事件的话,那大概的分发流程应该是Activity–>ViewGroup–>View。

针对分发流程我们需要关注下面几个方法:
dispatchTouchEvent() //分发触摸事件 (dispath 就是分发的意思)
onInterceptTouchEvent() // 拦截触摸事件 (Intercept 拦截的意思)
onTouchEvent() // 执行触摸事件 (on 可以理解为执行)

无论是Activity,View,ViewGroup都会调用dispatchTouchEvent 与 onTouchEvent。而onInterceptTouchEvent比较特殊,只有在ViewGroup中有。

先看前面一段事件是如何从Activity传递到ViewGroup的。
在这里插入图片描述
当我们查看Activity的dispatchTouchEvent()方法,向上追溯可以看到如下的调用路径:
Activity–>PhoneWindow–>DecorView–>ViewGroup
Activity

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

PhoneWindow

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

DecorView

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

ViewGroup的dispatchTouchEvent()比较长,但总体执行逻辑可以按照下面伪代码表示

public boolean dispatchTouchEvent(MotionEvent ev) {
	boolean consume = false;
	if(onInterceptTouchEvent(ev)){
		//拦截 将事件交给onTouchEvent()处理
		consume = onTouchEvent(ev);
	}else{
		//不拦截 将事件分发给子view
		consuem = child.dispatchTouchEvent(ev);
	}
	return consume;
}

这里分发的流程就从ViewGrop分发到了View的DispatchTouchEvent.

看下View的DispatchTouchEvent 方法:

public boolean dispatchTouchEvent(MotionEvent event) {       
		//一系列操作..
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            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;
            }
        }
        //又是一系列操作
        return result;
    }

上述代码只需要关注2个方法 一个onTouch方法,一个onTouchEvent()方法。了解onTouch方法是在onTouchEvent()之前执行,假如设置了View的mOnTouchListener,那onTouch()方法就会执行。

下面看下View的onTouchEvent()方法,看下它是如何来执行触摸事件的。

public boolean onTouchEvent(MotionEvent event) {
		//一系列操作1
		final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
       	//一系列操作2
        if (clickable) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    //一系列操作3
                    break;

                case MotionEvent.ACTION_DOWN:
                    //一系列操作4
                    break;

                case MotionEvent.ACTION_CANCEL:
                    //一系列操作5
                    break;

                case MotionEvent.ACTION_MOVE:
                    //一系列操作6
                    break;
            }

            return true;
        }

        return false;
    }

事件最终分发到View(View消费掉事件)

在onTouchEvent方法中,假如view是可以点击或者长按,无论是DOWN,UP ,MOVE,还是CANCEL 方法都会返回true,那就表示这个事件在view中被消费掉。那view的dispatchTouchEvent() 也会返回ture,继续往下追溯,ViewGroup的DispatchTouchEvent()返回true,Activity的dispatchTouchEvent() 返回true。

Activity PhoneWindow DecorView ViewGroup View dispatchTouchEvent() true superDispatchTouchEvent() superDispatchTouchEvent() 调用dispatchTouchEvent() 调用dispatchTouchEvent() onTouchEvent()返回true dispatchTouchEvent()返回true dispatchTouchEvent()返回true dispatchTouchEvent()方法返回true Activity PhoneWindow DecorView ViewGroup View

事件最终分发到View(View没有消费掉事件)

假如View的没有消费掉事件(返回false),往前回溯,那View的dispatchTouchEvent() 也会返回false,ViewGroup的dispatchTouchEvent()返回false,事件最终会通过Activity的dispatchTouchEvent()方法交由onTouchEvent()方法来处理。

Activity PhoneWindow DecorView ViewGroup View dispatchTouchEvent() true superDispatchTouchEvent() superDispatchTouchEvent() 调用dispatchTouchEvent() 调用dispatchTouchEvent() onTouchEvent()返回false dispatchTouchEvent()返回false dispatchTouchEvent()返回false dispatchTouchEvent()方法返回false dispatchTouchEvent() Activity PhoneWindow DecorView ViewGroup View

事件被VIewGroup拦截(ViewGroup消费掉事件):

当然上述的图默认表示的是ViewGroup的onInterceptTouchEvent()返回false的逻辑。假如ViewGroup的onInterceptTouchEvent()返回true。那代码很明显就要走ViewGroup的onTouchEvent()方法,此时如果子类重写了onTouchEvent()方法并消费了事件,方法调用关系如下图:

Activity PhoneWindow DecorView ViewGroup dispatchTouchEvent() true superDispatchTouchEvent() superDispatchTouchEvent() 调用dispatchTouchEvent() onInterceptTouchEvent()返回true onTouchEvent() 返回true dispatchTouchEvent()返回true dispatchTouchEvent()方法返回true Activity PhoneWindow DecorView ViewGroup

小结一下

假如ViewGroup拦截了View的事件,那么最终事件就被分发到ViewGroup的onTouchEvent() 方法中。
如果没有拦截,事件就被分发到了View的onTouchEvent()方法中。
此时的View默认是可以点击的(比如Button),那子View就默认消费了事件。如果子View不能点击(比如TextView),那子View就不会消费事件。

还剩下一个问题,之前说View要么不处理事件,要么就是整个事件序列一起处理,这部分是如何实现的?
这部分源码只看了个大概(没看太懂…)。
大致的流程:
在ViewGroup的dispatchTouchEvent() 方法中。如果接收到的事件是ACTION_DOWN时,会将处理DOWN事件的子View 保存到mFirstTouchTarget变量中。后面的MOVE,UP事件来的时候,通过调用dispatchTransformedTouchEvent(child)方法来执行child的dispatchTouchEvent()方法。

Click与LongClick事件

Click与LongClick事件是如何进行判断的呢?

Click事件对应较短时间内(<0.5s)的一次DOWN和UP事件。
LongClick事件对应较长事件(>0.5s)的一次DOWN事件和UP事件。

而DOWN跟UP事件都是发生在View的onTouchEvent()方法中。因此可以初步判断Click事件与LongClick事件都可以在View的onTouchEvent()事件中找到调用链。

public boolean onTouchEvent(MotionEvent event) {

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

        if (clickable ) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                   //一系列操作
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                      	//一系列操作
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                          	//一系列操作
                            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();
                                }
                            }
                        }

                        //一系列操作
                    break;

                case MotionEvent.ACTION_DOWN:
                   //一系列操作
                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }
               		//一系列操作
                    break;

                case MotionEvent.ACTION_CANCEL:
                    //一系列操作
                    break;

                case MotionEvent.ACTION_MOVE:
                   //一系列操作
                    break;
            }

            return true;
        }

        return false;
    }

通过上面的代码可以看到在UP事件中有一个mPerformClick对象;在DOWN事件中有个checkForLongClick()方法。通过名字我们似乎可以看出UP事件处理的Click相关的回调。而DOWN事件处理的LongClick相关的回调。

继续往下跟代码发现确实如此。在View的performClickInternal()方法

private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();

        return performClick();
    }

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;
    }

我们看到了li.mOnClickListener.onClick(this);

同样在也是在View的checkForLongClick()方法:

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);
        }
    }

CheckForLongPress() 对象中

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;
                }
            }
        }
        //...
 }

performLongClick()方法。继续往下多跟几个方法

private boolean performLongClickInternal(float x, float y) {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

        boolean handled = false;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
        //...
 }

最终可以看到li.mOnLongClickListener.onLongClick() 这一行代码。
基本可以验证我们上面的猜想。
在View的onTouchEvent() 方法的UP事件中调用了Click事件,在DOWN事件中调用了LongClick事件。

结论

  1. 一个时间序列从手指接触屏幕到手指离开屏幕,在这个过程产生的一系列事件,从DOWM事件开始,一系列的MOVE 事件,以UP事件结束
  2. 正常情况下,一个事件序列由一个view处理
  3. 某个ViewGroup一旦决定拦截,那么所有的事件序列都会由它的onTouchEvent()处理,并且它的onInterceptTouchEvent()不会再调用
  4. 某个View一旦开始处理事件,假如它没有消耗DOWN事件,那么同一事件序列的其它事件都不会再交给它处理。会交由它的父元素处理(父元素的onTouchEvent()被调用)
  5. 事件的传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View
  6. ViewGroup默认不拦截事件,即onInterceptTouchEvent默认返回false.View没有onInterceptTouchEvent方法,一旦有事件被拦截,那么它的onTouchEvent方法会被调用
  7. View的onTouchEvent默认会消耗事件(返回true),除非它是不可点击的(clickable与longClickable同时为false)。View的longClickable默认都是false,clickable要分情况,比如button的clickable默认是true,Textview的clickable默认是false
  8. View的enable属性不影响onTouchEvent()的默认返回值。哪怕一个View是disable状态,onTouchEvent()的返回值只跟clickable 与 longClickable 相关
  9. onClick的响应的前提是view是可点击的,并且收到了DOWN与UP事件,并且受长按事件的影响,当长按事件返回true,onClick不会响应
  10. onLongClick再DOWN事件中判断,要想执行长按事件该view必须是longClickable的并且设置了onLongClickListener.

引用:

https://blog.youkuaiyun.com/weixin_41101173/article/details/79685305?utm_source=blogxgwz1
https://www.gcssloop.com/customview/dispatch-touchevent-theory
https://zhuanlan.zhihu.com/p/27608989
https://blog.youkuaiyun.com/guolin_blog/article/details/9097463
https://blog.youkuaiyun.com/guolin_blog/article/details/9153747

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值