IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 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方法。

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-03-03 16:26:11  更:2022-03-03 16:31:09 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 17:24:41-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码