美文网首页
View的事件分发机制以及滑动冲突

View的事件分发机制以及滑动冲突

作者: 人失格 | 来源:发表于2016-10-12 21:05 被阅读186次

    View的事件分发机制以及滑动冲突

    [TOC]

    点击事件的传递规则

    点击时间的分发过程 总是绕不过三个很重要的方法来共同完成:dispatchTouchEvent(MotionEvent ev), onIntercepTouchEvent(MotionEvent ev), onTouchEvent(MotionEvent ev)

    public boolean dispatchTouchEvent(MotionEvent ev)

    ​ 用来进行事件的分发。如果时间能够分发到当前View,那么此方法一定会被调用,返回的结果受View的OntouchEvent和下级View的dispatchEvent方法的影响,表示是否消耗当前事件。

    public boolean onIntercepTouchEvent(MotionEvent ev)

    ​ 只有ViewGroup才会拥有的方法,用于拦截某个事件,如果当前的View拦截某个事件,那么在同一个时间序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

    ​ **public boolean onTouchEvent(MotionEvent ev) **

    ​ 在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接受到事件。

    那么这三个方法的调用顺序是如何呢?

    public boolean dispatchTouchEvent(MotionEvent ev){
      boolean consume = false;
      if(onInterceptTouchEvent(ev)){
            consume = onTouchEvent(ev); 
      }else{
        consume = child.dispatchTouchEvent(ev);
      }
      return consume;
    }
    
    

    对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,此时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,及它的onTouchEvent方法就会被调用,如果它的onInterceptTouchEvent返回为false,表示它不拦截当前事件,此时当前事件就会继续传递给它的子元素,此时如果是View,则会直接调用onTouchEvent方法。

    OnTouchListener, View.onTouchEvent 和OnclickListener的区别

    当一个View需要处理事件。设置了OnTouchListener,则OnTouchListener的onTouch方法会被回调,如果onTouch的方法返回True,则View.onTouchEvent方法不会被调用,反之则会被调用。View.onTouchEvent方法中,如果当前设置的有OnclickListener,其优先级最低。

    这三者的优先级: OnTouchListener -> View.onTouchEvent -> OnclickListener

    当一个点击事件产生后,它的传递过程遵循如下顺序 Activity -> PhoneWindow -> RootView。 由Activity 传给PhoneWindow Window 最后传给顶级View。

    关于时间传递的机制,我们首先在这里给一些结论,

    1. 同一事件顺序是指手指接触屏幕的那一刻起, 到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个时间序列以Down事件开始,中间含有数量不一的move事件。最后以up事件结束。
    2. 正常情况下,一个事件序列只能被一个View拦截且消耗,因此一个时间序列的事件不能分别由两个View同时处理,但是我们可以通过代码控制事件传递。
    3. 某个View一旦决定拦截,那么一个事件序列都只能由它来处理,并且它的onIntercepTouchEvent不会再被调用。当一个View决定拦截一个事件后,同一事件的剩下事件也会交给它来处理,也就是说onIntercepTouchEvent不会被再调用。
    4. 某个View一旦开始处理事件,如果他不消耗ACTION_DOWN事件(OnTouchEvent返回为false),那么同一事件中的其他时间都不会再交给它来处理,并且事件将重新交由它的父元素去处理。
    5. 如果View不消耗除了ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理
    6. ViewGroup默认是不拦截任何事件,默认onInterceptTouchEvent方法默认返回False
    7. View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用
    8. View的OntouchEvent默认都是会消耗事件,除非它是不可以点击的(clickable longclickable同时为false)。 View的longClickable属性都是false,clickable属性要分情况,Button的clickable属性默认为true,TextView的clickable属性默认为false。 当然 如果给view设置了setOnclickListener 或者setOnLongClickListener 会默认开启。
    9. 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子元素,通过requestDisallowInterceptTouchEvent方法可以在子元素中干涉父元素的事件分发过程,但是ACTION_DOWN事件除外。

    事件分发的源码分析

    点击事件是有MotionEvent来表示,当一个点击事件发生,最先传入的是Activity,由Activity的disPatchEvent来进行事件派发,,具体工作是由Activity内部的Window来完成的。 Window会将时间传递给DecorView,一般DecorView就是当前界面的顶级容器(即是setContentView所设置的View的父容器),如图:

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

    Window是如何将事件传递给ViewGroup的呢,Window类其实是一个抽象类 它可以控制顶级view的外观和行为策略,Window的唯一实现是PhoneView类,

    publlic boolean superDisPatchTouchEvent(MotionEvent event){
      return mDecor.superDispatchTouchEvent(event);
    }
    

    这个mDecot其实就是我们getWindow().getDecorView()返回的View,我们通过设置setContentView设置的View就是它的一个子View,自此事件传递到了顶级View,即我们设置的SetContentView所设置的View。

    ViewGroup的事件分发机制

    我们在看一下ViewGroup对点击事件的分发过程,主要实现在disPatchEvent方法中,

    final boolean intercepted
    if(actionMasked == MotionEvent.Action_Down || mFirsrtTouchTarget != null){
      final boolean disallowIntercept = (GroupFlag & FLAG_DISALLOW_INTERCEPT) != 0;
      if(!disallowIntercept){
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action);
      }else{
        intercepted = false;
      } 
    }else{
      intercepted = true;
    }
    

    由ViewGroup的子元素成功处理时, mFirsrtTouchTarget 会被赋值并指向子元素。所以一旦事件是由当前的ViewGroup拦截,接下来同一时序的其他事件都会默认交给ViewGroup来处理。

    另外一个特殊情况就是FLAG_DISALLOW_INTERCEPT标识符,这这个一般通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View,一般设置ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件,因为ViewGroup在事件分发的时候会重置FLAG_DISALLOW_INTERCEPT标识符,这意味着当面对ACTION_DOWN的时候,ViewGroup一定会调用onInterceptTouchEvent来判断自己是否需要拦截事件

                     final ArrayList<View> preorderedList = buildOrderedChildList();
                            final boolean customOrder = preorderedList == null
                                    && isChildrenDrawingOrderEnabled();
                            final View[] children = mChildren;
                            for (int i = childrenCount - 1; i >= 0; i--) {
                                final int childIndex = customOrder
                                        ? getChildDrawingOrder(childrenCount, i) : i;
                                final View child = (preorderedList == null)
                                        ? children[childIndex] : preorderedList.get(childIndex);                                                                           
                                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))                            {                           
                                    mLastTouchDownX = ev.getX();
                                    mLastTouchDownY = ev.getY();
                                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                    alreadyDispatchedToNewTouchTarget = true;
                                    break;
                                }
                            }
    

    首先遍历所有的ViewGroup的所有子元素,判断是否能够接受到点击事件主要由两点来衡量:子元素是否在播动画和点击事件的坐标是否落在子元素的区域内,dispatchTransformedTouchEvent就是调用了child的dispatchTouchEvent方法, 如果子元素的disPatchTouchEvent返回false 则会继续分发给下一个子元素(如果有的话), 在addTouchTarget(child, idBitsToAssign) 给mFirsrtTouchTarget 赋值。mFirsrtTouchTarget 是否为null直接影响ViewGroup对事件的拦截策略。

                dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
                {
                    if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                    } else {
                    handled = child.dispatchTouchEvent(event);
                    }
                }
    

    如果遍历了所有的子元素事件都没有被合适处理,这包含两种情况: 第一种是ViewGroup没有子元素,第二种是子元素处理了点击事件,但是在dispatchTouchEvenr中返回了false 一般是子元素在OnTouchEvent中返回了false,这是ViewGroup会自己处理点击事件。

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

    View的事件分发

             if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
    
                if (!result && onTouchEvent(event)) {
                    result = true;
                }
    

    首先判断是否有设置OnTouchListener,如果onTouchListener中的Touch返回true,那么onTouchEvent就不会被调用。

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

    只要View的CLICKABLE或者是LONG_CLICKABLE 那么他就会消耗这件事,当ACTION_UP事件发生 会触发performClick() 如果设置了OnclickListener ,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;
            }
        }
    

    设置Viewd的OnclickListener和OnLongClickListener()会自动将View的Clickable,LongClickAble为true。

        public void setOnClickListener(@Nullable OnClickListener l) {
            if (!isClickable()) {
                setClickable(true);
            }
            getListenerInfo().mOnClickListener = l;
        }
        
         public void setOnLongClickListener(@Nullable OnLongClickListener l) {
            if (!isLongClickable()) {
                setLongClickable(true);
            }
            getListenerInfo().mOnLongClickListener = l;
        }
    

    View的滑动冲突

    常见的滑动冲突场景

    • 场景1 ------ 外部滑动方向和内部滑动方向不一致
    • 场景2------- 外部滑动方向和内部滑动方向一致
    • 场景3------- 上面两种情况的嵌套

    对应处理的方法:

    1. 当用户左右滑动时,需要外部拦截点击事件,当用户需要上下滑动时,需要让内部拦截点击事件
    2. 这种场景无法根据滑动的角度, 距离差已经速度差来做判断 一般需要从业务上找到突破口
    3. 同上,一般也是从业务的需要上得出相应的处理规则

    两种滑动冲突的解决方案:

    1. 外部拦截法

      所谓的外部拦截法就是所有的点击事件都先经过父容器的拦截处理,如果父容器需要此事件则拦截 如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制,外部拦截法需要重写父容器的onInterceptTouchEvent方法。伪代码如下

       
          @Override
          public boolean onInterceptTouchEvent(MotionEvent ev) {
              int action = ev.getAction();
              float y = ev.getY();
      
              switch (action) {
                  case MotionEvent.ACTION_DOWN:
                      mLastY = y;
                      break;
                  case MotionEvent.ACTION_MOVE:
                      float dy = y - mLastY;
                     if(父容器需要当前点击事件){
                       return true;
                     }
                      break;
              }
             // 默认返回的都是false 
              return super.onInterceptTouchEvent(ev);
          }
      
      1. 内部拦截法

        内部拦截法主要是父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要此事件就直接消耗,否则交由父容器进行处理,需要配合requestDisallowInterceptTouchEvent()才能正常工作,一般内部拦截法比较的复杂,它的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:

        
          @Override
            public boolean dispatchTouchEvent(MotionEvent ev) {
                int action = ev.getAction();
                float y = ev.getY();
        
                switch (action) {
                    case MotionEvent.ACTION_DOWN:
                      parent.requestDisallowInterceptTouchEvent(true)
                        break;
                    case MotionEvent.ACTION_MOVE:              
                       if(父容器需要当前点击事件){
                         return parent.requestDisallowInterceptTouchEvent(false);
                       }
                        break;
                }
               // 默认返回的都是false 
                return super.dispatchTouchEvent(ev);
            }
        
        

        面对不同的滑动策越的时候只需要修改ACTION_MOVE事件即可,其他不需要动也不能改动。除了子元素需要处理外,父元素也要默认拦截除了ACTION_DOWN以外的所有其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)时,父元素才能继续拦截所需要的事件。

    相关文章

      网友评论

          本文标题:View的事件分发机制以及滑动冲突

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