美文网首页
Android View事件分发机制

Android View事件分发机制

作者: wayDevelop | 来源:发表于2018-10-19 16:44 被阅读0次

    Android事件分发机制主要由“事件分发”—>“事件拦截”—>“事件响应”这三步来进行逻辑控制的。本文也将从这三步对应的函数来分析。

    ViewGroup的三个方法:

    dispatchTouchEvent:事件分发
    onInterceptTouchEvent:事件拦截
    onTouchEvent:事件触发

    View的两个方法

    dispatchTouchEvent:事件分发
    onTouchEvent:事件触发

    点击事件有三个重要的方法它们分别是:

    • dispatchTouchEvent(MotionEvent ev):用来进行事件的分发

    • onInterceptTouchEvent(MotionEvent ev):用来进行事件的拦截,在dispatchTouchEvent()中调用,需要注意的是View没有提供该方法

    • onTouchEvent(MotionEvent ev):用来处理点击事件,在dispatchTouchEvent()方法中进行调用

    onTouchEvent()和onTouch()方法优先级及控制关系

    ①如果onTouch()方法返回值是true(事件被消费)时,则onTouchEvent()方法将不会被执行;

    ②只有当onTouch()方法返回值是false(事件未被消费,向下传递)时,onTouchEvent方法才被执行。

    由此可见,给View设置的OnTouchListener,其优先级比onTouchEvent要高,假如onTouch方法返回false,会接着触发onTouchEvent,反之onTouchEvent方法不会被调用。内置诸如click事件的实现等等都基于onTouchEvent,假如onTouch返回true,这些事件将不会被触发。


    我们对屏幕的点击,滑动,抬起等一系的动作都是由一个一个MotionEvent对象组成的。根据不同动作,主要有以下三种事件类型:
    1.ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
    2.ACTION_MOVE:手指在屏幕上移动时候产生该事件
    3.ACTION_UP:手指从屏幕上松开的瞬间产生该事件

    从ACTION_DOWN开始到ACTION_UP结束我们称为一个事件序列

    正常情况下,无论你手指在屏幕上有多么骚的操作,最终呈现在MotionEvent上来讲无外乎下面两种。
    1.点击后抬起,也就是单击操作:ACTION_DOWN -> ACTION_UP
    2.点击后再风骚的滑动一段距离,再抬起:ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP

    举个例子

     btn.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    switch (event.getAction()) {
                        case MotionEvent.ACTION_DOWN:
                            Log.w("wangwei", "MotionEvent: ACTION_DOWN");
                            break;
                        case MotionEvent.ACTION_MOVE:
                            Log.w("wangwei", "MotionEvent: ACTION_MOVE");
                            break;
                        case MotionEvent.ACTION_UP:
                            Log.w("wangwei", "MotionEvent: ACTION_UP");
                            break;
                    }
                    return false;
                }
            });
    
            btn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Log.w("wangwei", "-----onClick---");
                }
            });
        }
    
           
           点击按钮后  log日志
           W/wangwei: MotionEvent: ACTION_DOWN
           W/wangwei: MotionEvent: ACTION_UP
           W/wangwei: -----onClick---
    
    

    细心的同学一定发现了我们常用的按钮的onclick事件都是在ACTION_UP以后才被调用的。这和View的事件分发机制是不是有某种不可告人的关系呢?

    上面代码我们给button设置了OnTouchListener并重写了onTouch方法,方法返回值默认为false。如果这里我们返回true,
    onClick方法不再执行了!为什么会这样呢?你可以先理解成onTouch方法返回true就认为这个事件被onTouch消费掉了,因而不会再继续向下传递。

    首先你需要知道一点,只要你触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent方法。那当我们去点击按钮的时候,就会去调用Button类里的dispatchTouchEvent方法,可是你会发现Button类里并没有这个方法,那么就到它的父类TextView里去找一找,你会发现TextView里也没有这个方法,那没办法了,只好继续在TextView的父类View里找一找,这个时候你终于在View里找到了这个方法,示意图如下:



    当一个MotionEvent产生了以后,就是你的手指在屏幕上做一系列动作的时候,系统需要把这一系列的MotionEvent分发给一个具体的View。我们重点需要了解这个分发的过程,那么系统是如何去判断这个事件要给哪个View,也就是说是如何进行分发的呢?

    事件分发需要View的三个重要方法来共同完成:

    • public boolean dispatchTouchEvent(MotionEvent event)
      通过方法名我们不难猜测,它就是事件分发的重要方法。那么很明显,如果一个MotionEvent传递给了View,那么dispatchTouchEvent方法一定会被调用!
      返回值:表示是否消费了当前事件。可能是View本身的onTouchEvent方法消费,也可能是子View的dispatchTouchEvent方法中消费。返回true表示事件被消费,本次的事件终止。返回false表示View以及子View均没有消费事件,将调用父View的onTouchEvent方法。
    • public boolean onInterceptTouchEvent(MotionEvent ev)
      事件拦截,当一个ViewGroup在接到MotionEvent事件序列时候,首先会调用此方法判断是否需要拦截。特别注意,这是ViewGroup特有的方法,View并没有拦截方法
      返回值:是否拦截事件传递,返回true表示拦截了事件,那么事件将不再向下分发而是调用View本身的onTouchEvent方法。返回false表示不做拦截,事件将向下分发到子View的dispatchTouchEvent方法。
    • public boolean onTouchEvent(MotionEvent ev)
      真正对MotionEvent进行处理或者说消费的方法。在dispatchTouchEvent进行调用。
      返回值:返回true表示事件被消费,本次的事件终止。返回false表示事件没有被消费,将调用父View的onTouchEvent方法

    上面的三个方法可以用以下的伪代码来表示其之间的关系。

    public boolean dispatchTouchEvent(MotionEvent ev) {
            boolean consume = false;//事件是否被消费
            if (onInterceptTouchEvent(ev)){//调用onInterceptTouchEvent判断是否拦截事件
                consume = onTouchEvent(ev);//如果拦截则调用自身的onTouchEvent方法
            }else{
                consume = child.dispatchTouchEvent(ev);//不拦截调用子View的dispatchTouchEvent方法
            }
            return consume;//返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法
        }
    
    

    ViewGroup是View的子类,也就是说ViewGroup本身就是一个View,但是它可以包含子View(当然子View也可能是一个ViewGroup),所以不难理解,上面所展示的伪代码表示的是ViewGroup 处理事件分发的流程。而View本身是不存在分发,所以也没有拦截方法(onInterceptTouchEvent),它只能在onTouchEvent方法中进行处理消费或者不消费。
    上面结论先简单的理解一下,通过下面的流程图,会更加清晰的帮助我们梳理事件分发机制


    image.png image.png

    可以看出事件的传递过程都是从父View到子View。

    但是这里有三点需要特别强调一下
    • 子View可以通过requestDisallowInterceptTouchEvent方法干预父View的事件分发过程(ACTION_DOWN事件除外),而这就是我们处理滑动冲突常用的关键方法。关于处理滑动冲突,我们下一篇文章会专门去分析,这里就不做过多解释。

    • 对于View(注意!ViewGroup也是View)而言,如果设置了onTouchListener,那么OnTouchListener方法中的onTouch方法会被回调。onTouch方法返回true,则onTouchEvent方法不会被调用(onClick事件是在onTouchEvent中调用)所以三者优先级是onTouch->onTouchEvent->onClick

    • View 的onTouchEvent 方法默认都会消费掉事件(返回true),除非它是不可点击的(clickable和longClickable同时为false),View的longClickable默认为false,clickable需要区分情况,如Button的clickable默认为true,而TextView的clickable默认为false。


    View事件分发源码

    点击事件产生最先传递到当前的Activity,由Acivity的dispatchTouchEvent方法来对事件进行分发。那么很明显我们先看Activity的dispatchTouchEvent方法

       Class Activity:
    
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onUserInteraction();
            }
            if (getWindow().superDispatchTouchEvent(ev)) {//事件分发并返回结果
                return true;//事件被消费
            }
            return onTouchEvent(ev);//没有View可以处理,调用Activity onTouchEvent方法
        }
    
    

    通过上面的代码我们可以发现,事件会给Activity附属的Window进行分发。如果返回true,那么事件被消费。如果返回false表示事件发下去却没有View可以进行处理,则最后return Activity自己的onTouchEvent方法。

    到这里事件已经被传递到我们的顶级View中,一般是ViewGroup。
    那么接下来重点将放到ViewGroup的dispatchTouchEvent方法中。我们之前说过,事件到达View会调用dispatchTouchEvent方法,如果View是ViewGroup那么会先判断是否拦截该事件

    class ViewGroup:
        public boolean dispatchTouchEvent(MotionEvent 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);
                resetTouchState();//清除FLAG_DISALLOW_INTERCEPT设置,并且mFirstTouchTarget 设置为null
            }
            // Check for interception.
            final boolean intercepted;//是否拦截事件
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //FLAG_DISALLOW_INTERCEPT是子类通过requestDisallowInterceptTouchEvent方法进行设置的
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //调用onInterceptTouchEvent方法判断是否需要拦截
                    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;
            }
            ...
        }
    

    当ViewGroup决定拦截事件后,后续事件将默认交给它处理并且不会再调用onInterceptTouchEvent方法来判断是否拦截。子View可以通过设置FLAG_DISALLOW_INTERCEPT标志位来不让ViewGroup拦截除ACTION_DOWN以外的事件。

    所以我们知道了onInterceptTouchEvent并非每次都会被调用。如果要处理所有的点击事件那么需要选择dispatchTouchEvent方法
    而FLAG_DISALLOW_INTERCEPT标志位可以帮助我们去有效的处理滑动冲突

    当ViewGroup不拦截事件,那么事件将下发给子View进行处理。
    class ViewGroup:
        public boolean dispatchTouchEvent(MotionEvent ev) {
            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.
                if (childWithAccessibilityFocus != null) {
                    if (childWithAccessibilityFocus != child) {
                        continue;
                    }
                    childWithAccessibilityFocus = null;
                    i = childrenCount - 1;
                }
     
                //判断1,View可见并且没有播放动画。2,点击事件的坐标落在View的范围内
                //如果上述两个条件有一项不满足则continue继续循环下一个View
                if (!canViewReceivePointerEvents(child)
                        || !isTransformedTouchPointInView(x, y, child, null)) {
                    ev.setTargetAccessibilityFocus(false);
                    continue;
                }
     
                newTouchTarget = getTouchTarget(child);
                //如果有子View处理即newTouchTarget 不为null则跳出循环。
                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第三个参数child这里不为null
                //实际调用的是child的dispatchTouchEvent方法
                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();
                    //当child处理了点击事件,那么会设置mFirstTouchTarget 在addTouchTarget被赋值
                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                    alreadyDispatchedToNewTouchTarget = true;
                    //子View处理了事件,然后就跳出了for循环
                    break;
                }
            }
        }
    

    上面代码是将事件分发给子View的关键代码,需要关注的地方都加了注释。分发过程首先需要遍历ViewGroup的所有子View,可以接收点击事件的View需要满足下面条件

    我们看到了for循环,首先遍历ViewGroup的子元素,判断子元素是否能够接收到点击事件,如果子元素能够接收到则交由子元素来处理。接下来看看dispatchTransformedTouchEvent()方法中实现了什么:

    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;
           }
     ...省略      
    }
    

    这里child是我们遍历传入的子View此时不为null,则调用了child.dispatchTouchEvent(event);

    我们再来看看View的dispatchTouchEvent(event):

    
    public boolean dispatchTouchEvent(MotionEvent event) {
          ...省略
           boolean result = false;
           if (onFilterTouchEventForSecurity(event)) {
               //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;
       }
    
    

    我们看到如果OnTouchListener不为null并且onTouch()方法返回true,则表示事件被消费,就不会执行onTouchEvent(event),否则就会执行onTouchEvent(event)。再来看看onTouchEvent()方法的部分源码:

    public boolean onTouchEvent(MotionEvent event) {
         ...省略
           final int action = event.getAction();
           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 (!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();
                                   }
                               }
                           }
          ...省略    
           }
           return true;
          }          
          return false;
       }
    
    

    上面可以看到只要View的CLICKABLE和LONG_CLICKABLE一个为true,那么onTouchEvent就会返回true消耗这个事件。CLICKABLE和LONG_CLICKABLE代表View可以被点击和长按点击,可以通过View的setClickable和setLongClickable方法来设置,也可以通过View的setOnClickListenter和setOnLongClickListener来设置,他们会自动将View的设置为CLICKABLE和LONG_CLICKABLE。
    接着在ACTION_UP事件会调用performClick()方法:

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

    如果View设置了点击事件OnClickListener,那么它的onClick()方法就会被执行。

    相关文章

      网友评论

          本文标题:Android View事件分发机制

          本文链接:https://www.haomeiwen.com/subject/uqqyzftx.html