ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

View的事件分发机制

2022-02-27 18:01:18  阅读:242  来源: 互联网

标签:分发 ViewGroup dispatchTouchEvent onTouchEvent 事件 机制 true View


View的事件分发机制

VIew的事件包括什么

View的事件其实指的就是MotionEvent,也就是我们对屏幕的点击,滑动,抬起等一系的动作,它有以下四种事件类型

  1. ACTION_DOWN:手指刚接触屏幕
  2. ACTION_MOVE:手指在屏幕上移动
  3. ACTION_UP:手指从屏幕上松开的瞬间
  4. ACTION_CANCEL:事件被上层拦截时触发

正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,主要有以下两种情况:

  • 点击屏幕后松开,事件序列为:ACTION_DOWN -> ACTION_UP
  • 点击屏幕滑动一会再松开,事件序列为:ACTION_DOWN -> ACTION_MOVE -> … -> ACTION_MOVE -> ACTION_UP
    我们可以编写如下代码用于观察
public class MotionEventActivity extends AppCompatActivity {

    private static final String TAG = "MotionEvent";

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

        Button btn_motion_event = findViewById(R.id.btn_motion_event);
        btn_motion_event.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                switch (motionEvent.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        Log.d(TAG, "MotionEvent: ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.d(TAG, "MotionEvent: ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.d(TAG, "MotionEvent: ACTION_UP");
                        break;
                }
                return false;
            }
        });
        
        btn_motion_event.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.d(TAG, "onClick: button被点击");
            }
        });
    }
}

那么,ACTION_UP一定是一个事件的结束吗?其实不一定。经过上面代码的测试,如果我们意外结束了点击事件,比如按下菜单键主页键或者锁屏,都不会有ACTION_UP。
在这里插入图片描述

事件分发规则

三个事件分发方法

当一个MotionEvent产生了以后,系统需要把这一个事件传递给一个具体的View,这个传递的过程就是分发的过程。点击事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent()、 onInterceptTouchEvent()和onTouchEvent(),下面我们一一介绍。

  • public boolean dispatchTouchEvent(MotionEvent event)
    用来进行事件的分发,如果事件能够传递给当前View,那么dispatchTouchEvent方法一定会被调用。
    返回值:表示是否消费了当前事件。可能是View本身的onTouchEvent方法消费,也可能是子View的dispatchTouchEvent方法中消费。返回true表示事件被消费,本次的事件终止。返回false表示View以及子View均没有消费事件,将调用父View的onTouchEvent方法
  • public boolean onInterceptTouchEvent(MotionEvent ev)
    在dispatchTouchEvent()方法内部调用,用来判断是否拦截某个事件,当一个ViewGroup在接到MotionEvent事件序列时候,首先会调用此方法判断是否需要拦截。特别注意,这是ViewGroup特有的方法,View并没有拦截方法
    返回值:是否拦截当前事件,返回true表示拦截了事件,那么事件将不再向下分发而是调用View本身的onTouchEvent方法。返回false表示不做拦截,事件将向下分发到子View的dispatchTouchEvent方法。
  • public boolean onTouchEvent(MotionEvent ev)
    在dispatchTouchEvent进行调用,用来处理点击事件
    返回值:返回true表示事件被消费,本次的事件终止。返回false表示事件没有被消费,将调用父View的onTouchEvent方法。同时在同一个事件序列中,当前View无法再次接收到事件。
    在这里插入图片描述

上面的三个方法可以用以下的伪代码来表示其之间的关系:

public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;//事件是否被消费
        if (onInterceptTouchEvent(ev)){//调用onInterceptTouchEvent判断是否拦截事件
            consume = onTouchEvent(ev);//如果拦截则调用自身的onTouchEvent方法
        }else{
            consume = child.dispatchTouchEvent(ev);//不拦截调用子View的dispatchTouchEvent方法
        }
        return consume;//返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法
    }

这里或许会有两个疑问:

  1. 为什么View会有dispatchTouchEvent方法?
  2. 为什么View没有onInterceptTouchEvent方法?

关于第一个问题, 一个View 可以注册很多监听器吧,例如单击,长按,触摸事件(onTouch),并且View 本身也有 onTouchEvent 方法,那么问题来了,这么多事件相关的方法应该由谁管理,所以View也会有dispatchTouchEvent这个方法。
至于第二个,我们先来看一张图理清View的结构
在这里插入图片描述
我们可以看到,ViewGroup下是可以包含ViewGroup和View的,而View不可以,也就是说,如果一个事件分发给了一个具体的View,它只需要选择处理还是不处理,不用进行拦截,因为它肯定不需要向下分发。

传递规则

对于一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,这时他的 dispatchTouEvent 就会调用,如果这个 ViewGroup 的 onInterceptTouchEvent方法返回true,就表示它要拦截当前事件,接着事件就会交给这个 ViewGroup 处理,即它的 onTouchEvent 方法会被调用,如果这个 ViewGroup 的onInterceptTouchEvent 方法返回false 就表示它不拦截当前事件,这时当前事件就会继续传递给它的 子元素,接着子元素 dispatchTouEvent 方法就会被调用。如此反复直到事件最终被处理。
用一张事件分发流程图来说明一下:
在这里插入图片描述
当一个View需要处理事件时,如果它设置了 OnTouchListener, 那么 OnTouchListener 中的 onTouch 方法会被回调。这时事件如何处理还要看 onTouch 的返回值,如果返回false,则当前View 的onTouchEvent 方法会被调用;如果返回 true,那么 onTouchEvent 方法将不会被调用。由此可见,给View设置 onTouchListener,其优先级比 onTouchEvent 还要高。在 onTouchEvent 方法中,如果当前设置有 onClickListener,那么它的 onClick 方法会被调用。可以看出,平时我们常用的 onClickListener,其优先度最低,即处于事件传递的尾端.

当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View ,事件总是先传递给Activity,Activity在传递给Window,最后Window在传递给顶级View,顶级View接受到事件后,就会按照事件分发机制去分发事件,这里我们在考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,如果所有元素都不处理这个事件,那么这个事件最终会交给Activity去处理,就是Activity的OnTouchEvent方法会被调用。

关于事件传递机制,我们可以总结出以下结论,根据这些结论能更好的理解整个传递机制:

  1. 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的的一系列时间,这个事件序列以 down 事件开始,中间含有数量不定的 move 事件,最终以 up 事件结束。
  2. 正常情况下,一个事件序列只能被一个 View 拦截且消耗。这一条原因可以参考(3),因为一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个 View同时处理,但是通过特殊手段可以做到,比如一个 View 将本该自己处理的事件 通过 onTouchEvent 强行传递给其他View处理。
  3. 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的 onInterceptTouchEvent 不会再被调用。这条也很好理解,就是说当一个 View 决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不再调用这个View的 onInterceptTouchEvent 去询问它是否要拦截了。
  4. 某个View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件( onTOuchEvent 返回了 false ) ,那么同一事件序列中的其他事件都不会交给 它来处理,并且事件将重新交由它的父元素去处理,即父元素的 onTouchEvent 会被调用。意思就是事件一旦交给一个 View 处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了,这就好比上级交给程序猿一件事,如果事情没有处理好,短期内上级就不敢再把事情交给程序猿去做了。二者道理差不多。
  5. 如果View 不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。
  6. ViewGroup 默认不拦截任何事件。Android 源码中ViewGroup 的 onInterceptTouchEvent 方法默认返回false.
  7. View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。
  8. View 的 onTouchEvent 默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false).View 的 longClickable 属性默认为 false,clickable 属性要分情况,比如Button 的clickable 属性默认为true,而 TextView 的 clickable 属性默认为 false.
  9. View 的 enable 属性不受影响 onTouchEvent 的 默认返回值,哪怕一个 View 是 disable 状态的,只要它的 clickable 或者 longClickable 有一个 为 true,那么它的 onTouchEvent 就返回 true.
  10. onClick 会发生的前提是当前View 是可点击的,并且它收到了 down 和 up 的事件。
  11. 事件传递过程是由外而向的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。

事件分发源码

Activity和Window的分发

当一个点击操作发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行事件分发,具体的工作是由Activity内部的Window来完成的

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
            //默认是一个空函数,不用关心
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

现在分析上面的代码。首先事件开始交给 Activity所附属的 Window进行分发,如果返回true,整个事件循环就结束了,返回 false意味着事件没人处理,所有View的on Touchevent都返回了 false,那么 Activity的 on Touchevent就会被调用。

Window是一个抽象类,而Window的superDispatchTouchEvent方法是抽象方法,它的唯一实现类是PhoneWindow

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

PhoneWindow将事件直接传递给DecorView。
我们可以通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)即通过Activity来得到内部的View。这个mDecor显然就是getWindow().getDecorView()返回的View,而我们通过setContentView设置的View是它的一个子View。总之,现在事件传递给了顶级ViewGroup。

ViewGroup的分发

事件到达顶级ViewGroup后,会调用ViewGroup的dispatchTouchEvent方法,然后的逻辑是这样的:如果底层ViewGroup拦截事件即onInterceptTouchEvent返回true,则事件由ViewGroup处理。如果顶层ViewGroup不拦截事件,则事件会传递给它的在点击事件链上的子View,这个时候,子View的dispatchTouchEvent会被调用。如此循环,完成整个事件派发。
另外要说明的是,ViewGroup默认是不拦截点击事件的,其onInterceptTouchEvent返回false。

// Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

从上面的代码可以看出,ViewGroup在如下两种情况下会判断是否拦截当前事件:事件类型为down或者mFirstTouchTarget != null。mFirstTouchTarget可以这样理解,当ViewGroup不拦截事件并将事件交由子元素处理时,mFirstTouchTarget会被赋值也就是mFirstTouchTarget不为空。一旦事件由当前ViewGroup拦截,mFirstTouchTarget始终为空,当move和up事件到来,此ViewGroup就不会判断是否需要拦截,onInterceptTouchEvent()方法也不会调用,同一序列的其他事件都由它处理。
这里还有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标志位,这个标志位是通过的requestDisallowInterceptTouchEvent这个方法来设置,一般在子View中。即使事件已经分发下去,子元素仍然可以调用父元素的requestDisallowInterceptTouchEvent方法来置位FLAG_DISALLOW_INTERCEPT标志位,从而从父元素判断是否拦截事件。但是down事件除外。因为ViewGroup在分发事件时,如果是down事件就会重置FLAG_DISALLOW_INTERCEPT这个标志位,将导致子View中设置的这个标志位无效。

接下来再看当ViewGroup不拦截事件时,事件向下分发的处理

final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

首先遍历ViewGroup的所有子元素,然后判断子元素是否能够收到点击事件。是否能够收到点击事件主要由两点来衡量:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它处理,dispatchTransformedTouchEvent这个方法实际上就是调用子元素的dispatchTouchEvent方法,在它的内部有如下一段内容,如下所示,由于上面传递child不是null,因此它会直接调用子元素的 dispatchTouchEvent方法,这样事件就交由子元素处理,从而完成了一轮事件分发。

if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }

如果子元素的 dispatchTouchEvent返回true,表示子元素已经处理完事件,那么mFirstTouchTarget就会被赋值同时跳出for循环,如下所示:

	newTouchTarget = addTouchTarget(child, idBitsToAssign);
	//在addTouchTarget()方法中完成mFirstTouchTarget的赋值
	alreadyDispatchedToNewTouchTarget = true;
	break;

如果遍历完所有的子元素事件没有被合适处理,有两种情况,ViewGroup没有子元素;子元素处理了点击事件,但是dispatchTouchEvent返回false,一般是因为子元素在onTouchEvent返回了false。这时ViewGroup会自己处理点击事件。

View对点击事件的处理

先看View的dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent event) {
	boolean result = false;
	……

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

View对点击事件的处理过程就比较简单了,因为View是一个单独的元素,它没有子元素因此无法向下传递事件,所以它只能自己处理事件。View首先会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent方法就不会被调用,所以OnTouchListener的优先级要高于onTouchEvent,这样做的好处是方便子啊外界处理点击点击事件。

接下来再分析onTouchEvent方法

if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    ……
                    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();
                                }
                            }
                        }
                        ……
                    }
                    break;
			}
			……
            return true;
            //返回默认值是true.这样才能执行多次touch事件。
        }

可以看出只要CLICKABLE和LONG_ CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true。当up事件发生时,会触发performClick方法,如果View设置OnClickListener,那么performClick就会调用它的onClick方法。
View的 LONG_ CLICKABLE属性默认为false,而CLICKABLE属性默认为true,不过具体的View的CLICKABLE又不一定,确切来说是可点击的View其CLICKABLE属性true,比如Botton,不可点击的View的CLICKABLE为false,比如TextView。。通过setClickable和setLongClickable可以设置这两个属性。另外setOnClickListener和setOnLongClickListener会自动将View的这两个属性设为true。

问题探索

  1. 当ViewGroup里有一个View,如果ViewGroup只在ACTION_MOVE拦截,说说各个action是如何分发的

先是down事件会经过 ViewGroup的dispatchTouchEvent,再到 ViewGroup的onInterceptTouchEvent,最后到View的dispatchTouchEvent,此时 mFirstTouchTarget不为空,紧接着到了move首先到 ViewGroup的dispatchTouchEvent,再到 ViewGroup的onInterceptTouchEvent,由于在move过程中拦截了,紧接着走view的 dispatchTouchEvent的 action_ cancel,此时接着把
mFirstTouchTarget置为nul,因此后续的move和up事件只会走ViewGroup的dispatchTouchEvent和onTouchEvent。

  1. View在onTouchEvent中消费,然后拖动手指从其他ViewGroup上挪开

事件已经在View的onTouchEvent中消费,所以mFirstTouchTarget不为空,因此不管手指移动到什么地方,其他事件都会在这个View中消费

  1. View的onTouch和onTouchEvent的区别

onTouch是setOnTouchListener中的回调方法,它优先于onTouchEvent。如果onTouch方法返回true,那么就不会触发onTouchEvent方法

  1. View的onClick方法是在什么时候触发,和onTouch有什么区别

onClick是在onTouchEvent消费事件中的ACTION_UP触发,onTouch是在dispatchTouchEvent中触发,所以onTouch要优先于onClick事件,我们可以通过onTouch返回true来屏蔽掉onClick方法。

标签:分发,ViewGroup,dispatchTouchEvent,onTouchEvent,事件,机制,true,View
来源: https://blog.csdn.net/qq_56785698/article/details/123159190

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有