[Android]View的事件分发机制(源码解析)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: [Android]View的事件分发机制(源码解析)



1.分发对象-MotionEvent


事件类型有:

1.ACTION_DOWN-----手指刚接触屏幕

2.ACTION_MOVE------手指在屏幕上移动

3.ACTION_UP------手指从屏幕上松开的一瞬间

4.ACTION_CANCEL-----事件被上层拦截时触发

MotionEvent主要的方法:

getX() 得到事件发生的x轴坐标(相对于当前视图)
getY() 得到事件发生的y轴坐标(相对于当前视图)
getRawX() 得到事件发生的x轴坐标(相对于屏幕左顶点)
getRawY() 得到事件发生的y轴坐标(相对于屏幕左顶点)

2.如何传递事件


1.传递流程

底层IMS->ViewRootImpl->activity->viewgroup->view

2.事件分发的源码解析

1.Activity对点击事件的分发过程

  • Activity#dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
//事件交给Activity所附属的Window进行分发,如果返回true,循环结束,返回false,没人处理
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
//所有View的onTouchEvent都返回false,那么Activity的onTouchEvent就会被调用
        return onTouchEvent(ev);
}
  • Window#superDispatchTouchEvent
public abstract boolean superDispatchTouchEvent(MotionEvent event);
  • PhoneWindow#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
  • DecorView#superDispatchTouchEvent()
public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
}
  • ViewGroup#dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {

2.顶级View对点击事件的分发过程

把ViewGroup的dispatchTouchEvent()方法中的代码进行分段说明

第一段:

描述的是View是否拦截点击事件这个逻辑

// Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {//事件类型为down或者mFirstTouchTarget有值
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);//询问是否拦截,方法返回true就拦截
                    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;//直接拦截了
            }

当事件类型为down或者mFirstTouchTarget有值时,就不拦截当前事件,否则直接拦截了这个事件。那么mFirstTouchTarget什么时候有值?当ViewGroup不拦截事件并且把事件交给子元素处理时,mFirstTouchTarget就有值并且指向子元素。所以当事件类型为down并且拦截事件,那么mFirstTouchTarget为空,这会让后面的事件move和up无法满足mFirstTouchTarget有值的条件,直接无法调用onInterceptTouchEvent方法。

特殊情况:通过requestDisallowInterceptTouchEvent方法来设置标记位FLAG_DISALLOW_INTERCEPT,ViewGroup就无法拦截除了ACTION_DOWN以外的点击事件,这个标记位无法影响ACTION_DOWN事件,因为当事件为ACTION_DOWN时,就会重置这个标记位,将导致子View设置的这个标记位无效。

总结:

1.当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的onInterceptTouchEvent方法。

2.当ViewGroup不拦截ACTION_DOWN事件,那么标记位FLAG_DISALLOW_INTERCEPT让ViewGroup不再拦截事件。

第二段:

当ViewGroup不拦截事件时,分发事件给子View,看哪个子View处理事件

final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x =
                                isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                        final float y =
                                isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                       //对子元素进行排序
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        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方法

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
            ...
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            ...
}

dispatchTransformedTouchEvent实际上调用的是子元素的dispatchTouchEvent方法。

如果子元素的dispatchTouchEvent方法返回true,那么mFirstTouchTarget就会被赋值同时跳出for循环

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;

这几行代码完成了mFirstTouchTarget的赋值并终止对子元素的遍历,如果子元素的dispatchTouchEvent方法返回false,那么ViewGroup就会把事件分发给下一个子元素。

其实mFirstTouchTarget真正的赋值是在addTouchTarget方法里面,mFirstTouchTarget是一种单链表结构。

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

第三段:

执行事件

//当前View的事件处理代码
if (mFirstTouchTarget == null) {
          // No touch targets so treat this as an ordinary view.
           handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {
//子View的事件处理代码
...

dispatchTransformedTouchEvent方法的第三个参数为null,则会调用super.dispatchTouchEvent方法,也就是View的dispatchTouchEvent方法,所以点击事件给View处理。

View对点击事件的处理过程

View(不包含ViewGroup)是一个单独的元素,没有子元素,只能自己处理事件。

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

首先判断是否设置了OnTouchListener,如果OnTouchListener的onTouch方法返回true,就不会调用onTouchEvent方法,否则就会调用onTouchEvent方法。

public boolean onTouchEvent(MotionEvent event) {
           ...
           if ((viewFlags & ENABLED_MASK) == DISABLED
                && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
                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;
          }
               ...
}

不可用状态下的View照样会消耗点击事件

switch (action) {
    case MotionEvent.ACTION_UP:
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
         if ((viewFlags & TOOLTIP) == TOOLTIP) {
                  handleTooltipUp();
         }
         ...
         if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
         ...
              if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                           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();
                                }
                            }
                }     
         } 
         ...
         mIgnoreNextUpEvent = false;
         break;

当ACTION_UP事件发生时,会触发performClick方法,如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法。

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();
    }
public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        //关键代码,判断是否设置了onClickListener
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        ...
        return result;//最终返回执行结果
}

点击事件的分发机制的源码实现已经分析完了。

3.主要方法:


1.dispatchTouchEvent:用来进行事件的分发,如果事件可以传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。

2.onInterceptTouchEvent:用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

3.onTouchEvent:用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

4.requestDisallowInterceptTouchEvent:一般用于子View中,要求父View不拦截事件。

5.dispatchTransformedTouchEvent:如果child不为空,就发到child的dispatchTouchEvent中,否则发给自己。

4.事件传递中listener


onTouchperformClickonClick调用的顺序以及onTouch返回值的影响?

当一个View需要处理事件时,View的dispatchTouchEvent方法中,如果设置了OnTouchListener,那么OnTouchListener的onTouch方法会被调用,当onTouch方法返回true时,onTouchEvent就不会被调用,当onTouch方法返回false时,onTouchEvent方法就被调用,在onTouchEvent方法里面进入performClick方法,在performClick方法里面判断是否设置onClickListener,并且如果设置了onClickListener,那么onClick方法就被调用,performClick方法就返回true,如果没有设置了onClickListener,performClick方法就返回false。

总的来说方法调用的顺序为

5.滑动冲突如何用事件分发处理


滑动冲突定义:当有内外两层View都可以响应事件时,事件由谁来决定。

滑动冲突类型:1.当内外两层View滑动方向不一致

                        2.当内外两层滑动方向一致的时候

                        3.两种情况叠加

解决思路:

内部拦截:dispatchTouchEvent+dispatchTransformedTouchEvent

重写子元素的dispatchTouchEvent方法

down事件分发给子元素,move事件是看条件的,如果不满足条件,就把事件交给子元素处理,如果满足条件,就会取消子元素的处理事件,然后把事件交给父元

public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                //down事件,父容器不要拦截我
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (父容器需要此类点击事件) {
                   //父容器拦截我
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

(当move事件时,进入第一块代码,调用 intercepted = onInterceptTouchEvent(ev),我们在onInterceptTouchEvent方法中设置不是down事件就返回true,所以intercepted为true,然后第二块代码不会执行,进入第三块代码,因为intercepted为true,所以cancelChild就为true,取消子元素事件执行,调用dispatchTransformedTouchEvent方法,cancel为true->

event.setAction(MotionEvent.ACTION_CANCEL)->handled = child.dispatchTouchEvent(event)

把mFirstTouchTarget设置为空,所以到下一个move事件来的时候,mFirstTouchTarget是为空的,在第一段代码中intercepted为true,第二段代码不执行,第三块代码走dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS),即由当前View的事件处理代码(父元素))

重写父元素的 onInterceptTouchEvent方法

当为down事件时,要return false,因为在ViewGroup的dispatchTouchEvent方法中,当为down事件时,会调用resetTouchState()方法,在resetTouchState()方法里面会重置状态,把mGroupFlags也重置,这样会导致在前面的parent.requestDisallowInterceptTouchEvent(true)没有用,所以我们在onInterceptTouchEvent方法里面要设置为down事件时返回false,因为在down事件时onInterceptTouchEvent一定会执行。

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

外部拦截:onInterceptTouchEvent

点击事件先经过父容器的拦截处理,如果父容器需要这件事就拦截,不需要就不拦截。

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;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if (满足父容器的拦截要求) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }
目录
相关文章
|
2天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
2天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
2天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
25天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
53 12
|
21天前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
3天前
|
安全 搜索推荐 数据挖掘
陪玩系统源码开发流程解析,成品陪玩系统源码的优点
我们自主开发的多客陪玩系统源码,整合了市面上主流陪玩APP功能,支持二次开发。该系统适用于线上游戏陪玩、语音视频聊天、心理咨询等场景,提供用户注册管理、陪玩者资料库、预约匹配、实时通讯、支付结算、安全隐私保护、客户服务及数据分析等功能,打造综合性社交平台。随着互联网技术发展,陪玩系统正成为游戏爱好者的新宠,改变游戏体验并带来新的商业模式。
|
1月前
|
XML 前端开发 Android开发
Android:UI:Drawable:View/ImageView与Drawable
通过本文的介绍,我们详细探讨了Android中Drawable、View和ImageView的使用方法及其相互关系。Drawable作为图像和图形的抽象表示,提供了丰富的子类和自定义能力,使得开发者能够灵活地实现各种UI效果。View和ImageView则通过使用Drawable实现了各种图像和图形的显示需求。希望本文能为您在Android开发中使用Drawable提供有价值的参考和指导。
40 2
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
76 2
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
79 0
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
65 0

推荐镜像

更多