View事件分发机制——从现象深入源码的分析

234 阅读8分钟

前言

View事件分发机制是Android面试中备受关注的问题,今天我们就来扒开其表象,一起探究下他的本质。

TouchEvent的产生与封装

  当屏幕被触摸, 硬件设备会产生触摸事件传入内核层以及Framework层, 最后经过一系列事件处理到达ViewRootImpl的processPointEvent, 至于这中间的过程不是本次讨论的重点, 有想了解的小伙伴可参考从根源上看屏幕点击事件是如何传递到View中的, 当事件到达ViewRootImpl的processPointEvent方法之后调用mView.dispatchPointEvent(ev)开始对事件进行分发过程, 而此时的mView就是DecroView, dispatchPointEvent(ev)方法是其子类View的方法, 在View.dispatchPointEvent(ev)方法中对事件进行判断是否TouchEvent, 并将其封装成TouchEvent继续进行下一步的分发。 DecroView重写了dispatchTouchEvent(ev)方法, 并且在这里会获取到当前Window.CallBack(Window.Callback 是个接口,而 Activity 和 Dialog 都实现了这个接口)继而事件正式进入Activity, 这里如果不了解可以看下下面的Activity、Window与DecorView的之间的关系。 从点击屏幕到事件分发到Activity的过程如下:

image.png

//ViewRootImpl.java
private int processPointerEvent(QueuedInputEvent q) {
    boolean handled = mView.dispatchPointerEvent(event);//mView是DecroView
}

//View.java
public final boolean dispatchPointerEvent(MotionEvent event) {
    if (event.isTouchEvent()) {
        return dispatchTouchEvent(event);
    } else {
        return dispatchGenericMotionEvent(event);
    }
}

//DecroView.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    final Window.Callback cb = mWindow.getCallback();//Window.Callback 是个接口,而 Activity 和 Dialog 都实现了这个接口
    return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
            ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

关于Activity、Window、DecroView之间的关系

三者关系图如下:

image.png

Activity与Window关联

在Activity.attach()方法中会创建PhoneWindow, 并通过setCallBack()方法将自己传入Window中, 这样Activity和Window实现了互相持有。

//Activity.java
final void attach(){
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setCallback(this);//将Activity传入Window中
}

Window与DecroView的关联

  setContentView()经过一系列调用进入AppCompatDelegateImpl.setContentView(resId)中有一个ensureSubDecro()方法, 如果DecorView还没有创建就会进入createSubDecor()方法, 在这里会进行DecorView的创建以及与Window的绑定工作。

//MainActivity.java
override fun onCreate(savedInstanceState: Bundle?) {
    setContentView(R.layout.activity_main)
}
//AppCompatActivity.java
@Override
public void setContentView(@LayoutRes int layoutResID) {
    initViewTreeOwners();
    getDelegate().setContentView(layoutResID);
}

//AppCompatDelegateImpl.java
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
}
private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
        mSubDecor = createSubDecor();
    }
}
private ViewGroup createSubDecor() {
    ......
    mWindow.getDecorView();
    ......
    mWindow.setContentView(subDecor);
}

//PhoneWindow.java
@Override
public final @NonNull View getDecorView() {
    if (mDecor == null || mForceDecorInstall) {
        installDecor();
    }
    return mDecor;
}

private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor(-1);
    }else{
        mDecor.setWindow(this);
    }
}

protected DecorView generateDecor(int featureId) {
    ...
    return new DecorView(context, featureId, this, getAttributes());//把Window传入DecorView中
}

TouchEvent在Activity中的分发过程

  注意: 本次事件分发过程中的View布局为: xml文件下一个占满布局的OuterViewGroup, OuterViewGroup里面有一个占50%的InnerViewGroup, 要求在X轴滑动距离大于Y轴的时间InnerViewGroup响应, 否则OuterViewGroup响应。

  通过前面的一系列过程,Touch事件已经到达MainActivity.dispatchTouchEvent(ev)方法中,里面直接调用super.disptachTouchEvent()既进入其父类Activity的dispatchTouchEvent()方法中, 在这里就可以看出: 如果getWindow().superDispatchTouchEvent(ev)返回true那么其直接放回true, 并且onTouchEvent()方法就不会被执行, 否则就会进入onTouchEvent()方法, 并返回其执行结果。
TouchEvent在Activity中的分发过程如下:

image.png

//MainActivity.java
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    return super.dispatchTouchEvent(ev)
}

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

TouchEvent在ViewGroup中的分发过程

DOWN事件的分发过程

   上面的getWindow()方法返回Window, 它唯一的实现类就是PhoneWindow, 进入PhoneWindow的superDispatchTouchEvent()方法只是调用了mDecor.superDispatchTouchEvent(), 而DecorView的superDispatchTouchEvent()中只是调用了其父类ViewGroup.java的dispatchTouchEvent(ev)方法。

   当ACTION_DOWN进来时候, 先去判断一下拦截有没有被disllow, 如果没有会进入onInterceptTouchEvent(ev)判断是否拦截, DecorView重写的onInterceptTouchEvent(ev)方法会被执行, 这里会对点击的区域做出判断是否越界, 默认情况下会返回false既不拦截, 此时为DOWN事件, canceled以及intercept都为false, 并且newTouchTarget == null, 此时会进入子View的倒序遍历分过过程(这里会判断子View是否可以触摸等判断), 会将事件通过dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法分发给子View(此时的child为OuterViewGroup), 进而继续调用OuterViewGroup的dispatchTouchEvent(ev)方法, 至此事件分发到OuterViewGroup中。

   OuterViewGroup.dispatchTouchEvent(ev)方法只是调用其父类ViewGroup的分发方法, 其分发过程和上面基本一样, 只是重写了onInterceptTouchEvent(ev)方法, 在Down事件的时候记录下初始坐标的x,y位置并调用父类的拦截方法, 由于ViewGroup.onInterceptTouchEvent()默认返回false, 所以Down事件直接返回false, 后续来了move事件, 根据和初始位置的坐标距离计算在X,Y方向上的距离大小, 如果Y方向移动距离大,那么直接返回true,既拦截, 否则不拦截。

  当OuterViewGroup收到Down事件的时候其onInterceptTouchEvent返回false, 那么其Down事件会分发到其子View(InnerViewGroup)中, 由于InnerViewGroup的onInterceptTouchEvent以及dispatchTouchEvent方法都是调用其父类的方法而已, 所以最终还是进入ViewGroup.dispatchTouchEvent(ev)方法, 由于其没有子View, 所以mFirstTouchTarget == null, 最终事件会通过dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS)(此时的child == null)方法将事件分发到其父类View的dispatchTouchEvent(ev)方法中。

   注意: 针对OuterViewGroup其mFirstTouchTarget = newTouchTarget是包含InnerViewGroup信息的target, , InnerViewGroup的newTouchTarget, 以及mFirstTouchTarget 此时都为null

MOVE事件的分发过程

当MOVE事件到来之后, 根据上面的分析我们知道其会进入OuterViewGroup的dispatchTouchEvent(ev)方法, 此时:

  1. 如果Y轴的位移距离大于X轴, 那么onInterceptTouchEvent(ev)就会返回true既直接拦截, 那么事件不会被分发到其子View中, 而此时mFirstTouchTarget == newTouchTarget !=null, cancelChild = true;所以会执行dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)), 所以其子View(InnerViewGroup)会收到canceled消息,并且Target信息会被清除, 而后续的Move事件到来事件由于mFirstTouchTarget==null所以其会通过dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS)(其中child==null) 执行到其父类View的dispatchTouchEvent(ev)方法, 在这里的执行过程在后文会进行分析
  2. 如果Y轴的位移距离不大于X轴,那么onInterceptTouchEvent(ev)就会返回false既不拦截, 那么事件就会被分发到其子View中, 接下来的分析过程和Down基本相同了就。
//PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

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

//ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    //首先Down事件进来会进入此循环
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
        //如果拦截没有被disllow就会进入
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
        } else {
            intercepted = false;
        }
    }
    
    if (!canceled && !intercepted) {
        //ACTION_DOWN事件进来时候newTouchTarget == null, mFirstTouchTarget = null
        if (newTouchTarget == null && childrenCount != 0) {
            ...
            for (int i = childrenCount - 1; i >= 0; i--) {
                ...
                newTouchTarget = getTouchTarget(child);//这是个单链表的遍历查找
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    //只有子View消费了此事件, mFirstTouchTarget才会被赋值
                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                    alreadyDispatchedToNewTouchTarget = true;
                }
            }
        }
        //
        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;
        }
    }
    //没有子View或者没有子View消费此事件, 那么事件就会调用此ViewGroup处理过程
    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else{
        // 如果前面有子View消费了事件,但是此次父View又拦截了本次事件, 那么就给子View发送一个cancel事件, 并清除target,下次再来事件直接就分发给父View了
        TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                    //如果此时的父View拦截了事件, 那么cancelChild是true, 此时子View就会收到一个cancel事件, 如果子View消费了down事件,但是父View此时拦截了接下来的move事件, 那么子View会收到cancel事件
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                    }
                }
    }
}

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


private TouchTarget getTouchTarget(@NonNull View child) {
    for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
        if (target.child == child) {
            return target;
        }
    }
    return null;
}

//OuterViewGroup.java
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
    /**外部拦截法: 判断当前的事件我是否处理, 如果处理直接拦截,否则不拦截分发到子View中去**/
    if (event.action == MotionEvent.ACTION_DOWN) {
        startX = event.x
        startY = event.y
    }

    if (event.action == MotionEvent.ACTION_MOVE) {
        val pointX = event.x - startX
        val pointY = event.y - startY
        Log.d(TAG, "$TAG  onInterceptTouchEvent: x = $pointX, y = $pointY")
        if (Math.abs(pointX) < Math.abs(pointY)) {
            Log.d(TAG, "$TAG onInterceptTouchEvent: true")
            return true
        }
    }
    return super.onInterceptTouchEvent(event)
}

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
    Log.d(TAG, "$TAG dispatchTouchEvent: ${event.action}")
    return super.dispatchTouchEvent(event)
}


//InnerViewGroup.java
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
    return super.onInterceptTouchEvent(event)
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
    return super.dispatchTouchEvent(event)
}

TouchEvent在View中的分发过程

当事件进入View.dispatchTouchEvent()中, 会先判断其是否设置了OnTouchListener()如果设置了,那么就会执行他的onTouch()方法, 如果onTouch方法返回true,那么onTouchEvent()方法就不会被执行, 否则就会执行onTouchEvent()方法。由于InnerViewGroup重写了onTouchEvent()方法并直接返回true, 故分发放回回返回true, 由于其返回true故OuterViewGroup以及MainActivity的onTouch()方法就不会被执行。如果InnerViewGroup没有重写onTouchEvent那么就会执行其父类View的OnTouchEvent方法, 在performClick()方法中会对点击事件进行判断, 如果设置了OnClickListener()那么就会执行其onClick()方法。

//View.java
public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    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;
    }
}

public boolean onTouchEvent(MotionEvent event) {
    ...
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
            if (mPerformClick == null) {
                mPerformClick = new PerformClick();
            }
            if (!post(mPerformClick)) {
                performClickInternal();
            }
        }
    }
}

public boolean performClick() {
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
}

结语

上述为自己的一些理解,如有不同意见欢迎批评与指正!