View的事件分发源码分析
来源:互联网 发布:抢票软件哪个好 编辑:程序博客网 时间:2024/06/02 11:22
前言
在Android中,View主要负责界面的绘制和事件的分发、处理,它是所有控件Widgets的基类。通过源码分析View的事件分发,我们可以更加深刻地理解Android系统中View的工作原理。不仅如此,在日常的开发中,当我们遇到View事件冲突、滑动冲突时,处理起来将会游刃有余。
基础知识
当我们的手指触摸手机屏幕时,手机中的应用会对我们的触摸动作做出响应,确切地说是应用里的控件Widgets响应了触摸事件。在Android中,使用MotionEvent来描述触摸事件,我们可以通过getAction()方法来获取当前的事件类型。通常,一次手势动作会产生一系列的事件,下面列举了4个主要事件:
- ACTION_DOWN事件 当手指第一次触摸到屏幕时将产生此事件。ACTION_DOWN事件表示一系列事件的开始。
- ACTION_UP事件 当手指离开屏幕时将产生此事件。与ACTION_DOWN事件对应,ACTION_UP事件表示一系列事件的结束。
- ACTION_MOVE事件 当手指有在屏幕上滑动时将产生此事件。
- ACTION_CANCEL事件 表示当前的手势被中止了。如果一个View收到了ACTION_CANCEL事件,那么它不会再收到其它任何事件,包括ACTION_UP事件。
通过getX(), getY()方法可以获取到当前事件在屏幕上的坐标。注意,这个坐标是相对于父容器左上角的坐标。通过getRawX(), getRawY()方法可以获取到当前事件在屏幕上的原始坐标。通过前后两个ACTION_MOVE事件的坐标我们就可以知道当前手势动作的方向了。
在具体分析之前,先提一下View的事件分发的3个核心方法:
- dispatchTouchEvent()方法 主要负责事件的分发。
- onInterceptTouchEvent()方法 主要负责事件的拦截,ViewGroup专有。
- onTouchEvent()方法 主要负责事件的处理。
dispatchTouchEvent()和onTouchEvent()方法都有返回值,如果返回值为true,表示当前事件被处理了或者被消费了。另外再提一个ViewGroup的requestDisallowInterceptTouchEvent()方法,子控件通过调用这个方法可以控制是否允许父容器拦截事件,它具体影响了父容器的FLAG_DISALLOW_INTERCEPT标志位。
下面我们开始具体的源码分析。
Activity的事件分发
在Android中,底层的触摸事件最开始是传递到Activity中的,从Activity的dispatchTouchEvent()方法开始分发事件。
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev);}
从上面的代码可以知道,Activity将事件交给Window来负责分发到具体的页面布局中。如果Window的superDispatchTouchEvent()方法返回了true,即事件被消费了,那么直接退出。反之,如果没有任何一个View消费事件,那么最终Activity的onTouchEvent()方法将被调用,即Activity自己来处理事件。
Activity的Window是个抽象类,它的具体实现类是PhoneWindow。下面来看PhoneWindow的superDispatchTouchEvent()方法。
@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event);}
PhoneWindow的superDispatchTouchEvent()方法比较简单,它直接将事件传递给了DecorView。DecorView是Android系统中所有Activity页面布局的顶级父容器。平常我们在Activity的onCreate()方法中调用setContentView()方法来设置页面布局,其实页面布局是被添加到DecorView这个父容器中。下面来看DecorView的superDispatchTouchEvent()方法。
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event);}
DecorView的superDispatchTouchEvent()方法比较简单,它将事件传递给了父类的dispatchTouchEvent()方法。在Android中,所有的父容器都是继承自ViewGroup,而ViewGroup继承自View。ViewGroup重写了View的dispatchTouchEvent()方法,所以事件开始从ViewGroup中进行分发。
ViewGroup的事件分发
ViewGroup的dispatchTouchEvent()方法比较复杂,我们分段来分析。
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) { ... boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); resetTouchState(); } ... } ... return handled;}
前面说过,ACTION_DOWN事件表示一次手势动作产生的一系列事件的起始事件。在dispatchTouchEvent()方法的开始,如果是ACTION_DOWN事件,那么ViewGroup会做一些复位、重置操作。
private void cancelAndClearTouchTargets(MotionEvent event) { if (mFirstTouchTarget != null) { ... for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) { resetCancelNextUpFlag(target.child); dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits); } clearTouchTargets(); ... }}
ViewGroup使用mFirstTouchTarget变量来存储消费了事件的子控件。mFirstTouchTarget变量将所有消费了事件的子控件以链表的形式存储在一起。但是,通常要么没有子控件消费事件,要么只有一个子控件消费了事件。在cancelAndClearTouchTargets()方法中,如果之前有子控件消费了事件,那么ViewGroup将通过dispatchTransformedTouchEvent()方法向它们分发ACTION_CANCEL中止事件以便开始一轮新的事件传递。接着在clearTouchTargets()方法中将mFirstTouchTarget变量重置为null。
private void resetTouchState() { clearTouchTargets(); resetCancelNextUpFlag(this); mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; mNestedScrollAxes = SCROLL_AXIS_NONE;}
resetTouchState()方法中复位了一些标志位,包括了不允许父容器拦截事件的标志位FLAG_DISALLOW_INTERCEPT。接着往下看dispatchTouchEvent()方法。
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) { ... // 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; } ... // Check for cancelation. final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; ...}
这段代码的主要作用是检查ViewGroup是否拦截了事件、是否中止了事件传递。当ACTION_DOWN事件发生或者mFirstTouchTarget变量不为null,即之前有子控件消费了事件时,检查ViewGroup是否拦截事件。如果子控件没有调用ViewGroup的requestDisallowInterceptTouchEvent()方法来设置FLAG_DISALLOW_INTERCEPT标志位,那么ViewGroup将调用onInterceptTouchEvent()方法来决定是否拦截事件。
ViewGroup的onInterceptTouchEvent()方法默认返回false,即父容器默认是不拦截事件的。在平常的开发中,我们可以根据需要重写ViewGroup的onInterceptTouchEvent()方法来决定是否拦截事件。
如果ViewGroup拦截了ACTION_DOWN事件,那么mFirstTouchTarget变量将为null。根据上面的代码可以知道,当后续的ACTION_MOVE、ACTION_UP等其它事件到来时,intercepted直接为true,ViewGroup拦截事件。这种情况下所有的事件都将由ViewGroup自己处理,子控件一个事件也接收不到。所以,在平常的开发中一般不会让ViewGroup拦截ACTION_DOWN事件。
如果ViewGroup不拦截ACTION_DOWN事件,但是没有子控件消费ACTION_DOWN事件,那么mFirstTouchTarget变量将为null。同上,当后续的ACTION_MOVE、ACTION_UP等其它事件到来时,intercepted直接为true,ViewGroup拦截事件。这种情况下子控件只接收到一个ACTION_DOWN事件,不会接收到后续的其它事件。
接着往下看dispatchTouchEvent()方法。
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) { ... // Update list of touch targets for pointer down, if needed. final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { ... if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { ... final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { ... final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { ... if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } ... 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; } ... } if (preorderedList != null) preorderedList.clear(); } ... } } ...}
这段代码的主要作用是将事件分发到可以处理事件的子控件。当事件没有被中止和拦截时,如果是ACTION_DOWN事件,那么ViewGroup开始遍历子控件进行事件分发。ViewGroup主要通过两个方法来判断子控件是否可以接收事件,canViewReceivePointerEvents()方法判断子控件的可见性和是否有动画,isTransformedTouchPointInView()方法判断事件是否落在子控件的布局区域中。
当子控件满足条件时,ViewGroup将调用dispatchTransformedTouchEvent()方法将事件传递给子控件。如果dispatchTransformedTouchEvent()方法返回了true,即子控件消费了事件,那么将调用addTouchTarget()方法将子控件设置给mFirstTouchTarget变量,然后退出循环。如果没有一个子控件消费了事件,那么mFirstTouchTarget变量仍然为null。
接着往下看dispatchTouchEvent()方法。
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) { ... // Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it. Cancel touch targets if necessary. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } ...}
当ViewGroup一开始就拦截了ACTION_DOWN事件或者没有子控件消费ACTION_DOWN事件时,mFirstTouchTarget变量为null,ViewGroup将通过dispatchTransformedTouchEvent()方法将事件传递给自己处理。反之,如果有子控件消费了ACTION_DOWN事件,并且后续事件没有被ViewGroup拦截,那么ViewGroup将直接通过mFirstTouchTarget变量进行事件分发。
如果有子控件消费了ACTION_DOWN事件,即mFirstTouchTarget变量不为null,但是后续事件被ViewGroup拦截了,此时cancelChild为true,ViewGroup将通过dispatchTransformedTouchEvent()方法向子控件分发ACTION_CANCEL事件,之后mFirstTouchTarget将被置为null。当后续事件到来时,ViewGroup将自己处理拦截的事件了。
接着看下dispatchTransformedTouchEvent()方法。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // Canceling motions is a special case. We don't need to perform any transformations // or filtering. The important part is the action, not the contents. final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } ... // Perform any necessary transformations and dispatch. if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); } handled = child.dispatchTouchEvent(transformedEvent); } // Done. transformedEvent.recycle(); return handled;}
当参数cancel为true或者是ACTION_CANCEL事件时,ViewGroup通过dispatchTransformedTouchEvent()方法传递ACTION_CANCEL事件给子控件或者ViewGroup自身。反之,将传递其它事件。
如果参数child为null,那么将调用ViewGroup父类的dispatchTouchEvent()方法,即ViewGroup自己处理事件。如果child不为null,那么将调用child,即View的dispatchTouchEvent()方法,即子控件处理事件。此时,事件分发由ViewGroup传入了View。
View的事件分发
因为ViewGroup也继承自View,所以要特别说明一下这部分提到的View特指子控件,不包括ViewGroup。View的事件处理相对来说就比较简单了,来看下View的dispatchTouchEvent()方法。
public boolean dispatchTouchEvent(MotionEvent event) { ... boolean result = false; ... if (onFilterTouchEventForSecurity(event)) { ... //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是enabled状态并且设置了OnTouchListener时,View将先调用OnTouchListener的onTouch()方法。如果onTouch()方法返回了true,那么将不再调用View的onTouchEvent()方法。可见,View的OnTouchListener的优先级高于onTouchEvent()方法。
如果View没有设置OnTouchListener,那么onTouchEvent()方法将被调用。最后我们来看下onTouchEvent()方法。
public boolean onTouchEvent(MotionEvent event) { ... if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } ... if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { ... if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 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(); } } } ... } mIgnoreNextUpEvent = false; break; ... } return true; } return false;}
当View是disabled状态时,只要View是clickable的,onTouchEvent()方法将返回true。如果View是enabled状态并且是clickable的,onTouchEvent()方法默认也返回true。这说明,默认情况下只要有事件传递到了View并且View是clickable的,那么事件就会被消费。
阅读View的源码可以发现,默认情况下View不是clickable的,即默认情况下View没有消费事件。ViewGroup继承自View,但是ViewGroup没有重写View的onTouchEvent()方法,所以默认情况下ViewGroup也没有消费事件。
通过View的setClickable()、setLongClickable()和setContextClickable()方法可以设置相应的clickable状态。特别要提一下的是,平常我们通过View的setOnClickListener()方法设置监听器时其实也设置了View的clickable状态。
最后,在ACTION_UP事件时View将调用performClick()方法。
public boolean performClick() { 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); return result;}
在performClick()方法中,如果View设置了OnClickListener,那么将调用OnClickListener的onClick()方法。
到这里View的事件分发源码分析就结束了。
总结
通过对View的事件分发的源码进行分析,我们可以总结出以下一些结论:
- 触摸事件的传递顺序是:Activity -> Window -> DecorView -> 具体的页面布局容器 -> 具体的子控件。如果没有View消费事件,那么事件将逐级返回,最终Activity的onTouchEvent()方法会被调用。
- ViewGroup的onInterceptTouchEvent()方法默认返回false,即父容器默认是不拦截事件的。
- 如果父容器拦截了ACTION_DOWN事件,那么它的子控件一个事件也接收不到。
- 如果一个View没有消费ACTION_DOWN事件,那么后续的ACTION_MOVE、ACTION_UP等其它事件它都接收不到了。
- 如果一个View消费了ACTION_DOWN事件,并且后续事件没有被父容器拦截,那么父容器会将后续事件直接传递给此View。
- 如果一个View消费了ACTION_DOWN事件,但是后续事件被父容器拦截了,那么这个View只会再收到一个ACTION_CANCEL事件。
- 默认情况下,ViewGroup和View都是不消费事件的。
- OnTouchListener的onTouch()方法优先级高于onTouchEvent()方法。
例子
这里举了两个简单的例子。例子代码地址:https://github.com/chongyucaiyan/ViewDemo
第一个例子主要用来了解正常情况下触摸事件的传递顺序。
如上图所示,布局很简单,垂直方向的LinearLayout布局里放置了一个TextView和一个Button。代码里主要是在View的事件分发核心方法里加了日志打印,PhoneWindow和DecorView没办法加日志就没加了。首先,在TextView上触发一次手势,打印的日志如下图所示:
如上图所示,触摸事件从Demo01Activity传递到MyLinearLayout01父容器,最后传递到MyTextView01子控件。同时我们可以看到,默认情况下,父容器不拦截事件,父容器和子控件不消费事件。下面简要分析一下。
ACTION_DOWN事件发生后,事件先被传递到Demo01Activity的dispatchTouchEvent()方法。接着事件被传递到MyLinearLayout01的dispatchTouchEvent()方法。MyLinearLayout01的onInterceptTouchEvent()方法返回false,即父容器默认不拦截事件。接着事件被传递到MyTextView01的dispatchTouchEvent()方法。此时,MyTextView01调用onTouchEvent()方法来处理事件。MyTextView01的onTouchEvent()方法返回false,即子控件默认不消费事件。这时事件返回,MyLinearLayout01调用onTouchEvent()方法来处理事件。MyLinearLayout01的onTouchEvent()方法返回false,即父容器默认不消费事件。事件接着返回,最终Demo01Activity的onTouchEvent()方法被调用。
因为MyTextView01和MyLinearLayout01都没有消费ACTION_DOWN事件,所以后续的ACTION_MOVE、ACTION_UP事件它们都接收不到了。
然后,在Button上触发一次手势,打印的日志如下图所示:
如上图所示,默认情况下MyButton01消费了事件,代码里并没有给MyButton01设置OnClickListener。这是因为Android应用默认使用的theme之中设置了Button的clickable属性为true,造成Button默认消费事件。
MyButton01消费了ACTION_DOWN事件,所以MyLinearLayout01和Demo01Activity的onTouchEvent()方法都不会被调用了。并且MyButton01可以正常接收到后续的ACTION_MOVE、ACTION_UP事件。
第二个例子主要用来了解拦截情况下触摸事件的传递情况。
布局更简单,FrameLayout里放置了一个Button。代码里在MyFrameLayout02中重写了onInterceptTouchEvent()方法,对ACTION_MOVE事件进行拦截。
@Overridepublic boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: intercepted = false; break; case MotionEvent.ACTION_MOVE: intercepted = true; break; case MotionEvent.ACTION_UP: intercepted = false; break; default: intercepted = super.onInterceptTouchEvent(event); break; } Log.i(TAG, "onInterceptTouchEvent(), " + Utils.getActionString(event) + ", intercepted = " + intercepted); return intercepted;}
然后,在Button上触发一次手势,打印的日志如下图所示:
如上图所示,ACTION_DOWN事件被MyButton02正常消费,当ACTION_MOVE事件发生时,MyFrameLayout02对事件进行拦截。此时,MyButton02只再接收到一个ACTION_CANCEL事件,其它事件都接收不到了。
参考
- Android 7.1.1 (API level 25)
- https://developer.android.com/reference/android/view/View.html
- 源码分析View的事件分发机制
- View的事件分发源码分析
- View的事件分发机制源码分析
- View事件分发源码分析
- View事件分发源码分析
- android源码分析之View的事件分发(上)
- View的事件分发机制(源码分析)
- Android—— View事件分发机制的源码分析
- View事件的分发机制流程源码分析
- 源码跟踪分析View的事件分发(改)
- Android源码分析--View的事件分发机制
- View的事件分发机制,从源码角度分析一下
- view事件分发源码学习和分析
- View事件分发机制源码分析
- View事件分发源码分析(一)
- View事件分发源码分析(二)
- View的事件分发机制源码解析
- android View的事件分发源码解析
- [VIM]vim实用技巧小结
- leetcode--21. Merge Two Sorted Lists
- python中的切片以及注意事项
- 抓包看数据流转 | 小米音乐躺枪
- AndroidStudio中xml文件无法自动提示第三方库控件
- View的事件分发源码分析
- iOS Hacker 动态库 dylib 注入
- python中的字符串操作及注意事项
- Java EE
- [EMACS]smex-mode
- varnish
- 数据结构复习-线性表的定义和基本操作
- Spark编程的基本的算子之:combineByKey,reduceByKey,groupByKey
- 利python写用赌博游戏函数版赏析: