当前位置:网站首页>根据热门面试题分析Android事件分发机制(一)

根据热门面试题分析Android事件分发机制(一)

2022-07-07 07:03:00 光阴剑客

(一)事件分发机制概述

面试题:你了解过Android的事件分发机制吗?请大致介绍一下

点击事件产生后,首先传递给Activity的dispatchTouchEvent方法,这时会调用getWindow().superDispatchTouchEvent(ev),由于PhoneWindow是Android Window的唯一实现类,所以会通过PhoneWindow中的mDecor.superDispatchTouchEvent(event)去调用父类的dispatchTouchEvent方法,mDecor 是DecorView,它继承自FrameLayout,FrameLayout继承自ViewGroup,因为FrameLayout没有重写dispatchTouchEvent方法,所以在DecorView中:

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

就会调用ViewGroup的dispatchTouchEvent方法,在该方法中会执行onInterecptTouchEvent方法判断是否拦截,在不拦截的情况下,会遍历ViewGroup的子元素,进入子view的dispatchTouchEvent方法,如果子view设置了onTouchListener,就执行onTouch方法,并根据onTouch方法的返回值为true还是false决定是否执行onTouchEvent方法,如果是false,则继续执行onTouchEvent方法,在onTouchEvent的ACTION_UP事件中判断,如果设置了onClickListener,就执行onClick方法。

(二)源码解析面试中的高频考点

面试题1:如果父view中不拦截down事件,拦截move,up事件,在子view中设置了requestDisallowInterceptTouchEvent(true);(请求父view不拦截事件)这个标志后,子view能收到move,up事件吗?

解析: 视情况而定:(1)假如子view消费down事件,并且设置了requestDisallowInterceptTouchEvent(true),子view可以收到move,up事件(2)子view不消费down事件,则只会收到down事件,不会收到move,up事件。原因如下:
(1)子view消费down事件:在我们父view不重写dispatchTouchEvent的情况下,会使用ViewGroup的dispatchTouchEvent方法分发事件。下面先通过源码了解这个问题的相关部分:

 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    
    ...//省略部分代码
     // 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);
                resetTouchState();//如果是down事件来的时候,重置Touch的状态
            }
            
   private void resetTouchState() {
    
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;//这个状态可以通过
        //requestDisallowInterceptTouchEvent(true)这个方法设置,
        //这里会重置它
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
    

上面的代码的意思就是当down事件来时会做一些状态重置,因为down事件是事件处理的开始,所以每次down事件来,都相当于一次事件处理的开始,需要将相关的状态重置。

然后是判断是否拦截事件部分的源码:

   @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    
    ......//省略部分代码
// Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
    
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                //如果事件是down事件的话,disallowIntercept
                //会一直是false,因为down的状态在刚开始时都被重置过了
                //mFirstTouchTarget是一个链表,用来保存用户第一次点击的信息,如果子view消费了事件,就会给这个变量赋值,反之则不会
                if (!disallowIntercept) {
    //所以down事件时,这个判断一定会进
                    intercepted = onInterceptTouchEvent(ev);
                    //最后是否拦截down事件就靠这里了,如果拦截了down事件
                   // onInterceptTouchEvent(ev)为true,那么
                    //子view就收不到事件了,设置啥标记也没有用,不拦截的话,down
                    //事件可以通过,子view有机会处理事件
                    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;
            }

上面的代码执行完会得到一个intercepted值,这个值直接会影响到事件的分发,同样在viewGroup的dispatchTouchEvent方法中:

  if (!canceled && !intercepted) {
    //在不拦截的情况下会进入下面的代码块
....//省略部分代码

                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();//遍历子view
                        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);
                                    //获取到子view的索引值
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
    
                                    //如果子view不能收到事件或者是没有在点击的范围内,也就是没点击到view上,结束本次循环,进行下一次循环遍历
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
    
                       //newTouchTarget不为空,表示当前的view已经处理了事件,直接结束循环。
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            //dispatchTransformedTouchEvent这个方法是询问子view是否处理事件,处理的话返回true,不处理返回false
                            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);//如果view处理事件的话就会通过addTouchTarget()为mFirstTouchTarget赋值,后续move和up事件不会被拦截。
                                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;
                    }
                }
            }

所以情况一根据上面的源码可以总结为:
(1)down事件到来后,首先会对其相关的状态进行重置,比如mFirstTouchTarget,FLAG_DISALLOW_INTERCEPT等
(2)接着就会判断是否拦截事件,如果是down事件,会直接进入是否进行拦截的代码块,这时候会判断disallowIntercept这个标志位,但是由于是down事件,这个标志位都被重置了,所以down事件的情况下,这个标志位一直是false,所以down事件会进入到判断的代码块中,由onInterceptTouchEvent决定是否拦截,如果拦截了,子view就收不到事件了,这里讨论的是不拦截的情况下:

 if (!disallowIntercept) {
    
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                }

(3)不拦截down事件的情况下,mFirstTouchTarget会被赋值,所以当move和up事件来临时,会进入到下面的代码块中

 if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
    
                    //mFirstTouchTarget不为空,会走入到是否拦截判断代码块中
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//disallowIntercept设置为true,则不会进入下面的判断代码块,所以就不会执行父view的onInterceptTouchEvent(ev)方法,所以在父view中设置啥拦截都没有用,反正也执行不了。若是设置为false,即子view设置了requestDisallowInterceptTouchEvent(false),那么会进入到下面的代码块中,具体是否拦截由父view的onInterceptTouchEvent(ev)方法决定
                if (!disallowIntercept) {
    
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
    
                    intercepted = false;
                }
            }

通过上面的源码分析我们可以知道,在父view不拦截down事件的时候,子view设置了requestDisallowInterceptTouchEvent(true),那么就算父view拦截了move和up事件也没有用,因为根本就执行不到父view的onInterceptTouchEvent(ev)方法。如果子view设置了requestDisallowInterceptTouchEvent(false)后,那么子view是否能收到事件得看父view的onInterceptTouchEvent方法处理逻辑,本题中是拦截move和up事件,所以子view中只会收到一个父view没有拦截的down事件,以及一个cancel事件。会收到cancel事件是因为move和up事件都被拦截了,不会进入到事件分发到逻辑里面,也就是父view的:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    
    ......
       if (!canceled && !intercepted) {
    
       ......//省略
       }

在子view消费事件的情况下(子view的dispatchTouchEvent返回true)

//子view消费事件,则mFirstTouchTarget不为空,如果子view不消费事件,则父view会自己去处理事件
 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;//由于拦截了move和up事件,所以这cancelChild会为true,
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
    
                            handled = true;
                        }
                   ......
            }
         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);
           // cancel就是上面传的cancelChild,在这里会设置一个ACTION_CANCEL事件,所以子view中消费事件的情况下会收到这个事件
            if (child == null) {
    
                handled = super.dispatchTouchEvent(event);
            } else {
    
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

所以在自view消费事件的情况下,父view中若是move和up事件被拦截,则子view会收到一个cancel事件。

面试题2:如果view设置了onTouchListener,onClickListener,onTouchEvent,那么会先执行哪一个?
解析: 在这个地方,回答的时候很多人可能都会回答,先执行onTouchListener的onTouch方法。然后就没了,这个回答表面上没有错,但是容易把天聊死。面试官也不知道该咋问了,只会吐出三个字,为什么?根据我的面试经验,这样是很难获得面试官的认可的,就算你答对了先执行哪一个函数,也只会让面试官觉得你在背面试题,虽然我们很多时候都是在背,但是我们要把原理弄清楚,争取不让面试官问太多的问题。争取一个回答将他想问的问题都回答完。
下面咱们看下应该怎么回答这种题,面试官问你这个问题,就是想考察你对事件分发机制的了解程度。我们先看源码;当子view不重写dispatchTouchEvent的时候,事件分发到子view后,会执行view的dispatchTouchEvent()方法。

 public boolean dispatchTouchEvent(MotionEvent event) {
    
 ...//省略掉不相关的代码
 //从下面的代码中不难看出,当用户设置了onTouchListener监听时,会去执行onTouch方法,根据onTouch()方法的返回值判断是否执行onTouchEvent.
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
    
                result = true;
            }
//如果onTouch()返回为true,下面的代码就不执行,因为java条件是短路的,也就是说如果//(!result)条件是false,与另外一个条件用“&&”连接的话,那么后面的一个条件就不会执行,整个判断返回false
            if (!result && onTouchEvent(event)) {
    
                result = true;
            }

由上面的代码可以知道,如果用户设置了onTouchListener,onTouchevent和onClickListener方法,那么会首先执行onTouchListener中的onTouch方法,然后根据onTouch的返回结果判断是否执行onTouchEvent,假设onTouch()返回false,继续执行onTouchEvent(),我们继续看源码:

 public boolean onTouchEvent(MotionEvent event) {
    
 ...省略部分代码
case MotionEvent.ACTION_UP:
       //......省略部分代码
       //在事件的 ACTION_UP 中会去执行clickListener的onClick()方法。 
       if (mPerformClick == null) {
    
            mPerformClick = new PerformClick();
          }
        if (!post(mPerformClick)) {
    
            performClickInternal();
         }
                            }
                        }
    public boolean performClick() {
    
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        //如果用户设置了onClickListener的话。就会执行onClick()方法
        if (li != null && li.mOnClickListener != null) {
    
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
    
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

综上:咱们的回答应该是,如果用户设置了onTouchListener,onTouchEvent,onClickListener时,首先会执行onTouchEvent的onTouch方法,根据onTouch的返回值决定是否执行onTouchEvent方法,如果onTouch方法返回false,则继续执行onTouchEvent,如果onTouchEvent中没有直接返回true或者false(这样会导致事件到不了view的OnTouchEvent方法,从而导致不执行咱们设置的onClickListener中的onClick方法),那么会执行到view的onTouchEvent方法,然后就会接着执行ClickListener的onClick方法

篇幅太长,所以事件分发机制的滑动冲突问题在下一个博客中总结,也是问题加源码分析的方式,希望各大朋友提出宝贵意见,大家一起进步~~~~,一起read the fucking sorce code!

原网站

版权声明
本文为[光阴剑客]所创,转载请带上原文链接,感谢
https://blog.csdn.net/zxj2589/article/details/125568753