您的位置:首页 > 移动开发 > Android开发

Android源码分析--View的事件分发机制

2017-07-18 15:45 711 查看

Android源码分析–View的事件分发机制

网上关于View的事件分发机制的文章有很多,所以这次更博呢,更重要的目的是让自己重新温习一遍View的分发机制,通过文字的描述,希望自己能讲透讲明白这个过程,从头到尾从源码层面去剖析内部原理。

看了本文你能学到什么?

View事件的分发流程;

View事件分发过程中,源码级的分析

View事件的分发流程

首先我们来看一张图片,大体上了解下这整一个过程



从上图我们可以看到,最先拿到View事件的是Activity,Activity可以先下分发,也可以自己消费,向下分发这个过程,下面我们会通过源码来分析,我们主要先看下大体的一个流程。可以发现,Activity、ViewGroup、View三者都能消费事件,事件消费既在onTouchEvent中返回true。如果View和ViewGroup都不消费这个事件,最终事件会交还给Activity,由它来消费。当然,ViewGroup中还可以拦截事件,不向下继续传递。这一part先到这里,看不太不明白没关系其实,下面我们将从源码层面去一步步跟踪分析这一个过程。

View事件分发过程中,源码级的分析

首先,我们先得了解事件分发过程中,非常重要的三个方法:

public boolean dispatchTouchEvent(MotionEvent ev); //用来分发事件

public boolean onInterceptTouchEvent(MotionEvent ev); //用来拦截事件

public boolean onTouchEvent(MotionEvent ev); //用来处理事件

这三个方法,在事件分发过程中,起了非常重要的作用,需要注意的是,onInterceptTouchEvent是ViewGroup才有的方法。我们先要有一个大体的了解,上面我们说了,首先获得事件的是Activity,获得事件之后,会调用自己的dispatchTouchEvent,我们来看下源码:

public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}


可以看到,方法里面有一句getWindow().superDispatchTouchEvent(ev),这句话是调用了Window的superDispatchTouchEvent(ev)方法,事件就交给了Window,但是,问题来了,假如对源码有一定了解的话,Window是一个抽象类,该方法是空方法,那么,我们就得着Window的实现类,我们知道,Android中,PhoneWindow是Window的唯一实现类,所以,我们在PhoneWindow中查看superDispatchTouchEvent(ev)的具体实现:

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}


哈哈,坑爹了吧,这里直接return的是mDecor的superDispatchTouchEvent(ev)返回值,那么,事件就传递到了mDecor上面了。什么是mDecor呢,这是PhoneWindow中的一个成员变量,是DecorView的一个对象,我们回忆一下,我们在Activity的onCreate方法里面有这一句:setContentView(view),这个View我们可以通过getWindow().getDecorView().findViewById(R.id.content)得到View的父控件,这的返回值其实是FrameLayout,我们把返回值强转为ViewGroup之后调用getChildAt(0),得到的就是setContentView中我们设置的View。

刚刚说到现在事件由PhoneWindow传递到了DecorView上面,而DecorView是继承自FrameLayout的,所以,到这一步,实际上事件就已经传递给View了。我们接下来看下DecorView中的superDispatchTouchEvent(event)方法:

public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}


可以看到,还是没有具体的处理case,继续往前看,刚刚说到DecorView是继承自FrameLayout的,FrameLayout是没有实现dispatchTouchEvent方法的,而FrameLayout有是继承自ViewGroup的,所以 我们可以直接看ViewGroup中的dispatchTouchEvent方法,由于这个方法实现过长,我们分开看

// Handle an initial down.
//如果是down事件的话,设置的请求不拦截的FLAG_DISALLOW_INTERCEPT标记将被清空,
//这个标记位是在requestDisallowInterCeptTouchEvent方法中
//设置的。换句话表达,就是ViewGroup将无法拦截设置了
//FLAG_DISALLOW_INTERCEPT标记的move和up事件。
//下面的代码,如果是down事件,将重置标记位,所以如果down事件
//设置了FLAG_DISALLOW_INTERCEPT标记,其实也是没用的,
//依然会判断是否拦截。这点需要非常清楚,后续的事件冲突处理需要灵活运用。
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();
}
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;
}


//设置标记位
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}

if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}

// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}


上面的代码是关于拦截事件的判断,我们可以看到,条件是down事件或者mFirstTouchTarget不为空时,才可能拦截,也就是说,当事件是move或者up时,mFirstTouchTarget不为空才能进入这个if分支。那么mFirstTouchTarget是什么呢?看到这里我也不明白,咱暂且先放下。继续往下看

// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;

// 是否需要拆分MotionEvent动作,默认为拆分
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//MotionEvent没有被取消并且没有被拦截时进入分支
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;
//当前事件的down,或者接下来手指的down,这里需要明白
//一个事情,MotionEvent.ACTION_DOWN是指出发当前MotionEvent事件的触点的down事件,
//MotionEvent.ACTION_POINTER_DOWN是指有另外的触点触发的down事件,
//这里其实是一个多点触控的逻辑,以下,大致理解为找出触发当前一系列MotionEvent的触点,
//然后获取事件相当于父控件的X,Y坐标。
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.
//清除触点信息,只留下动作信息,一个MotionEvent是由Action和Pointer组成的
//一个16位值,前8位表示触点信息,后八位表示动作信息。
removePointersFromTouchTargets(idBitsToAssign);

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
//子View根据动作索引去获取当前触点的相当于父控件(其实就是当前这个ViewGroup)X,Y坐标
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<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//遍历所有子View
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.
//子View是否能获取焦点
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//子View是否能接受事件,坐标是否在当前控件范围内
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实际调用的是子View(child)的dispatchTouchEvent方法。
//实际上就是事件传递给子View的一个过程。如果子View的dispatchTouchEvent返回true,
//mFristTouchEvent会被赋值,并且终止循环,如果返回false,那么循环继续,直至没有子View
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();
//mFristTouchEvent就是在下面这个方法被赋值的。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
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();
}

...
}
}


上面可以看到,当TouchEvent被传递给child时,mFristTouchEvent会被赋值。

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


可以看到 其实TouchTarget是一种单链表的结构,mFirstTouchTarget是否被赋值,将影响到ViewGroup的拦截case,那么,我们需要回过头再去想想我们放下没有解决的问题,mFirstTouchTarget在事件分发给子View之后会被赋值,就意味着,onInterCeptTouchEvent这个方法的调用时机实际上是如果ViewGroup拦截了当前的down事件的话,那么mFristTouchEvent必然为null,onInterCeptTouchEvent这个方法不会被调用,而是由它直接处理。

所以我们得出以下结论:

如果ViewGroup拦截了TouchEvent事件,那么,move和up事件也必然交由它处理,不再分发。

接下来我们继续往下看,如果mFristTouchEvent为空时如何处理的:

if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}


上面调用了dispatchTransformedTouchEvent方法,注意,这里是第二次调用这个方法了,第一次是在ViewGroup分发事件给子View时,child传递是的非空的,第二次是上面,我们可以看到 ,这里的第三个参数传递的是null,我们跟踪下,只看它关键部分的代码:

if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
//child为空,事件交到View的dispatchTouchEvent处理
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {//子View处理
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}


接下来看下View的dispatchTouchEvent的实现:

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


因为View没有子View,这里的实现也比较简单,我把主要的代码贴出来了,从上面的代码可以看出,只要一个View设置了OnTouchListener,那么,将执行的是onTouch方法,通过onTouch返回true,事件到此为止消耗完毕,如何OnTouchListener为null,那么将执行View的onTouchEvent方法,执行完毕如果返回true,事件消费完毕。可以这么理解,OnTouchListener的优先级是高于onTouchEvent的。

接下来我们看下View中的onTouchEvent方法的实现:

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);
}
//如果View设置有代理,将执行TouchDelegate中的onTouchEvent方法
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}


上面的代码,我们可以得出一个结论

一个View的状态即使是disable的状态下,照样是能消费点击事件的。

//只要View的CLICKABLE、LONG_CLICKABLE或者CONTEXT_CLICKABLE有一个为true,那么就会消费这个事件
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) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}

if (prepressed) {
// The button is being released before we actually
// showed it as pressed.  Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}

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

if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}

if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}

removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;

case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;

if (performButtonActionOnTouchDown(event)) {
break;
}

// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();

// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;

case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;

case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);

// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();

setPressed(false);
}
}
break;
}

return true;
}

return false;
}


up事件发生时,会触发调用performClick()方法,如果View设置了clickListener,performClick中会调用它的onClick方法。

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


以上,就是一个TouchEvent完整的分发机制,然后,看懂上面的代码,回过头再看这图片,是不是非常简单了呢?祝各位进步!

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息