本期三篇文章目录(可点击跳转)
一. Android TouchEvent事件传递机制初识
二. Android View 事件分发机制 源码解析(ViewGroup篇)
三.Android View 事件分发机制 源码解析(View篇)
android点击 事件一直以来都是很多安卓程序员的心病,之前通过demo模拟总结出一些经验,但是不看源码的程序员不是好程序员,这段时间,系统的梳理了下整个事件传递的源码,希望可以帮助大家彻底理解andriod的点击事件传递过程。
通过前段时间读源码,总结出一点心得:阅读源码要深入浅出,抓住主流程,忽略掉部分无关代码,避免一行一行的扣细节,容易把自己绕进去,造成最终读不下去。
1)activity中传递解析
当我们手指点击手机屏幕的时候,首先会触发activty的dispatchTouchEvent,源码如下:
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();//此方法是activity的方法,当此activity在栈顶时,用户对手机:触屏点击,按home,back,menu键都会触发此方法。注:下拉statubar,旋转屏幕,锁屏,不会触发此方法.
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
还好,代码不多,可以看出一共就两个判断,上面那个if注释已经写得很清晰了,由于跟点击分发其实没有多大关系,可以忽略。主要就是if (getWindow().superDispatchTouchEvent(ev))这个判断的代码理解,getWindow()返回Window对象,通过Window的源码可以看出,他是一个抽象类,那他的具体实现类是哪个?在Activity里面搜索window对象的创建部分代码,如下:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this);
......
}
很明显,PhoneWindow就是他的具体实现类,所以我们继续跟踪到PhoneWindow的superDispatchTouchEvent源码:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
我们看到,这边又有了一个mDecor,这个mDecor是什么呢?看下注释:
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
显然这个view就是activity的顶级view,通过getWindow().getDecorView()可以获取到mDecor对象,他继承FrameLayout,并且我们经常在activity里面通过setContentView加入的view其实就是mDecor的子view。到这里,点击事件已经从activity传到View当中了,接下来要分析的就是顶级View把事件如何分发到各个子view中了。
继续了解mDecor.superDispatchTouchEvent(event)的具体实现:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
实现很简单,就是调用父类的dispatchTouchEvent方法,刚刚说了mDecor继承FrameLayout,所以他的super肯定是ViewGroup,super.dispatchTouchEvent(event)则调用ViewGroup的dispatchTouchEvent方法,到此点击事件就传递到ViewGroup中。
2)ViewGroup中传递解析
继续查看ViewGroup的dispatchTouchEvent方法的源码:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
// 辅助功能焦点视图
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
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);//mFirstTouchTarget初始化
resetTouchState();//状态位重置
}
// Check for interception.
//是否拦截标志
final boolean intercepted;
//ACTION_DOWN或者mFirstTouchTarget != null都会进行到if当中,mFirstTouchTarget对象很关键
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//这个标志位通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)可以控制
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;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// 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 the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//倒序遍历所有的子view
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 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;
}
//调用getTouchTarget方法去查找当前子View是否在mFirstTouchTarget.next这条target链中,
//如果存在则返回这个target,否则返回null。
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()方法将Touch事件传递给特定的子View
//该方法返回false--则说明子view未消耗点击事件,从而下面的newTouchTarget = addTouchTarget(child, idBitsToAssign)方法无法调用,mFirstTouchTarget则为null
//该方法返回true--则说明子view消耗点击事件,从而进入if区域,从而mFirstTouchTarget不为null。
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);//给mFirstTouchTarget赋值,所有消耗点击事件的子View加入到mFirstTouchTarget链表中
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);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// Dispatch to touch targets.
// mFirstTouchTarget == null说明点击事件被拦截,或者子view没有消耗事件
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);//调用父类dispatchTouchEvent,再调用onTouchEvent处理焦点
} 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;
//根据alreadyDispatchedToNewTouchTarget 判断,如果已经分发了,则返回true
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//点击事件未被上面的子view消耗时,事件传递过程
//比如:有些人强制在ViewGroup中MotionEvent.ACTION_DOWN时,onInterceptTouchEvent返回false,ACTION_MOVE时,返回true,则进入到此方法
//清除掉mFirstTouchTarget链表中所有target,及mFirstTouchTarget==null;这样下一次直接跑入到
//if (mFirstTouchTarget == null)内容区域内,则点击事件传递到ViewGroup的onTouchEvent处理焦点
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;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
代码比较多,没有事,我们关注重点,忽略掉一些安全,控制的代码,分析上面的代码。
1)前面都是一些跟主流程无关的代码,我们直接看第19-25行
当点击事件是MotionEvent.ACTION_DOWN时,及手指刚刚触碰屏幕第一个触发的事件,里面调用了两个方法,cancelAndClearTouchTargets(ev)和resetTouchState();,下面分别来看下这两个方法的实现:
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) {
boolean syntheticEvent = false;
if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
}
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets();
if (syntheticEvent) {
event.recycle();
}
}
}
/**
* Clears all touch targets.
*/
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
可以看到cancelAndClearTouchTargets方法里面进行了mFirstTouchTarget的初始化工作,及把mFirstTouchTarget链表清空。
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
通过注释也可以看到,resetTouchState方法是重置所有触摸状态以准备新的循环,其中FLAG_DISALLOW_INTERCEPT标志位其实就是requestDisallowInterceptTouchEvent(boolean disallowIntercept)设置的标志位用来阻止父类拦截点击事件,所以在子view中调用requestDisallowInterceptTouchEvent方法的时候,对于MotionEvent.ACTION_DOWN事件是无效的,因为MotionEvent.ACTION_DOWN事件会清楚所有触摸状态。
2)继续看29-44行,判断是否父类是否拦截点击事件
在29行可以看到,通过intercepted布尔值来标记是否拦截,接下来通过if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null)判断,来确定是否进入到onInterceptTouchEvent方法中,一般两种情况下肯定会调用onInterceptTouchEvent方法:1.点击事件为MotionEvent.ACTION_DOWN。2.mFirstTouchTarget !=null及子类处理点了击事件,因为mFirstTouchTarget 是在dispatchTransformedTouchEvent返回true的时候赋值的(后面代码会介绍,现在先记住)。反过来,点击事件MotionEvent.ACTION_DOWN被ViewGroup拦截,及mFirstTouchTarget =null,则后面move,up事件将不在调用onInterceptTouchEvent方法,因为mFirstTouchTarget =null,直接跳过if语句执行下面的else语句。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//这个标志位通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)可以控制
通过位运算,判断是否进入onInterceptTouchEvent()方法,可见,我们一般控制滑动冲突时候,在子View中调用的requestDisallowInterceptTouchEvent(boolean disallowIntercept),就是在这里发挥的作用。
3)继续看61-169行,事件传递到子View
当没有被取消并且没有被拦截点击事件时,进入到if语句内,通过一个for循环i从childrenCount - 1开始遍历到0,倒序遍历所有的子view,第118行, newTouchTarget = getTouchTarget(child);调用getTouchTarget方法去查找当前子View是否在mFirstTouchTarget.next这条target链中,如果存在则返回这个target,并且跳出循环,并且此时alreadyDispatchedToNewTouchTarget =false,则事件的分发在186行里面执行,否则返回null。
130行到148行注释写得比较多,这边就不在说明了。
4)继续看172-210行,事件传递到子View
if (mFirstTouchTarget == null)如果条件成立,则说明mFirstTouchTarget 为空,没有被赋值,因为mFirstTouchTarget 是在第146行被赋值,根据分析不执行这行代码的逻辑如下:dispatchTransformedTouchEvent返回false或者点击事件被拦截。总之,就是事件没有被子View处理。这个时候,就入到if语句中,发现就一个方法,如下:
// Dispatch to touch targets.
// mFirstTouchTarget == null说明点击事件被拦截,或者子view没有消耗事件
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);//调用父类dispatchTouchEvent,再调用onTouchEvent处理焦点
}
继续看下dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);方法的源码:
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;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// 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;
}
可以看到,由于child=null,所以调用了handled = super.dispatchTouchEvent(transformedEvent);,及父类的dispatchTouchEvent方法,ViewGroup的父类也就是View,关于View的相关事件,在后面一篇博客中讲解,这里只要知道,调用了父类的dispatchTouchEvent。
如果mFirstTouchTarget !=null时,则执行179-210行代码,这里面的逻辑比较复杂,注释也比较多,主要做两件事:1.未下子View分发的事件继续向下分发(193行)2.清除掉mFirstTouchTarget链表中所有target(197行)
刚刚提到在第35行,会调用intercepted = onInterceptTouchEvent(ev);//调用拦截方法,我们看下这个方法的具体源码:
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
默认返回false,如果重写了ViewGroup的onInterceptTouchEvent方法,返回false就不阻止事件继续传递派发,否则阻止传递。
1.android的事件传递是先传递到父类,再到子类的。
2.ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,但是子View可以通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)控制父类的拦截事件是否调用。
3.子View消耗掉点击事件后,父类onTouchEvent方法不会调用,子View不消耗点击事件,会传到父类onTouchEvent方法,父类onTouchEvent方法返回false,则最终传递到activity的onTouchEvent方法。
4.ViewGroup一旦调用onInterceptTouchEvent方法拦截点击事件后,本次点击序列事件则都交于该ViewGroup处理,并且onInterceptTouchEvent将不再执行。原因见【2)继续看29-42行,判断是否父类是否拦截点击事件】中的解释。
5.当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发下一个action.也就是说,子view 未消耗点击事件,及dispatchTouchEvent返回false,这样mFirstTouchTarget =null,后面只能执行172行代码,则后续action直接由ViewGroup执行,不传递给子View
如有错误欢迎指出来,一起学习。
交流讨论群
群号:469890293