当前位置: 首页 > article >正文

Android View 的事件分发机制解析

前言当一个事件发生时(例如触摸屏幕),事件会从根View(通常是Activity的布局中的最顶层View)开始,通过一个特定的路径传递到具体的View,这个过程涉及到三个关键的阶段:事件分发、事件拦截和事件消费。

1. 触摸事件类型

ACTION_DOWN: 用户按下触摸屏幕的事件。
ACTION_UP: 用户抬起手指的事件。
ACTION_MOVE: 用户在屏幕上移动手指的事件。
ACTION_CANCEL: 事件被取消,例如由于系统的其他操作。

ACTION_POINTER_DOWN: 当已经有一个手指按下的情况下,另一个手指按下会产生该事件;ACTION_POINTER_UP: 多个手指同时按下的情况下,抬起其中一个手指会产生该事件;


2. 事件传递流程示例

事件产生:用户触摸屏幕。

事件分发:从根View(例如Activity的根布局)开始调用dispatchTouchEvent。

事件拦截:如果根布局的dispatchTouchEvent返回true,则调用其onInterceptTouchEvent判断是否拦截。如果拦截,则调用onTouchEvent处理;如果不拦截,则继续向下传递。

子View处理:如果根布局不拦截,则根据情况继续传递给子View的dispatchTouchEvent进行处理。

最终处理:最终某个View的onTouchEvent返回true,表示该事件被消费,不再继续传递。如果所有View都未消费该事件,则可能由系统处理(如背景点击)。

 3. 事件分发机制

事件分发主要通过三个方法实现:

-- dispatchTouchEvent(MotionEvent ev)

-- onInterceptTouchEvent(MotionEvent ev)

-- onTouchEvent(MotionEvent ev)

1.1 dispatchTouchEvent(MotionEvent ev)

当一个事件发生时,首先会调用dispatchTouchEvent方法。如果当前View是Activity的顶层布局,那么这个方法首先会被调用。该方法返回一个布尔值,如果返回true,表示当前View消费了该事件,事件流终止;如果返回false,则事件将继续传递给其他可能的父View或子View。

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean result = false;
    if (onInterceptTouchEvent(ev)) {
        result = onTouchEvent(ev);
    } else {
        // 尝试传递事件给子View
        result = event.dispatchToChild(ev);
    }
    return result;
}

1.2 onInterceptTouchEvent(MotionEvent ev)

当dispatchTouchEvent返回false时,会调用onInterceptTouchEvent方法。这个方法允许父View拦截子View的事件。如果这个方法返回true,那么当前View将消费这个事件,不再继续向下传递;如果返回false,则事件将继续传递给子View。默认情况下,大多数View的onInterceptTouchEvent返回false。

public boolean onInterceptTouchEvent(MotionEvent ev) {
    // 默认不拦截所有点击事件
    return false;
}


1.3 onTouchEvent(MotionEvent ev)

当事件没有被拦截或者已经被当前View拦截时,会调用onTouchEvent方法。如果这个方法返回true,表示当前View消费了该事件;如果返回false,则事件将继续传递给其他可能的父View或子View。

public boolean onTouchEvent(MotionEvent event) {
    // 处理触摸事件
    return true; // 表示消费了事件
}

4、ViewGroup 事件分发源码

public boolean dispatchTouchEvent(MotionEvent ev) {
            ......

            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
       // 当有 down 操作,会把之前的target 以及标志位都复位
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);

                //清除 FLAG_DISALLOW_INTERCEPT,并且设置 mFirstTouchTarget 为 null
                resetTouchState(){
                    if(mFirstTouchTarget!=null){mFirstTouchTarget==null;}
                    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
                    ......
                };
            }
            final boolean intercepted;//ViewGroup是否拦截事件

            // mFirstTouchTarget是ViewGroup中处理事件(return true)的子View
            //如果没有子View处理则mFirstTouchTarget=null,ViewGroup自己处理
            if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);//onInterceptTouchEvent
                    ev.setAction(action);
                } else {
                    intercepted = false;

                    //如果子类设置requestDisallowInterceptTouchEvent(true)
                    //ViewGroup将无法拦截MotionEvent.ACTION_DOWN以外的事件
                }
            } else {
                intercepted = true;

                //actionMasked != MotionEvent.ACTION_DOWN并且没有子View处理事件,则将事件拦截
                //并且不会再调用onInterceptTouchEvent询问是否拦截
            }

            ......
            ......
}

先看标红的代码,这句话的意思是:当 ACTION_DOWN 事件到来时,或者有子元素处理事件( mFirstTouchTarget != null ),如果子 view 没有调用 requestDisallowInterceptTouchEvent 来阻止 ViewGroup 的拦截,那么 ViewGroup 的 onInterceptTouchEvent 就会被调用,来判断是否是要拦截。所以,当子 View 不让父 View 拦截事件的时候,即使父 View onInterceptTouchEvent 中返回true 也没用了。

这里需要注意的就是:onInterceptTouchEvent 默认返回 false。 当 ACTION_DOWN 事件到来时,此时 mFirstTouchTarget 为 null,此时其实也还未收到子 view requestDisallowInterceptTouchEvent。所以这时候,只要父 view 把 ACTION_DOWN 事件给拦截了,那么子 view 就收不到任何事件消息了。所以,一般在 ACTION_DOWN 的时候,父 view 不作拦截。

当 ACTION_MOVE 事件来临时,满足某些条件,父 view 想拦截的时候,这时候子 view 可以在 dispatchTouchEvent 中 ACTION_DOWN 事件来临的时候,调用 requestDisallowInterceptTouchEvent 就可以避免被父 view 拦截。

FLAG_DISALLOW_INTERCEPT 标记位就是通过子 View requestDisallowInterceptTouchEvent 方法设置的。 具体可参看如下代码。

 @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

5、View的事件分发源码

public boolean dispatchTouchEvent(MotionEvent event) {  

        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
                mOnTouchListener.onTouch(this, event)) {  
            return true;  
        } 
        return onTouchEvent(event);  
  }

上述方法只有以下3个条件都为真,dispatchTouchEvent() 才返回 true;否则执行 onTouchEvent()。

  •  mOnTouchListener != null

  •  (mViewFlags & ENABLED_MASK) == ENABLED

  •  mOnTouchListener.onTouch(this, event)

这也就说明如果调用了 setOnTouchListener 设置了 listener, 就会先调用 onTouch 方法。没有的话才会去调用 onTouchEvent 方法。接下去,我们看 onTouchEvent 源码。

public boolean onTouchEvent(MotionEvent event) {  
    final int viewFlags = mViewFlags;  

    if ((viewFlags & ENABLED_MASK) == DISABLED) {  
         
        return (((viewFlags & CLICKABLE) == CLICKABLE ||  
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));  
    }  
  // 如果进行了事件代理,就会被拦截,不会在往下面走了
    if (mTouchDelegate != null) {  
        if (mTouchDelegate.onTouchEvent(event)) {  
            return true;  
        }  
    }  

    // 若该控件可点击,则进入switch判断中
    if (((viewFlags & CLICKABLE) == CLICKABLE ||  
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  

                switch (event.getAction()) { 

                    // a. 若当前的事件 = 抬起View(主要分析)
                    case MotionEvent.ACTION_UP:  
                        boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  

                            ...// 经过种种判断,此处省略

                            // 执行performClick() ->>分析1
                            performClick();  
                            break;  

                    // b. 若当前的事件 = 按下View
                    case MotionEvent.ACTION_DOWN:  
                        if (mPendingCheckForTap == null) {  
                            mPendingCheckForTap = new CheckForTap();  
                        }  
                        mPrivateFlags |= PREPRESSED;  
                        mHasPerformedLongPress = false;  
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                        break;  

                    // c. 若当前的事件 = 结束事件(非人为原因)
                    case MotionEvent.ACTION_CANCEL:  
                        mPrivateFlags &= ~PRESSED;  
                        refreshDrawableState();  
                        removeTapCallback();  
                        break;

                    // d. 若当前的事件 = 滑动View
                    case MotionEvent.ACTION_MOVE:  
                        final int x = (int) event.getX();  
                        final int y = (int) event.getY();  
        
                        int slop = mTouchSlop;  
                        if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                                (y < 0 - slop) || (y >= getHeight() + slop)) {  
                            // Outside button  
                            removeTapCallback();  
                            if ((mPrivateFlags & PRESSED) != 0) {  
                                // Remove any future long press/tap checks  
                                removeLongPressCallback();  
                                // Need to switch from pressed to not pressed  
                                mPrivateFlags &= ~PRESSED;  
                                refreshDrawableState();  
                            }  
                        }  
                        break;  
                }  
                // 若该控件可点击,就一定返回true
                return true;  
            }  
             // 若该控件不可点击,就一定返回false
            return false;  
        }

/**
  * 分析1:performClick()
  */  
    public boolean performClick() {  

        if (mOnClickListener != null) {  
            playSoundEffect(SoundEffectConstants.CLICK);  
            mOnClickListener.onClick(this);  
            return true;  
            // 只要我们通过setOnClickListener()为控件View注册1个点击事件
            // 那么就会给mOnClickListener变量赋值(即不为空)
            // 则会往下回调onClick() & performClick()返回true
        }  
        return false;  
    }

从上面的代码我们可以知道,当手指抬起的时候,也就是处于 MotionEvent.ACTION_UP 时,才会去调用 performClick()。而 performClick 中会调用 onClick  方法。

也就说明了:三者优先级是 onTouch->onTouchEvent->onClick

如果 setOnTouchListener 和 setOnclickListener 一起使用:

  • onTouch 方法 return false: 会执行 onClick 方法; down,move,up 这三个方法都会执行

  • onTouch 方法 return true: 不会执行 onClick 方法; down,move,up 这三个方法都会执行

不建议一起使用,可以将点击事件放在up中.

至此 View 的事件分发机制讲解完毕。

6、滑动冲突解决方法

滑动冲突产生的原因:只要在界面中存在内外两层可以同时滑动,就会产生滑动冲突。如下所示:图1是左右滑动和上下滑动冲突,图二是两个view之间的上下滑动冲突;

 解决方案:根据实际情况,判断到底需要谁去响应滑动事件。

主要解决方式有两种,一种是外部拦截法,一种是内部拦截法。

6.1 外部拦截法:

看标题就应该可以知道,外部拦截法,就是通过父 view 来解决滑动冲突。 因为父 view 肯定属于 ViewGroup,所以父 view 根据自己需要来判断是否需要拦截事件。对于 ViewGroup,有个 onInterceptTouchEvent 方法,再需要拦截的时候,返回 true 即可。

public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted=false;
        int x= (int) event.getX();
        int y= (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                intercepted=false;//必须不能拦截,否则后续的ACTION_MOME和ACTION_UP事件都会拦截。
                break;
            case MotionEvent.ACTION_MOVE:
                if (父容器需要当前点击事件){
                    intercepted=true;
                }else {
                    intercepted=false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted=false;
                break;
            default:
                break;
        }
        mLastXIntercept=x;
        mLastXIntercept=y;
        return intercepted;
    }

6.2 内部拦截法:

既然外部拦截法是子 view 主动处理拦截,那么内部拦截法就是需要子 view 来处理滑动冲突的情况。那么子view应该如何处理呢?首先子 view 在 dispatchTouchEvent 方法内部调用 requestDisallowInterceptTouchEvent 不让父 view 拦截事件,然后再 onTouchEvent 方法中处理需要拦截的情况。不拦截的时候返回 false,将事件交还给父 view 处理。

// 子 view    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                //如果是左右滑动
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
            }
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return super.dispatchTouchEvent(ev);
    }

同时为了避免父 view 消费事件,还需要在 DOWN 事件来临的时候,父 view 不会拦截,否则事件就不会传到子 view 了。

public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            return false;
        } else {
            return true;
        }
    }

如果子 view 不处理,  父 view 会再次获得事件的处理权限。

参考:

https://blog.csdn.net/qian520ao/article/details/77429593
https://www.jianshu.com/p/38015afcdb58
https://blog.csdn.net/u010302764/article/details/72636459
https://www.cnblogs.com/huansky/p/9656394.html


http://www.kler.cn/a/523727.html

相关文章:

  • 【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】1.30 性能巅峰:NumPy代码优化全攻略
  • ODP(OBProxy)路由初探
  • 万物皆有联系:驼鸟和布什
  • (done) MIT6.S081 2023 学习笔记 (Day6: LAB5 COW Fork)
  • HarmonyOS DevEco Studio模拟器点击运行没有反应的解决方法
  • 【redis】redis操作set类型的key发生了什么?
  • JS中的Date()操作与易错点
  • 磁珠的选型以及变压器气隙问题
  • Ubuntu 20.04 Realtek 8852无线网卡驱动
  • 接口技术-第4次作业
  • C/C++中的#define和const的特点与区别
  • Baklib如何重塑内容中台的智能化推荐系统实现个性化服务
  • 《Java核心技术 卷II》日期和时间API的时间线
  • 马尔科夫模型和隐马尔科夫模型区别
  • PostGIS笔记:PostgreSQL中表、键和索引的基础操作
  • 蛇年 | 巳巳如意,生生不息
  • 2025金蛇迎春,和数集团祝您新年快乐,蛇年大吉
  • 【机器学习】自定义数据集 使用pytorch框架实现逻辑回归并保存模型,然后保存模型后再加载模型进行预测
  • Java中ExecutorService接口介绍、应用场景和示例代码
  • 如何运用python爬虫爬取百度贴吧的静态图片?
  • 懂球短视频微信小程序SpringBoot+论文源码调试讲解
  • 06-AD向导自动创建P封装(以STM32-LQFP48格式为例)
  • Linux 基础1
  • 【C++题解】1393. 与7无关的数?
  • centos7搭建flink1.18并以 standalone模式启动
  • 前端UI组件库的打包与发布