一. 概述
Android的事件分发主要有这几个角色:Activity、Window、ViewGroup和View。当Activity接收到事件时,会将事件传递给Window,然后Window将事件传递给顶层容器DecorView(继承自FrameLayout),事件分发由此开始。
这边我将对DOWN、MOVE和UP事件结合源码单独分析。
二. 源码分析
2.1 前言
首先先明确几个概念:
- 同一事件序列: 由一个DOWN事件,若干个MOVE事件,一个UP事件组成
- 新的一个事件序列开始前会重置所有的点击状态
当Activity接收到事件时,Activity的dispatchTouchEvent方法会被调用。
Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev);}复制代码
从代码中可以看到,Activity收到事件后将事件交由Windows处理
PhoneWindow.java
@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event);}复制代码
Window会将事件交由DecorView处理
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker { ... public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }}复制代码
由此可以看到事件传递给了ViewGroup,View事件分发由此开始。
2.2 DOWN事件
首先分析DOWN事件,当我们触摸手机屏幕的一瞬间,Activity接收到DOWN事件,事件由Activity传递到Window,再到DecorView。当DecorView接收到事件,会调用ViewGroup的dispatchTouchEvent方法。
由于是DOWN事件传递到ViewGroup,在dispatchTouchEvent方法中首先会重置触摸状态,包括清除保存处理事件View的单链表,因此每次DOWN事件代表一个新的事件序列的开始,这点之后会具体分析。
if (actionMasked == MotionEvent.ACTION_DOWN) { //重置所有的触摸状态 cancelAndClearTouchTargets(ev); resetTouchState();}复制代码
由于是DOWN事件,则先会去判断当前容器是否禁止拦截事件。默认情况下,父容器可以拦截事件,此时会调用onInterceptTouchEvent方法,该方法默认返回false;若父容器被禁止拦截事件,则不会调用onInterceptTouchEvent方法。
ViewGroup#dispatchTouchEvent
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 { //拦截事件 intercepted = true;}复制代码
2.2.1 父容器不拦截事件
这里先看默认情况,事件没有被父容器拦截,即intercepted为false。此时会去遍历该ViewGroup的子View,寻找能够处理事件的View,若找到发生触摸事件的View,将事件分发给对应的子View,若View能够处理事件,也就是子View的dispatchTouchEvent方法返回true,则将该处理事件的View加入mFirstTouchTarget这个链表中,并标记当前DOWN事件已被处理。
ViewGroup#dispatchTouchEvent
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) { //遍历所有的子View,寻找处理事件的View final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); ... if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { //找到处理事件的子View,保存该子View newTouchTarget = addTouchTarget(child, idBitsToAssign); //事件已经分发 alreadyDispatchedToNewTouchTarget = true; break; } } } }复制代码
父ViewGroup通过调用dispatchTransformedTouchEvent 将事件分发给对应的子View。子View处理了DOWN事件,也就是子View的dispatchTouchEvent方法返回true,从而会使得dispatchTransformedTouchEvent方法返回true。
ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { ... 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;}复制代码
若没有找到能够处理事件的子View,此时事件会交给当前ViewGroup来处理。没有找到处理DOWN事件的子View,也就是mFirstTouchTarget这个链表没有被赋值,此时为null。此时通过dispatchTransformedTouchEvent将事件传递给当前ViewGroup的父类,调用View的dispatchTouchEvent方法进行事件处理。
ViewGroup#dispatchTouchEvent
if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);}复制代码
接下来事件传递到了View,下方是View的dispatchTouchEvent方法。
View.java
public boolean dispatchTouchEvent(MotionEvent ev){ ... 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; } }复制代码
事件到了View的dispatchTouchEvent方法,先会去判断事件是否由OnTouchListener消费掉且View可用,若事件被消费,则该DOWN事件处理结束。若事件未被OnTouchListener消费掉,则将事件交由View的onTouchEvent方法。
View.java
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { //当前View不可用 if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } //若View可点击或者可长按,则事件被消费 return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); } ... if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { //若View可点击或者长按,则事件被消费 switch (event.getAction()) { case MotionEvent.ACTION_DOWN: ... break; } return true; } return false; }}复制代码
- 若当前View处于不可用状态,但是View可以被点击或者长按,则该事件被消费,反之事件未被当前View消费,则将事件交由父容器处理。
- 若当前View处于可用状态,并且View可以被长按或者点击,事件被消费,返回事件交由父容器处理。
2.2.2 父容器拦截了事件
上述2.1.1的前提是父容器没有拦截事件,也就是intercepted的值为false。若此时intercepted值未true(当onInterceptToucnEvent方法返回true)。此时不会去寻找处理事件的子View,也就是mFirstTouchTarget为null,同样事件会交由ViewGroup父类的dispatchTouchEvent方法处理。
2.2.3 DOWN事件分发总结
DOWN事件的分发流程如图所示,总的来说可以归纳一下几点:
- Activity接收DOWN事件后,将事件传递给Window,Window将事件分发给ViewGroup
- ViewGroup接收到DOWN事件,默认情况下ViewGroup会遍历所有子View,寻找发生触摸事件的子View,若找到子View且子View消费了DOWN事件,则ViewGroup会保存该处理事件的子View。
- 若ViewGroup拦截了事件,事件交由ViewGroup自己去处理,此时会调用ViewGroup父类的dispatchTouchEvent方法。在View处理事件时,若View可用并且OnTouchListener处理了事件,则DOWN事件被消费,DOWN事件分发结束;反之,则交由onTouchEvent方法处理DOWN事件
- 事件传递到了View的onTouchEvent方法中,只要View可点击或者长按,则事件一定被消费,反之,ViewGroup没有处理事件,事件交由父容器处理。
2.3 UP事件
上述2.2分析了DOWN事件的分发,接下来先分析UP事件的分发。 当手机抬起屏幕的一瞬间,Activity会接收到UP事件,Activity将UP事件传递给Window,Window将事件传递给DecorView,DecorView父类ViewGroup的dispatchTouchEvent方法被调用。
由于dispatchTouchEvent方法接收到的是UP事件,若mFirstTouchTarget不为null,此时代表存在处理DOWN和MOVE事件的子View。mFirstTouchTarget是一个链表,用于保存处理事件的子View,在DOWN事件被子View处理后再子View的父容器内被赋值,至于要用一个链表的原因是存在多点触控的情况,这里只考虑单点触控的事件分发。
2.3.1 存在处理DOWN和MOVE事件的子View
存在处理DOWN和MOVE事件的子View,也就是mFirstTouchTarget不为空。在mFirstTouchTarget不为空的情况下,会去判断当前容器是否禁止拦截事件。默认情况下为不拦截事件,此时会调用onInterceptTouchEvent方法,该方法默认返回false;
ViewGroup#dispatchTouchEvent
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 { //拦截事件 intercepted = true;}复制代码
2.3.1.1 当前容器不拦截UP事件
当intercepted为false时,此时为默认情况,代表当前容器不拦截UP事件,事件被分发给保存在mFirstTouchTarget的子View。
ViewGroup#dispatchToucnEvent
if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);} else { 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; //将事件分发给子View 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; }}复制代码
可以看到事件传递到了dispatchTransformedTouchEvent内部,由于child不为null,则会调用child的dispatchTouchEvent方法将事件分发给子View。
ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { ... 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;}复制代码
2.3.1.2 当前容器拦截UP事件
当intercepted的值未true时,代表当前UP事件被当前容器拦截。UP事件被当前容器拦截,但是之前的DOWN和MOVE事件都被子View处理了,此时mFirstTouchTarget不为空,所以此时走else分支,取消当前的UP事件,变为CANCEL事件,往下分发或者交由自己处理,并且此时会在遍历时清空保存在mFirstTouchTarget中处理事件的子View,最终mFirstTouchTarget的值为空。
ViewGroup#dispatchTouchEvent
if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { //由于事件被拦截,intercepted为true,cancelChild为true,代表取消事件 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; }}复制代码
由于是UP事件,最终会清除View的触摸状态
ViewGroup#dispatchTouchEvent
if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { resetTouchState();}复制代码
2.3.2 不存在处理UP事件的子View
当mFirstTouchTarget为空时,不存在处理事件的子View,此时容器父类View的dispatchTouchEvent方法会接收到事件。
if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { ...}复制代码
事件传递到View的dispatchTouchEvent方法,同样先会去判断事件是否由OnTouchListener消费掉,若事件被消费且View可用,则该UP事件处理结束。若事件未被OnTouchListener消费掉,则将事件交由View的onTouchEvent方法。
View.java
public boolean dispatchTouchEvent(MotionEvent ev){ ... 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; } }复制代码
事件传递到View的onTouchEvent中,可以看到在UP的时候在条件满足的情况下会执行单击事件,同时事件被消费。
View.java
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { //View不可用,但View可单击或者长按,同样可以消费事件 return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); } if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { //View 可点击或者长按 switch (event.getAction()) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { .. if (!mHasPerformedLongPress) { removeLongPressCallback(); if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } //执行单击事件 if (!post(mPerformClick)) { performClick(); } } } } break; } return true; } return false;}复制代码
UP事件交给ViewGroup自己处理时,除了在UP事件会在条件满足下触发单击事件,其余的流程和ViewGroup处理DOWN事件类似。
2.3.3 UP事件总结
2.4 MOVE事件
MOVE事件和UP事件流程类型
最后给大家分享一份非常系统和全面的Android进阶技术大纲及进阶资料,及面试题集
想学习更多Android知识,请加入Android技术开发企鹅交流 7520 16839
进群与大牛们一起讨论,还可获取Android高级架构资料、源码、笔记、视频
包括 高级UI、Gradle、RxJava、小程序、Hybrid、移动架构、React Native、性能优化等全面的Android高级实践技术讲解性能优化架构思维导图,和BATJ面试题及答案!
群里免费分享给有需要的朋友,希望能够帮助一些在这个行业发展迷茫的,或者想系统深入提升以及困于瓶颈的朋友,在网上博客论坛等地方少花些时间找资料,把有限的时间,真正花在学习上,所以我在这免费分享一些架构资料及给大家。希望在这些资料中都有你需要的内容。