Android View事件分发-从源码分析

View事件分发-从源码分析

学习自

《Android开发艺术探索》
https://blog.csdn.net/qian520ao/article/details/78555397?locationNum=7&fps=1
https://yq.aliyun.com/articles/267500?do=login&accounttraceid=dbd12b5a-dd5a-4599-a843-de5160e60936

闲谈

上一章我们已经,了解View时间分发的流程,那么本章呢,就从源码的角度看一看到底是否是这样的。

从Activity开始

首先呢,当一个事件产生的时候,会由Activity的 dispatchTouchEvent 方法进行分发。而分发事件的具体的总做是由Activity内部的Window完成的。Window会将事件传递给 decor view(Window的顶级View,这里不多做解释,详细请参考我在博客园找到的一篇Blog)。现在我们一步步分析

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    //!! 这里Activity将MotionEvent传递给了Window去处理
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    //!! 如果所有的View都没有将事件处理掉,那么Activity自己处理
    return onTouchEvent(ev);
}

从Activity的源码中,我们可以发现,实际上事件分发的工作是交给了Window来做,所以继续顺藤摸瓜,看看Window做了些什么工作。

Window

在这里,我遇到了一个没有见过类 Window ,所以向下学习之前,我要先搞清Window是什么。在寻找资料的过程中,我也发现了一些比较好的资源.
https://blog.csdn.net/qian520ao/article/details/78555397?locationNum=7&fps=1

首先我们来看一下官方文档的解释:

Abstract base class for a top-level window look and behavior policy. An instance of this class should be used as the top-level view added to the window manager. It provides standard UI policies such as a background, titlearea, default key processing, etc.
定义顶级Window的外观和行为策略的基类。此类的一个实例应该是被用来作为顶级View被添加到window manager 中。提供了标准的UI策略比如 背景,标题区域,默认秘钥处理等等。
The only existing implementation of this abstract class is android.view.PhoneWindow, which you should instantiate when needing a Window.
此抽象类的唯一的实现是 android.view.PhoneWindow,当你需要一个Window的时候请实例化它。

从官方文档和源码中我们可以得出,Phone是抽象类并且PhoneWindow是Window的唯一实现类,可能官方文档说的比较抽象一点,下面这个是我从上面的那个连接中找到的一个更通俗的解释:

Android手机中所有的视图都是通过Window来呈现的,像常用的Activity,Dialog,PopupWindow,Toast,他们的视图都是附加在Window上的,所以可以这么说 ——「Window是View的直接管理者。」

Window和Activity的关系

Activity一直以来给我们感觉就是界面,但是Activity本事并不能呈现界面,而是通过Window来实现界面的显式和对View的管理,Activity就是一个控制器控制Window实现功能. 这些我们都能够在源码中找到证据。

class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //查看setContentView 的源码
        setContentView(R.layout.activity_main)
    }
}

public void setContentView(@LayoutRes int layoutResID) {
    //!!在这里我们就可以了解到了Activity并没有初始化布局,而是通过Window来初始化并管理布局的
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

好了理解了Window我们接着向下看,这里对Window只是简单的了解了一下,有时间了还是得仔细了解一下Window。

Window是如何处理事件的

因为WIndow是一个抽象类本身并没有实现,具体的实现是在PhoneWindow中的,我们继续去PhoneWindow中寻找。

PS:在寻找PhoneWindow时,在AS中并不能只能感知到此类,所以我用EveryThink搜索了一下,或者大家直接从SDK的源码中找也行。

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

从上面PhoneWindow我们发现了PhoneWindow又将MotionEvent交给了mDecor去处理。现在问题又来了mDecor是什么?

@Override
public final View getDecorView() {
    if (mDecor == null || mForceDecorInstall) {
        installDecor();
    }
    return mDecor;
}

通过上面的方法,得知DecorView,而DecorView是Window的顶级的View,如果此时你从 setContentView 方法开始溯本求源查看源码的话,其实通过此方法设置的布局文件最终会成为 mDecor 的子View. 现在我们知道了mDecor是一个View了,但是具体还不是很了解,我们接着从源码中寻找。

PhoneWindow 的 setContentView方法

@Override
public void setContentView(int layoutResID) {
    //我们在这里找到了一些线索
    if (mContentParent == null) {
        installDecor();
//....
}

installDecor方法

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        //这里又发现了一些线索
        mDecor = generateDecor(-1);
//...
}
protected DecorView generateDecor(int featureId) {
    Context context;
    if (mUseDecorContext) {
        Context applicationContext = getContext().getApplicationContext();
        if (applicationContext == null) {
            context = getContext();
        } else {
            context = new DecorContext(applicationContext, getContext().getResources());
            if (mTheme != -1) {
                context.setTheme(mTheme);
            }
        }
    } else {
        context = getContext();
    }
    //注意看这里,在这里实例化了DecorView
    return new DecorView(context, featureId, this, getAttributes());
}

我们去寻找 DecorView 类.

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks

通过DecorView类的声明我们发现了此类继承自 FrameLayout 也就是说DecorView是 ViewGroup 的间接子类。最后得知: mDecor是一个ViewGroup

从ViewGroup开始分析-事件的拦截

我们从PhoneWindow终于找到了些线索,现在我们就回到我们熟悉的ViewGroup来分析。首先我们来分析ViewGroup的dispatch方法,以为此方法的代码量比较大,所以我们一步一步来分析。

//..... 前面的一些代码都是一些很简单的代码,这里就没有复制过来,
// Handle an initial down.
//当新的任务序列开始的时候(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.
    //将mFirstTouchTarget设置为NULL !!!
    cancelAndClearTouchTargets(ev);
    //在此方法中会重置FLAG_DISALLOW_INTERCEPT标记
    resetTouchState();
}
//
// Check for interception.
final boolean intercepted;
//!!首先需要注意这里, 这里是第一层判断
/*
* 1. 判断是否是事件序列的开始
* 2. 判断ViewGroup是否拦截了事件,如果拦截了事件的话mFirstTouchTarget=null
*/
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;
}
//...

源码中的注释和一些变量的名称上,可以了解到这一段代码是用来判断是否需要拦截事件。首先判断是否拦截事件有两个条件,具有其中一个条件即可。

  1. 事件类型=MotionEvent.ACTION_DOWN
  2. mFirstTouchTarget != null 表示ViewGroup不处理事件,而处理事件的子View

当事件序列刚开始,即Down事件触发,则是必须判断是否需要拦截事件,如果当前处理的事件不是Down事件,而是事件序列中的其他事件的时候,这是候 事件类型=MotionEvent.ACTION_DOWN 这一条件是无法通过的,所以只能看 mFirstTouchTarget != null 这一条件,当事件序列刚开始的时候,mFirstTouchTarget 是一个null值,如果Down事件触发的时候,并没有拦截事件,那么mFirstTouchEvent就会指向子View,条件满足继续判断是否需要拦截事件,如果条件不满足,则因为if语句的所有条件都不成立,所以在同一事件序列中的时间都不会被再次处理。

当第一个if语句通过了以后,还会有第二层判断 FLAG_DISALLOW_INTERCEPT 如果设置了次标记了的话(通过requestDisallowInterceptTouchEvent方法来设置并且一般是由子View来调用),那么ViewGroup将不能够拦截 Down 事件意外的任何事件,至于为什么能够拦截 Down 事件,疑问在分发事件的时候,如果是Down事件的话,此标记就会被重置。

结论

通过上面源码的分析我们证明了,当ViewGroup拦截了事件以后,后续的事件都会默认地交给ViewGroup本身去处理,并且不会重复地调用onInterceptTouchEvent方法同时也证明了上一章总结的最后一条的正确性。并且得到了以下的经验:

  • 如果要想提前处理所有的事件需要在 dispatchTouchEvent 方法中处理。
  • 通过FLAG_DISALLOW_INTERCEPT 标记我们可以利用其来处理滑动冲突,这将在之后的文章中将会介绍到

将事件分发给子View

如果ViewGroup不拦截事件,事件会向下分发给它的子View来处理,接下来我们来看一下它分发的过程。

//事件分发业务流程
// 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;
	//对DOWN事件进行处理
    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<View> preorderedList = buildTouchDispatchChildList();
            final boolean customOrder = preorderedList == null
                    && isChildrenDrawingOrderEnabled();
			//遍历子View
            final View[] children = mChildren;
            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.
                if (childWithAccessibilityFocus != null) {
                    if (childWithAccessibilityFocus != child) {
                        continue;
                    }
                    childWithAccessibilityFocus = null;
                    i = childrenCount - 1;
                }

                if (!canViewReceivePointerEvents(child)
                        || !isTransformedTouchPointInView(x, y, child, null)) {
                    ev.setTargetAccessibilityFocus(false);
                    continue;
                }
				//如果已经找到了接收TouchEvent的View那么跳出循环
                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);
				//如果接收TouchEvent的View不是此ViewGroup的子view那么继续循环分发
				//返回Ture则表示其中一个View消费了Touch事件
                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();
					//调用addTouchTarget()将child添加到mFirstTouchTarget链表的表头
                    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();
        }
		//此判断检测的是
		//newTouchTarge 为空,并且之前的mFirstTouchTarget还存在
		//表示子View没有消费事件将newTouchEvent指向最近的接收者
		//也就是mFirstTouchTarget
        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;
        }
    }
}//对Down事件的处理结束

//对事件的处理业务

//如果mFirstTouchTarget如果等于null,那么证明事件没有被消费
//出现这种情况的原因可能是:
//1. 没有找到消费事件的View 
//2. 事件被拦截了 
// 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 {//否则则表示找到了指定事件的消费者View并且事件没有被拦截
    // 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;
	//对于非ACTION_DOWN事件继续传递给目标子组件进行处理
	//依然是递归调用dispatchTransformedTouchEvent()
    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;
    }
}

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

dispatchTransformedTouchEvent

在上面的ViewGroup事件分发的代码中有一个十分重要的方法 dispatchTransformedTouchEvent 此方法负责将事件分发给子View,下面我们来看一看此方法.

Transforms a motion event into the coordinate space of a particular child view,
filters out irrelevant pointer ids, and overrides its action if necessary.
If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
将Motion Event转换到一个特定的View的坐标空间,过滤掉不相关的指针 id,并且如果必须则重写他们的行为。如果child 是 null,假定这个MotionEvent将会被传递给ViewGroup。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits)

这是此方法的签名,此方法共有三个参数

  • MotionEvent,这个不用多说
  • boolean cancel, 表示事件是否取消
  • View child 这个参数非常重要我们接下来会详细分析, 表示接受TouchEvent 的View
  • desiredPointerIdBits 一个指针参数

此方法的代码量也比较多,但是最核心的就是这一个判断,时间就是通过这个判断来分发到子View的。

//若Child为null那么表明,由ViewGroup自己处理TouchEvent
if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {//如果不为null 那么则分发给子View处理
    handled = child.dispatchTouchEvent(event);
}

View的dispatchTouchEvent

在View中dispatchTouchEvent方法的业务逻辑比较简单,因为不涉及到向下传递事件,其核心代码如下:

if (onFilterTouchEventForSecurity(event)) {
    if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
        result = true;
    }
    //noinspection SimplifiableIfStatement
    ListenerInfo li = mListenerInfo;
    //在这里我们就可以发现,OnTouchListener要优先于TouchEvent事件的
    //优先调用OnTouchListener的onTouch方法,如果onTouch方法没有消耗
    //事件,则交由TouchEvent方法去处理。
    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
    }
    //通过onTouchEvent方法去处理事件
    if (!result && onTouchEvent(event)) {
        result = true;
    }
}

View 的OnTouchEvent

此方法比较长,将分为几个部分来介绍。

首先呢,需要对View处于 Disable 状态的情况进行处理。下面的源码表明了Disable状态的View仍然会接收到事件,但是默认不会对其做任何处理,但是该消耗事件还是会消耗事件。

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事件处理的代理,如果设置有代理,那么则将事件交由代理处消耗

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}

接下来看一看View对点击事件的处理的核心代码

if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
    // This is a tap, so remove the longpress check
    removeLongPressCallback();
    // Only perform take click actions if we were in the pressed sta
    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();
        }
        //在performClick方法中会调用onClick事件
        if (!post(mPerformClick)) {
            performClick();
        }
    }
}

performClick

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        //如果存在onClickListener的haul,调用此回调
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    notifyEnterOrExitForAutoFillIfNeeded(true);
    return result;
}

总结

本文的篇幅有些长,其中源码非常多,但是学习的时候不一定要将所有的细节都弄清楚,毕竟这些代码都不是我们自己写的,很难免有些代码的左右不知道是干什么的,值需要大的方向掌握就行。

posted @ 2018-07-16 15:14  鲁迅认识的那只猹  阅读(210)  评论(0编辑  收藏  举报