Android事件分发机制
一、事件分发
1、事件分发的对象
-
事件分发的对象是点击事件(Touch事件),而当用户触摸屏幕时,将产生点击事件。
-
事件类型分为四种
类型 说明 MotionEvent.ACTION_DOWN 手指刚接触屏幕,一般为事件的开始 MotionEvent.ACTION_MOVE 手指在屏幕移动,在移动的过程中会产生多个move事件 MotionEvent.ACTION_UP 手指从屏幕上松开的一瞬间 MotionEvent.ACTION_CANCEL 结束事件,非人为原因 -
同一个事件序列:指从手指刚接触屏幕,到手指离开屏幕的那一刻结束,在这一过程产生的一系列事件,这个序列一般以down事件开始,中间含有多个move事件,最终以up事件结束
2、事件分发的本质
事件分发的本质,其实就是将点击事件(MotionEvent)传递到某个具体的View处理的整个过程
3、事件分发的顺序
事件传递的顺序:Activity->Window->DecorView->ViewGroup->View。一个点击事件发生后,总是先传递给当前的Activity,然后通过Window传给DecorView再传给ViewGroup,最终传到View。
二、核心方法
View的事件分发机制主要由事件分发->事件拦截->事件处理三步来进行逻辑控制,很巧的这三步刚好对应了三个核心方法
1、事件分发:dispatchTouchEvent
dispatchTouchEvent用来进行事件的分发,如果事件能够传递给当前View,则该方法一定会被调用。返回结果受当前View的onTouchEvent和下级的dispatchTouchEvent的影响,表示是否消耗当前事件。
原型:public boolean dispatchTouchEvent(MotionEvent ev)
return:
- ture:当前View消耗所有事件
- false:停止分发,交由上层控件的onTouchEvent方法进行消费,如果本层控件是Activity,则事件将被系统消费,处理
2、事件拦截:onInterceptTouchEvent
-
在Activity,ViewGroup,View中只有ViewGroup有onInterceptTouchEvent这个方法。故一旦有点击事件传递给View,则View的onTouchEvent方法就会被调用
-
在dispatchTouchEvent内部使用,用来判断是否拦截事件。如果当前View拦截了某个事件,那么该事件序列的其它方法也由当前View处理,故该方法不会被再次调用,因为已经无须询问它是否要拦截该事件。
原型:public boolean onInterceptTouchEvent(MotionEvent ev)
return:
- ture:对事件拦截,交给本层的onTouchEvent进行处理
- false:不拦截,分发到子View,由子View的dispatchTouchEvent进行处理
- super.onInterceptTouchEvent(ev):默认不拦截
3、 事件处理:onTouchEvent
在dispatchTouchEvent中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一事件序列中,当前View无法再接受到剩下的事件,并且事件将重新交给它的父元素处理,即父元素的onTouchEvent会被调用
原型:public boolean onTouchEvent(MotionEvent ev)
return:
- true:表示onTouchEvent处理后消耗了当前事件
- false:不响应事件,不断的传递给上层的onTouchEvent方法处理,直到某个View的onTouchEvent返回true,则认为该事件被消费,如果到最顶层View还是返回false,则该事件不消费,将交由Activity的onTouchEvent处理。
- super.onTouchEvent(ev):默认消耗当前事件,与返回true一致。
三、事件分发机制
从上文我们知道事件分发顺序为:Activity->Window->DecorView->ViewGroup->View。由于Window与DecorView可以看作是Activity->ViewGroup的过程,故这里将从三部分通过源码来分析事件分发机制:
- Activity对点击事件的分发机制
- ViewGroup对点击事件的分发机制
- View对点击事件的分发机制
1、 Activity事件的分发机制
-
当一个点击事件发生时,事件总是最先传递到当前Activity中,由Activity的dispatchTouchEvent来进行事件分发。而Activity会将事件传递给Window对象来分发,Window对象再传递给DecorView。先看看Activity中的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent ev) { //点击事件的开始一般为按下事件,所以总是true if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } //如果Activity所属Window的dispatchTouchEvent返回了ture,则Activity.dispatchTouchEvent返回ture,点击事件停止往下传递 if (getWindow().superDispatchTouchEvent(ev)) { return true; } //如果Window的dispatchTouchEvent返回了false,则点击事件传递给Activity.onTouchEvent return onTouchEvent(ev); }
-
点击事件发生时,首先会执行onUserInteraction(),该方法为空方法,当该Activity在栈顶时,触屏点击home,back,menu会触发此方法,所以这个方法可以实现屏保功能
public void onUserInteraction() { }
-
接着调用了getWindow().superDispatchTouchEvent(ev)方法将事件交给Activity所附属的Window进行分发,如果最终事件被消耗了,则返回true,如果事件没人处理,则Activity调用在自己的onTouchEvent()方法来处理事件。getWindow是一个Window对象,在Window源码中我们可以发现其实Window就是一个抽象类,显而易见其方法自然是抽象方法,它的唯一实现类就是PhoneWindow
//这个抽象类的唯一现有实现是isandroid.view.PhoneWindow,当需要Window时应该实例化它。 public abstract class Window { ... //抽象方法 public abstract boolean superDispatchTouchEvent(MotionEvent event); ... }
-
PhoneWindow中的superDispatchTouchEvent方法,将事件直接传递给了DecorView
private DecorView mDecor; @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
-
DecorView就是我们通过setContentView设置布局的父容器,我们可以通过getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0)这个方式就能获取到setContentView中设置的布局。从DecorView的源码中可以发现DecorView是继承FrameLayout,而FrameLayout又是继承ViewGroup的,故DecorView的间接父类为ViewGroup,在DecorView中superDispatchTouchEvent方法是使用super来调用父类的dispatchTouchEvent,故等于调用ViewGroup的dispatchTouchEvent方法(从源码中我们可以得知FrameLayout并没有dispatchTouchEvent这个方法),于是DecorView将事件传递到了ViewGroup去处理。也可以这么说,事件已经传递到了顶级View也就是Activity中通过setContentView所设置的View(顶级View通常为ViewGroup)
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks { ...... public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); } ...... } @RemoteView public class FrameLayout extends ViewGroup { ...... }
-
流程图
2、ViewGroup事件的分发机制
-
ViewGroup事件分发机制是从dispatchTouchEvent()开始。ViewGroup判断是否要拦截只会是在ACTION_DOWN的时候,或者是mFirstTouchTarget != null。mFirstTouchTarget 从后面的代码才能知道其作用。它的作用就是:当事件被ViewGroup的某个子View处理时,mFirstTouchTarget 就会指向这个子View。所以当事件被这个ViewGroup拦截时,子类就不会处理这个事件,因此mFirstTouchTarget =null,那么这个时候ACTION_MOVE和ACTION_UP事件到来时,由于判断条件为false,将导致ViewGroup的onInterceptTouchEvent不会再被调用,然后intercepted被赋予true,所以同一事件序列的其它事件都会默认交给该ViewGroup来处理。FLAG_DISALLOW_INTERCEPT是个标记位,这个标记位是通过 requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法来设置的,一般用在子View中。如果FLAG_DISALLOW_INTERCEPT被设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其它点击事件,这是因为ViewGroup在分发事件中,如果是ACTION_DOWN事件,将会重置FLAG_DISALLOW_INTERCEPT这个标记位。当点击事件为ACTION_DOWN时,ViewGroup总是会调用自己的onInterceptTouchEvent来询问自己是否要拦截事件。requestDisallowInterceptTouchEvent方法针对的是ACTION_DOWN以外的其他事件,并且是在不拦截ACTION_DOWN事件的情况下才会起作用。
@Override public boolean dispatchTouchEvent(MotionEvent ev) { ........ // Check for interception. final boolean intercepted; //是否拦截 //当事件由ViewGroup的子元素处理时,mFirstTouchTarget会被赋值并指向子元素 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不拦截事件时,首先会遍历ViewGroup的所有子元素,然后判断子元素是否能够接受到点击事件。判断的依据是:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。如果找到一个目标子View来处理事件时,则调用dispatchTransformedTouchEvent方法。
final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { //遍历ViewGroup的所有子元素 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 (!canViewReceivePointerEvents(child) || !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); //dispatchTransformedTouchEvent实际调用的是子元素的dispatchTouchEvent方法 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(); //完成了mFirstTouchTarget的赋值并终止了对子元素的遍历 newTouchTarget = addTouchTarget(child, idBitsToAssign); //记录ACTION_DOWN事件已经被处理了 alreadyDispatchedToNewTouchTarget = true; break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); }
-
dispatchTransformedTouchEvent方法重要实现逻辑,child不等于null,所以将直接调用子元素的dispatchTouchEvent方法,使得事件传递到子View上,然后继续分发
if (child == null) { handled = super.dispatchTouchEvent(event); } else { ........ handled = child.dispatchTouchEvent(event); }
-
addTouchTarget方法完成了对mFirstTouchTarget的赋值,可以看出其实mFirstTouchTarget是一种单链表结构,首先根据坐标点找到了目标子View,然后将子View放在链表头上,从而实现了mFirstTouchTarget!=null
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; }
-
到这里就完成了ViewGroup一轮的事件分发了,然而还没有结束,如果遍历了所有子元素后事件都没有被合适处理,包括了两种情况:
- ViewGroup没有子元素
- 子元素处理了点击事件,但是在dispatchTouchEvent中返回了false(默认是返回true,只有重写View的这个方法或者在onTouchEvent中返回了false)
那么这时候ViewGroup将会自己处理点击事件(当ViewGroup拦截了事件时也是做同样的处理)
if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); }
-
调用了dispatchTransformedTouchEvent方法,不过这时候第三个参数不是child而是null,所以会调用View中的dispatchTouchEvent方法。ViewGroup并没有调用onTouchEvent,ViewGroup也没有去重写onTouchEvent
//dispatchTransformedTouchEvent handled = super.dispatchTouchEvent(event);
-
流程图
3、View事件的分发机制
-
从上面对ViewGroup事件分发机制可知,View事件分发机制是从dispatchTouchEvent开始的。由于View是一个单独元素,没有子元素可以继续向下传递事件,只能自己处理事件。从源码中可以看到View对点击事件的处理过程,result代表是否消耗该事件,然后进行onTouchListener的判断,如果onTouchListenter中的onTouch方法返回了true,那么就不会再调用onTouchEvent方法,由此可见onTouchListener的优先级高于onTouchEvent。
public boolean dispatchTouchEvent(MotionEvent event) { ...... boolean result = false; ...... //判断窗口是否被遮挡,如果被遮挡则返回false,比如有时候两个View是会重叠的,导致其中一个被遮挡了。 if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; //判断是否设置了mOnTouchListener,如果设置了onTouchListener,且onTouch方法返回了ture, //则result = true if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } //在result = ture情况下,就不会调用onTouchEvent,可见onTouchListener的优先级高于onTouchEvent if (!result && onTouchEvent(event)) { result = true; } } ...... return result; }
-
从onTouchEvent方法可以知道只要View的CLICKABLE,LONG_CLICKABLE,CONTEXT_CLICKABLE有一个为true,那么它就会消耗该事件,不管它是不是DISABLE状态。然后假如控件可点击,就对四种事件类型进行相对应的处理,这里值得一说的是ACTION_UP事件,从源码中可以发现在ACTION_UP事件发生时,会触发performClickInternal方法
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; //不可用状态下点击事件的处理,依然会消耗点击事件 if ((viewFlags & ENABLED_MASK) == DISABLED) { 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设置了代理,将会执行代理的onTouchEvent方法 if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: ..... boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; ...... //经过种种判断 performClickInternal(); break; case MotionEvent.ACTION_DOWN: .... break; case MotionEvent.ACTION_CANCEL: .... break; case MotionEvent.ACTION_MOVE: .... break; } //若该控件可点击,就一定返回true return true; } //若该控件不可点击,就一定返回false return false; }
-
performClickInternal方法,最后还是会调用performClick,而在performClick内部中,只要我们通过setOnClickListener为View注册点击事件,那么就会给li.mOnClickListener赋值,则会调用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() { // 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; }
-
流程图
-
onTouch,onTouchEvent,onClick的优先级:onTouch>onTouchEvent>onClick
四、总结
事件分发的大致过程如下:
- 当一个点击事件发生后,总是先传递给当前的Activity,由Activity的dispatchTouchEvent进行分发,而Activity会将事件传递给Window,然后由Window的唯一实现类PhoneWindow将事件传递给DecorView,接着DecorView将事件传递给自己的父类ViewGroup,此时的ViewGroup就是通过setContentView所设置的View,故可以称为顶级View,这时候ViewGroup可能是自己处理该事件或者传递给子View,但是最终都会调用View的dispatchTouchEvent来处理事件。
- 在View的dispatchTouchEvent中,如果设置了onTouchListener,会调用其onTouch方法,如果onTouch返回true,则不再调用onTouchEvent。如果有设置点击事件,则在onTouchEvent会调用onClick方法。如果子View的onTouchEvent返回了false,则表示不消耗事件,事件会回传给上一级的ViewGroup的onTouchEvent,如果所有的ViewGroup都没有返回true,则最终会回传到Activity的onTouchEvent。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Blazor Hybrid适配到HarmonyOS系统
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 分享4款.NET开源、免费、实用的商城系统
· 解决跨域问题的这6种方案,真香!
· 一套基于 Material Design 规范实现的 Blazor 和 Razor 通用组件库