乐趣区

关于android:高级-UI-成长之路-二-深入理解-Android-80-View-触摸事件分发机制

前言

在上一篇文章中咱们介绍了 View 的基础知识以及 View 滑动的实现,本篇将为大家带来 View 的一个外围知识点 事件散发机制。事件散发机制不仅仅是外围知识点也是 Android 中的一个难点,上面咱们就从源码的角度来剖析事件的传递还有最初是如何解决滑动抵触的。

事件散发机制

点击事件的传递规定

在介绍事件传递规定之前,首先咱们要明确要剖析的对象就是 MotionEvent , 对于 MotionEvent 在上一篇文章介绍滑动的时候咱们曾经用过了。其实所谓的点击事件散发就是对 MotionEvent 事件的散发过程,点击事件的散发过程由三个很重要的办法共同完成,如下:

1. dispatchTouchEvent(MotionEvent ev)

用来进行事件的散发。如果事件可能传递给以后的 View,那么此办法肯定会被调用,返回后果受以后 View 的 onTouchEvent 和上级 View 的 dispatchTouchEvent 办法的影响,示意是否耗费以后事件。

2. onInterceptTouchEvent(MotionEvent ev)

在上述外部办法调用,用来判断是否拦挡某个事件,如果以后 View 拦挡了某个事件,那么在同一个事件序列中,此办法不会被再次调用,返回后果示意是否拦挡以后事件。

3. onTouchEvent(MotionEvent ev)

在第一个办法中调用,用来解决点击事件,返回后果示意是否耗费此事件,如果不耗费,以后 View 就无奈再次接管到事件。

上面我画了一个图来具体阐明下下面 3 个办法之间的关系

也能够用一段伪代码来阐明,如下:

fun dispatchTouchEvent(MotionEvent ev):Boolean{
  var consume = false
  // 父类是否拦挡
  if(onInterceptTouchEvent(ev)){
    // 如果拦挡将执行本身的 onTouchEvent 办法
    consume = onTouchEvent(ev)
  }else{
    // 如果事件在父类不拦挡,将持续分发给子类
    consume = child.dispatchTouchEvent(ev)
  }
  reture consume
}

上图跟伪代码意思一样,特地是伪代码曾经将它们三者的关系体现得十分到位,通过下面伪代码咱们能够大抵理解到点击事件的一个传递规定,对应一个根 ViewGroup 来说,点击事件产生后,首先会传递给它, 这时它的 dispatchTouchEvent 就会被调用,如果这个 ViewGroup 的 onInterceptTouchEvent 办法返回 true 就示意它要拦挡以后事件,接着事件就会交给这个 ViewGroup 解决,即它的 onTouchEvent 办法就会被调用;如果这个 ViewGroup 的 onInterceptTouchEvent 返回 false 就示意它不拦挡以后事件,这时以后事件就会传递给它的子元素,接着子元素的 dispatchTouchEvent 办法就会被调用,如此重复直到事件被最终解决。

当一个 View 须要处理事件时的调用规定, 如下伪代码:

fun  dispatchTouchEvent(MotionEvent event): boolean{
  //1. 如果以后 View 设置 onTouchListener
if(onTouchListener != null){
  //2. 那么本身的 onTouch 就会被调用,如果返回 false 其本身的 onTouchEvent 被调用
  if(!onTouchListener.onTouch(v: View?, event: MotionEvent?)){
       //3. onTouch 返回了 false,onTouchEvent 调用, 并且会调用外部的 onClick 事件
    if(!onTouchEvent(event)){
        //4. 如果也设置了 onClickListener 那么 onClick 也会被调用
       onClickListener.onClick()}
  }
 }
}

下面的伪代码的逻辑总结一下就是如果以后 View 设置了 onTouchListener 那么本身的 onTouch 就会执行,如果 onTouch 返回值是 false,其本身的 onTouchEvent 会被调用。如果 onTouchEvent 返回也为 false 那么 onClick 就会执行。优先级为 onTouch > onTouchEvent > onClick。

当一个点击事件产生后,它的传递过程遵循如下程序:Activity -> Window -> View , 即事件总是先传递给 Activity , Activity 再传递给 Window , 最初 Window 再传递给顶级 View。顶级 View 接管到事件后,就会依照事件散发机制去散发事件。思考一种状况,如果一个 View 的 onTouchEvent 返回 false,那么它的父容器的 onTouchEvent 将会被调用,顺次类推。如果所有的元素都不解决这个事件,那么这个事件将会最终传递给 Activity 解决,即 Activity 的 onTouchEvent 办法会被调用。上面咱们就以一段代码示例来演示一下这种场景,代码如下:

  1. 重写 Activity dispatchTouchEvent 散发和 onTouchEvent 事件处理

    class MainActivity : AppCompatActivity() {override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {if (ev.action == MotionEvent.ACTION_DOWN)
            println("事件散发机制开始散发 ----> Activity  dispatchTouchEvent")
            return super.dispatchTouchEvent(ev)
        }
    
    
        override fun onTouchEvent(event: MotionEvent?): Boolean {if (event.action == MotionEvent.ACTION_DOWN)
            println("事件散发机制解决 ----> Activity onTouchEvent 执行")
            return super.onTouchEvent(event)
        }
    }  
  2. 重写根 ViewGroup dispatchTouchEvent 散发和 onTouchEvent 事件处理

    public class GustomLIn(context: Context?, attrs: AttributeSet?) : LinearLayout(context, attrs) {override fun onTouchEvent(event: MotionEvent?): Boolean {if (event.action == MotionEvent.ACTION_DOWN)
            println("事件散发机制解决 ----> 父容器 LinearLayout onTouchEvent")
            return false
        }
    
        override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {if (ev.action == MotionEvent.ACTION_DOWN)
            println("事件散发机制开始散发 ----> 父容器  dispatchTouchEvent")
            return super.dispatchTouchEvent(ev)
        }
    
        override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {if (ev.action == MotionEvent.ACTION_DOWN)
            println("事件散发机制开始散发 ----> 父容器是否拦挡  onInterceptTouchEvent")
            return super.onInterceptTouchEvent(ev)
        }
    }    
  3. 重写子 View dispatchTouchEvent 散发和 onTouchEvent 事件处理

    public class Button(context: Context?, attrs: AttributeSet?) : AppCompatButton(context, attrs) {override fun dispatchTouchEvent(event: MotionEvent?): Boolean {if (event.action == MotionEvent.ACTION_DOWN)
            println("事件散发机制开始散发 ----> 子 View  dispatchTouchEvent")
            return super.dispatchTouchEvent(event)
        }
    
        override fun onTouchEvent(event: MotionEvent?): Boolean {if (event.action == MotionEvent.ACTION_DOWN)
            println("事件散发机制解决 ----> 子 View onTouchEvent")
            return false
        }
    
    } 

输入:

System.out: 事件散发机制开始散发 ----> Activity       dispatchTouchEvent
System.out: 事件散发机制开始散发 ----> 父容器          dispatchTouchEvent
System.out: 事件散发机制开始散发 ----> 父容器是否拦挡    onInterceptTouchEvent
System.out: 事件散发机制开始散发 ----> 子 View          dispatchTouchEvent
System.out: 事件散发机制开始解决 ----> 子 View         onTouchEvent
System.out: 事件散发机制开始解决 ----> 父容器         LinearLayout onTouchEvent
System.out: 事件散发机制开始解决 ----> Activity         onTouchEvent 执行

得出的论断跟之前的形容完全一致,这就阐明了如果子 View,父 ViewGroup 都不处理事件的话,最初交于 Activity 的 onTouchEvent 办法。也能够从下面的后果看进去事件传递是由内向内传递的,即事件总是先传递给父元素,而后再由父元素分发给子 View。

事件散发源码解析

上一大节咱们剖析了 View 的事件散发机制,本节将从源码的角度进一步去剖析。

  1. Activity 对点击事件的散发过程

    点击事件用 MotionEvent 来示意,当一个点击操作产生时,事件最先传递给以后 Activity,由 Activity 的 dispatchTouchEvent 来进行事件派发,具体的工作是由 Activity 外部的 Window 来实现的。Window 会将事件传递给 DecorView ,DecorView 个别就是以后界面的底层容器也就是 setContentView 所设置的父容器,它继承自 FrameLayout,它在 Activity 中能够通过 getWindow().getDecorView() 取得,因为事件最先由 Activity 开始进行散发,那么咱们就间接看它的 dispactchTouchEvent 办法,代码如下:

    //Activity.java
        public boolean dispatchTouchEvent(MotionEvent ev) {
            /**
             * 首先按下的触发的是 ACTION_DOWN 事件
             */
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {onUserInteraction();
            }
            /**
             * 拿到以后 Window 调用 superDispatchTouchEvent 办法
             */
            if (getWindow().superDispatchTouchEvent(ev)) {return true;}
            /**
             * 如果所有的 View 都没有解决,那么最终会执行到 Activity onTouchEvent 办法中。*/
            return onTouchEvent(ev);
        }

    通过下面的代码咱们晓得首先执行的是 ACTION\_DOWN 按下事件执行 onUserInteraction 空办法,而后调用 getWindow() 的 superDispatchTouchEvent 办法,这里的 getWindow 其实就是它的惟一子类 PhoneWindow 咱们看它的具体调用实现,代码如下:

    //PhoneWindow.java
    public class PhoneWindow extends Window implements MenuBuilder.Callback {
      ...
            private DecorView mDecor;    
        @Override
        public boolean superDispatchTouchEvent(MotionEvent event) {return mDecor.superDispatchTouchEvent(event);
        }
      ...
    }

    在 PhoneWindow 的 superDispatchTouchEvent 函数中又交于了 DecorView 来解决,那么 DecorView 是什么呢?

    //DecorView.java
    
    public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
      ...
            DecorView(Context context, int featureId, PhoneWindow window,
                WindowManager.LayoutParams params) {super(context);
       ...
         
             @Override
        public final View getDecorView() {if (mDecor == null || mForceDecorInstall) {installDecor();
            }
            return mDecor;
        }
        }
      ...
        
    }

    咱们看到 DecorView 它其实就是继承的 FrameLayout,咱们晓得在 Activity 中咱们能够通过 getWindow().getDecorView().findViewById() 拿到对应在 XML 中的 View 对象 , 那么 DecorView 又是什么时候进行实例化呢?还有 PhoneWindow 又是何时进行实例化的呢?因为这些不是咱们明天解说的次要内容,感兴趣的能够看我之前对 Activity 启动源码剖析该篇中有讲过 它们其实都是在 Activity 启动的时候进行各自的实例化。好了,DecorView 实例化就讲到这里。目前事件传递到了 DecorView 这里,咱们看它的外部源码实现,代码如下:

      //  DecorView.java
    public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {public boolean superDispatchTouchEvent(MotionEvent event) {return super.dispatchTouchEvent(event);
        }

    咱们看到外部又调用了父类 dispatchTouchEvent 办法,所以最终是交给 ViewGroup 顶级 View 来解决散发了。

  2. 顶级 View 对点击事件的散发过程

    在上一大节中咱们晓得了一个事件的传递流程,这里咱们就大抵在回顾一下。首先点击事件达到顶级 ViewGroup 之后,会调用本身的 dispatchTouchEvent 办法,之后如果本身的拦挡办法 onInterceptTouchEvent 返回 true,则事件不会持续下发给子类,如果本身设置了 mOnTouchListener 监听,则 onTouch 会被调用,否则 onTouchEvent 会被调用,如果 onTouchEvent 中设置了 mOnClickListener 那么 onClick 会调用。如果 ViewGroup 的 onInterceptTouchEvent 返回 false, 则事件会传递到所点击的子 View 中,这时子 View 的 dispatchTouchEvent 会被调用。到此为止,事件曾经从顶级 View 传递给了下一层 View , 接下来的传递过程和顶级 ViewGroup 一样,如此循环就实现了整个事件的散发。

    在该大节的第一点中咱们晓得,在 DecorView 中的 superDispatchTouchEvent 办法外部调用了父类的 dispatchTouchEvent 办法,咱们看它的实现,代码如下:

    //ViewGroup.java
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
         ...
    
                if (actionMasked == MotionEvent.ACTION_DOWN) {
                    // 这里次要是在新事件开始时解决完上一个事件
                    cancelAndClearTouchTargets(ev);
                    resetTouchState();}
           
                /** 查看事件拦挡, 示意事件是否拦挡 */
                final boolean intercepted;
                /**
                 * 1. 判断以后是否是按下
                 */
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || mFirstTouchTarget != null) {final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                  //2. 子类能够通过 requestDisallowInterceptTouchEvent 办法来设置父类不要拦挡
                    if (!disallowIntercept) {
                      //3
                        intercepted = onInterceptTouchEvent(ev);
                        // 复原事件避免其扭转
                        ev.setAction(action); 
                    } else {intercepted = false;}
                } else {intercepted = true;}
         ...
        }

    从下面代码咱们能够看出如果 actionMasked == MotionEvent.ACTION\_DOWN 或者 mFirstTouchTarget != null 成立的话会执行正文 2 的判断(mFirstTouchTarget 的意思如果以后事件被子类生产了,就不成立,前面会进步),disallowIntercept 能够在子类中通过调用父类的 requestDisallowInterceptTouchEvent(true) 申请父类不要拦挡散发事件,也就是阻止执行正文 3 的拦挡子类接管按下的事件,反之执行 onInterceptTouchEvent(ev); 如果返回 true 阐明拦挡了事件。

    下面介绍了正文 1,2,3 onInterceptTouchEvent 返回 true 的状况,阐明拦挡了事件,上面咱们来解说 intercepted = false 以后 ViewGroup 不拦挡事件的时候,事件会下发给它的子 View 进行解决,上面看子 View 解决的源码,代码如下:

    //ViewGroup.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
     ... 
       
       if (!canceled && !intercepted) {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 (childWithAccessibilityFocus != null) {if (childWithAccessibilityFocus != child) {continue;}
                                    childWithAccessibilityFocus = null;
                                    i = childrenCount - 1;
                                }
    
                                if (!canViewReceivePointerEvents(child)
                                        || !isTransformedTouchPointInView(x, y, child, null)) {ev.setTargetAccessibilityFocus(false);
                                    continue;
                                }
    
                                newTouchTarget = getTouchTarget(child);
                                if (newTouchTarget != null) {
                                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                                    break;
                                }
    
                                resetCancelNextUpFlag(child);
                                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {mLastTouchDownTime = ev.getDownTime();
                                    if (preorderedList != null) {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);
                                    alreadyDispatchedToNewTouchTarget = true;
                                    break;
                                }
                                ev.setTargetAccessibilityFocus(false);
                            } 
         
       }
      
     ...
    } 

    下面这段代码也很好了解,首先遍历 ViewGroup 子孩子,而后判断子元素是否在播放动画和点击事件是否落在了子元素的区域内。如果某个子元素满足这 2 个条件,那么事件就会传递给该子类来解决,能够看到,dispatchTransformedTouchEvent 实际上调用的就是子类的 dispatchTouchEvent 办法,在它的外部有如下一段内容,而在下面的代码中 child 传递不是 null , 因而它会间接调用子元素的 dispatchTouchEvent 办法,这样事件就交由子元素解决,从而实现了一轮事件散发。

    //ViewGroup.java
        private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
         ...
          if (child == null) {handled = super.dispatchTouchEvent(event);
                } else {handled = child.dispatchTouchEvent(event);
                }
           
         ...
          
          
        }

    这里如果 child.dispatchTouchEvent(event) 返回 true , 那么 mFirstTouchTarget 就会被赋值同时跳出 for 循环,如下所示:

    //ViewGroup.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
      ...
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
      ...
    }
    
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
            target.next = mFirstTouchTarget;
                  // 这个时候 mFirstTouchTarget 就代表子 View 胜利解决了事件
            mFirstTouchTarget = target;
            return target;
    }

    这几行代码实现了 mFirstTouchTarget 的赋值并终止了对子元素的遍历。如果子元素的 dispatchTouchEvent 返回 false,ViewGroup 会持续遍历进行事件分发给下一个子元素。

    如果遍历所有的子元素后事件都没有被解决的时候,那么 ViewGroup 就会本人解决点击事件,这里蕴含 2 种状况下 ViewGroup 会本人处理事件 (其一: ViewGroup 没有子元素,其二:子元素解决了点击事件,然而在 dispatchTouchEvent 中返回了 false, 这个别是在子元素的 onTouchEvent 中返回了 false)

    代码如下:

    public boolean dispatchTouchEvent(MotionEvent ev) {
      ...
       if (mFirstTouchTarget == null) {
                   
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                            TouchTarget.ALL_POINTER_IDS);
         }
      ...
        
        
    }
    

    能够看到如果 mFirstTouchTarget == null 的时候,那么就是代表 ViewGroup 的子 View 没有被生产点击事件,将调用本身的 dispatchTransformedTouchEvent 办法。留神下面这段代码这里的第三个参数 child 为 null,从后面的剖析能够晓得,它会调用 super.dispatchTouchEvent(event),显然,这里就会调用父类 View 的 dispatchTouchEvent 办法,即点击事件开始交由 View 解决,请看上面的剖析:

  3. View 对点击事件的处理过程

    其实 View 对点击事件的处理过程略微简略一些,留神这里的 View 不蕴含 ViewGroup。先看它的 dispatchTouchEvent 办法,代码如下:

    //View.java
        public boolean dispatchTouchEvent(MotionEvent event) {
            ...
    
            if (onFilterTouchEventForSecurity(event)) {if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {result = true;}
    
                ListenerInfo li = mListenerInfo;
              //1. 
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {result = true;}
                        //2. 
                if (!result && onTouchEvent(event)) {result = true;}
            }
    
            ....
    
            return result;
        }

    View 中的事件处理逻辑比较简单,咱们先看正文 1 处,如果咱们内部设置了 mOnTouchListener 点击事件,那么就会执行 onTouch 回调,如果该回调的返回值为 false,那么才会执行 onTouchEvent 办法,可见 onTouchListener 优先级高于 onTouchEvent 办法,上面咱们来剖析 onTouchEvent 办法实现,代码如下:

    //View.java
     public boolean onTouchEvent(MotionEvent event) {final float x = event.getX();
            final float y = event.getY();
            final int viewFlags = mViewFlags;
            final int action = event.getAction();
    
            final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
            /**
             * 1. View 处于不可用状态下的点击事件的处理过程
             */
            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;
            }
    
            /**
             * 2. 如果 View 设置了代理,那么还会执行 TouchDelegate 的 onTouchEvent 办法。*/
            if (mTouchDelegate != null) {if (mTouchDelegate.onTouchEvent(event)) {return true;}
            }
    
            /**
             * 3. 如果  clickable 或 (viewFlags & TOOLTIP) == TOOLTIP 有一个成立那么就会解决该事件
             */
               if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {switch (action) {
                    case MotionEvent.ACTION_UP:
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        if ((viewFlags & TOOLTIP) == TOOLTIP) {handleTooltipUp();
                        }
                        if (!clickable) {removeTapCallback();
                            removeLongPressCallback();
                            mInContextButtonPress = false;
                            mHasPerformedLongPress = false;
                            mIgnoreNextUpEvent = false;
                            break;
                        }
                        // 用于辨认疾速按下
                        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
    
                            boolean focusTaken = false;
                            if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {focusTaken = requestFocus();
                            }
    
                            if (prepressed) {setPressed(true, x, y);
                            }
    
                            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {removeLongPressCallback();
                                if (!focusTaken) {if (mPerformClick == null) {mPerformClick = new PerformClick();
                                    }
                                    /**
                                     * 如果设置了点击事件 mOnClickListener 就会执行外部回调
                                     */
                                    if (!post(mPerformClick)) {performClick();
                                    }
                                }
                            }
    
                            if (mUnsetPressedState == null) {mUnsetPressedState = new UnsetPressedState();
                            }
    
                            if (prepressed) {
                                postDelayed(mUnsetPressedState,
                                        ViewConfiguration.getPressedStateDuration());
                            } else if (!post(mUnsetPressedState)) {
                                // If the post failed, unpress right now
                                mUnsetPressedState.run();}
    
                            removeTapCallback();}
                        mIgnoreNextUpEvent = false;
                        break;
    
                    case MotionEvent.ACTION_DOWN:
                       ...
                        // 判断是否是在滚动容器中
                        boolean isInScrollingContainer = isInScrollingContainer();
                        if (isInScrollingContainer) {
                            mPrivateFlags |= PFLAG_PREPRESSED;
                            if (mPendingCheckForTap == null) {mPendingCheckForTap = new CheckForTap();
                            }
                            mPendingCheckForTap.x = event.getX();
                            mPendingCheckForTap.y = event.getY();
                              // 发送一个提早执行长按事件的操作
                            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                        } else {
                            // Not inside a scrolling container, so show the feedback right away
                            setPressed(true, x, y);
                            checkForLongClick(0, x, y);
                        }
                        break;
    
                    case MotionEvent.ACTION_CANCEL:
                        if (clickable) {setPressed(false);
                        }
                    // 移除一些回调比方长按事件
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                        if (clickable) {drawableHotspotChanged(x, y);
                        }
                        if (!pointInView(x, y, mTouchSlop)) {
                              // 移除一些回调比方长按事件
                            removeTapCallback();
                            removeLongPressCallback();
                            if ((mPrivateFlags & PFLAG_PRESSED) != 0) {setPressed(false);
                            }
                            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        }
                        break;
                }
    
                return true;
            }
    
            return false;
        } 

    下面代码尽管比拟多,然而逻辑还是很分明的,咱们来剖析一下

    1. 判断 View 是否处于不可用的状态下应用,返回一个 clickable。
    2. 判断 View 是否设置了代理,如果设置了代理将会执行 代理的 onTouchEvent 办法。
    3. 如果 clickable 或 (viewFlags & TOOLTIP) == TOOLTIP 有一个成立那么就会解决 MotionEvent 事件。
    4. 在 MotionEvent 事件中别离会在 up 和 down 中会执行点击 onClick 和 onLongClick 回调。

    到这里点击事件的散发机制源码实现曾经剖析完了,联合之前剖析的传递规定和上面这张图,而后联合源码置信你应该了解了事件散发跟事件处理机制了。

![image](https://upload-images.jianshu.io/upload_images/27242968-9bfc508e6f06b847.image?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)



滑动抵触

本大节将介绍 View 体系中一个十分重要的知识点 滑动抵触,置信在开发中特地是做一些滑动成果解决的时候而且还不止一层滑动,又的是嵌套好几层的滑动,那么它们之间如果不解决滑动抵触必然是不可行的,上面咱们先来看看造成滑动抵触的场景。

滑动抵触场景及解决规定

1. 内部滑动方向和外部滑动方向不统一

次要是将 ViewPager 和 Fragment 配合应用所组成的页面滑动成果,支流利用简直都会应用这个成果。在这种成果中,能够通过左右滑动来切换页面,而每个页面外部往往是一个 RecyclerView。原本这种状况下是有滑动抵触的,然而 ViewPager 外部解决了这种滑动抵触,因而采纳 ViewPager 时咱们毋庸关注这个问题,然而如果咱们采纳的是 ScrollView 等滑动控件,那就必须手动解决滑动抵触了,否则造成的结果就是内外两层只能由一层可能滑动,这是因为两者之间的滑动事件有抵触。

它的解决规定是:

当用户左右滑动时,须要让内部的 View 拦挡点击事件,当用户高低滑动的时候,须要让外部的 View 拦挡点击事件。这个时候咱们就能够依据他们的特色来解决滑动抵触。具体来说就是能够通过判断滑动手势是程度方向还是竖直方向具体来对应拦挡事件。

2. 内部滑动方向和外部滑动方向统一

这种状况就略微简单一些,当内外两层都在同一个方向能够滑动的时候,显然存在逻辑问题。因为当手指开始滑动的时候,零碎无奈晓得用户到底是想让那一层滑动,所以当手指滑动的时候就会呈现问题,要么只有一层能滑动,要么就是内外两层都滑动得很卡顿。在理论的开发中,这种场景次要是指内外两层同时能高低滑动或者内外两层同时能左右滑动。

它的解决规定是:

这种事比拟非凡的,因为它无奈依据滑动的角度、间隔差以及速度差来做判断,然而这个时候个别都能在业务上找到突破点,比方业务有规定,当解决某种状态的时候须要内部 View 响应用户的滑动,而处于另外一种状态时则须要外部 View 来响应 View 的滑动,依据这种业务上的需要咱们也能得出相应的解决规定,有了解决规定同样能够进行下一步解决。这种场景通过文字描述可能比拟形象,在下一大节中咱们会通过理论例子来演示这种状况。

3. 1 + 2 场景的嵌套

场景三是场景一和场景二两种状况的嵌套,因而场景三的滑动抵触看起来就更加简单了。比方在许多利用中会有这么一个成果:内层有一个场景 1 中的滑动成果,而后外层又有一个场景 2 中的滑动成果。尽管说场景三的滑动抵触看起来是比较复杂的,然而它是几个繁多的滑动抵触的叠加,所以只须要别离解决内中外层之间的抵触就行了,解决形式跟场景 1 和 2 统一。

上面咱们就来看一下滑动抵触的解决规定。

它的解决规定是:

它的滑动规定就更简单了,和场景 2 一样,它也无奈间接依据滑动的角度、间隔以及速度差来做判断,同样还是只能从业务员上找到突破点,具体方法和场景 2 一样,都是从业务的需要上得出相应的解决规定,在下一节中同样会给出代码示例来进行演示。

滑动抵触的解决形式

下面说过针对场景 1 中的滑动,咱们能够依据滑动的间隔差来进行判断,这个间隔差就是所谓的滑动规定。如果用 ViewPager 去实现场景 1 中的成果,咱们不须要手动解决滑动抵触,因为 ViewPager 曾经帮咱们做了,然而这里为了更好的演示滑动抵触解决思维,没有采纳 ViewPager。其实在滑动过程中失去滑动的角度这个是相当简略的,然而到底要怎么做能力将点击事件交给适合的 View 去解决呢?这时就要用到 3.4 节所讲述的事件散发机制了。针对滑动抵触,这里给出 2 种解决滑动抵触的形式,内部拦挡和外部拦挡发。

  1. 内部拦截法

    所谓内部拦挡就是指点击事件先通过父容器的拦挡解决,如果父容器须要此事件就拦挡,如果不须要此事件就不拦挡,这样就能够解决滑动抵触的问题,这种办法比拟合乎点击事件的散发机制。内部拦截法须要重写 onInterceptTouchEvent 办法,在外部做响应的拦挡即可,能够参考上面代码:

        override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {when (ev.action) {
                MotionEvent.ACTION_DOWN -> {isIntercepted = false}
                MotionEvent.ACTION_MOVE -> {
                    // 拦挡子类的挪动事件
                    if (true) {println("事件散发机制开始散发 ----> 拦挡子类的挪动事件  onInterceptTouchEvent")
                        isIntercepted = true
                    } else {isIntercepted = false}
    
                }
                MotionEvent.ACTION_UP -> {isIntercepted = false}
            }
            return isIntercepted
        }

    上述代码是内部拦挡的典型逻辑,针对不同的滑动抵触只须要批改父容器须要以后点击事件这个条件即可,其它均不做批改也不能批改。这里对上述代码再形容一下,在 onInterceptTouchEvent 办法中,首先是 ACTION\_DOWN 这个事件,父容器必须返回 false。既不拦挡 ACTION\_DOWN 事件,这是因为一旦父容器拦挡了 ACTION\_DOWN , 这是因为一旦父容器拦挡 ACTION\_DOWN, 那么后续的 ACTION\_DOWN, 那么后续的 ACTION\_MOVE 和 ACTION\_UP 事件都会间接交由父容器解决,这个时候事件没法再传递给子元素了;其次是 ACTION\_MOVE 事件,这个事件能够依据须要来决定是否拦挡,如果是 ACTION\_UP 事件,这里必须要返回 false , 因为 ACTION\_UP 事件自身没有太多意义。

    思考一种状况,假如事件交由子元素解决,如果父容器在 ACTION\_UP 时返回了 true,就会导致子元素无奈接管到 ACTION\_UP 事件,这个时候子元素中的 onClick 事件就无奈触发,然而父容器比拟非凡,一旦它开始拦挡任何一个事件,那么后续的事件都会交给它来解决,而 ACTION\_UP 作为最初一个事件也必然能够传递给父容器,即使父容器的 onInterceptTouchEvent 办法在 ACTION\_UP 时返回了 false.

  2. 外部拦截法

    外部拦截法是指父容器不拦挡任何事件,所有的事件都传递给子元素,如果子元素须要此事件就间接消耗掉,否则就交由父容器进行解决,这种办法和 Android 中的事件散发机制不统一,在解说源码的时候,咱们解说了,能够通过 requestDisalloWInterceptTouchEvent 办法能力失常工作,应用起来较内部拦截法稍显简单,咱们须要重写子元素的 dispatchTouchEvent 办法

        override fun dispatchTouchEvent(event: MotionEvent): Boolean {when (event.action) {
                MotionEvent.ACTION_DOWN -> {println("事件散发机制开始散发 ----> 子 View  dispatchTouchEvent ACTION_DOWN")
                    parent.requestDisallowInterceptTouchEvent(true)
                }
                MotionEvent.ACTION_MOVE -> {println("事件散发机制开始散发 ----> 子 View  dispatchTouchEvent ACTION_MOVE")
                    if (true){parent.requestDisallowInterceptTouchEvent(false)
                    }
                }
                MotionEvent.ACTION_UP -> {println("事件散发机制开始散发 ----> 子 View  dispatchTouchEvent ACTION_UP")
                }
            }
            return super.dispatchTouchEvent(event)
        } 

    上述代码是外部拦截法的典型代码,当面对不同的滑动策略时只须要批改外面的条件即可,其它不须要做改变而且也不能有改变,除了子元素须要做解决以外,父元素也要默认拦挡除了 ACTION\_DOWN 以外的其它事件,这样当子元素调用 parent.requestDisallowInterceptTouchEvent(false),父元素能力持续拦挡所需的事件。

    上面就以实战的 demo 具体来说明一下。

实战

场景一 滑动抵触案例

咱们自定义一个 ViewPager + RecyclerView 蕴含左右 + 高低滑动,这样就满足了咱们场景一的滑动抵触,咱们先来看一下残缺的效果图:

[图片上传失败 …(image-1d2523-1636703567722)]

下面录屏的成果解决了高低滑动跟左右滑动抵触,实现形式就是自定义 ViewGroup 利用 Scroller 达到像 ViewPager 一样丝滑般的感觉,而后外部增加了 3 个 RecyclerView。

咱们看一下自定义 ViewGroup 实现:

class ScrollerViewPager(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
    /**
     * 定义 Scroller 实例
     */
    private var mScroller = Scroller(context)

    /**
     * 判断拖动的最小挪动像素点
     */
    private var mTouchSlop = 0

    /**
     * 手指按下屏幕的 x 坐标
     */
    private var mDownX = 0f

    /**
     * 手指以后所在的坐标
     */
    private var mMoveX = 0f

    /**
     * 记录上一次触发 按下是的坐标
     */
    private var mLastMoveX = 0f

    /**
     * 界面能够滚动的左边界
     */
    private var mLeftBorder = 0

    /**
     * 界面能够滚动的右边界
     */
    private var mRightBorder = 0

    /**
     * 记录下一次拦挡的 X,y
     */
    private var mLastXIntercept = 0
    private var mLastYIntercept = 0

    /**
     * 是否拦挡
     */
    private var interceptor = false

    init {init()
    }

    constructor(context: Context?) : this(context, null) { }


    private fun init() {
        /**
         * 通过 ViewConfiguration 拿到认为手指滑动的最短的挪动 px 值
         */
        mTouchSlop = ViewConfiguration.get(context).scaledPagingTouchSlop


    }


    /**
     * 测量 child 宽高
     */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        // 拿到子 View 个数
        val childCount = childCount
        for (index in 0..childCount - 1) {val childView = getChildAt(index)
            // 为 ScrollerViewPager 中的每一个子控件测量大小
            measureChild(childView, widthMeasureSpec, heightMeasureSpec)

        }
    }

    /**
     * 测量完之后,拿到 child 的大小而后开始对号入座
     */
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {if (changed) {
            val childCount = childCount
            for (child in 0..childCount - 1) {
                // 拿到子 View
                val childView = getChildAt(child)
                // 开始对号入座
                childView.layout(
                    child * childView.measuredWidth, 0,
                    (child + 1) * childView.measuredWidth, childView.measuredHeight
                )
            }
            // 初始化左右边界
            mLeftBorder = getChildAt(0).left
            mRightBorder = getChildAt(childCount - 1).right

        }

    }


    /**
     * 内部解决 1.  依据垂直或程度的间隔来判断
     */
//    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
//         interceptor = false
//        var x = ev.x.toInt()
//        var y = ev.y.toInt()
//        when (ev.action) {
//            MotionEvent.ACTION_DOWN -> {
//                interceptor = false
//            }
//            MotionEvent.ACTION_MOVE -> {
//                var deltaX = x - mLastXIntercept
//                var deltaY = y - mLastYIntercept
//                interceptor = Math.abs(deltaX) > Math.abs(deltaY)
//                if (interceptor) {//                    mMoveX = ev.getRawX()
//                    mLastMoveX = mMoveX
//                }
//            }
//            MotionEvent.ACTION_UP -> {
//                // 拿到以后挪动的 x 坐标
//                interceptor = false
//                println("onInterceptTouchEvent---ACTION_UP")
//
//            }
//        }
//        mLastXIntercept = x
//        mLastYIntercept = y
//        return interceptor
//    }

    /**
     * 内部解决 2.  依据第二点坐标 - 第一点坐标 如果差值大于 TouchSlop 就认为是在左右滑动
     */
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        interceptor = false
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                // 拿到手指按下相当于屏幕的坐标
                mDownX = ev.getRawX()
                mLastMoveX = mDownX
                interceptor = false
            }
            MotionEvent.ACTION_MOVE -> {
                // 拿到以后挪动的 x 坐标
                mMoveX = ev.getRawX()
                // 拿到差值
                val absDiff = Math.abs(mMoveX - mDownX)
                mLastMoveX = mMoveX
                // 当手指拖动值大于 TouchSlop 值时,就认为是在滑动,拦挡子控件的触摸事件
                if (absDiff > mTouchSlop)
                    interceptor =   true
            }
        }
        return interceptor
    }


    /**
     * 父容器没有拦挡事件,这里就会接管到用户的触摸事件
     */
    override fun onTouchEvent(event: MotionEvent): Boolean {when (event.action) {
            MotionEvent.ACTION_MOVE -> {println("onInterceptTouchEvent---onTouchEvent--ACTION_MOVE")
                mLastMoveX = mMoveX
                // 拿到以后滑动的绝对于屏幕左上角的坐标
                mMoveX = event.getRawX()
                var scrolledX = (mLastMoveX - mMoveX).toInt()
                if (scrollX + scrolledX < mLeftBorder) {scrollTo(mLeftBorder, 0)
                    return true
                } else if (scrollX + width + scrolledX > mRightBorder) {scrollTo(mRightBorder - width, 0)
                    return true

                }
                scrollBy(scrolledX, 0)
                mLastMoveX = mMoveX
            }
            MotionEvent.ACTION_UP -> {
                // 当手指抬起是,依据以后滚动值来断定应该回滚到哪个子控件的界面上
                var targetIndex = (scrollX + width / 2) / width
                var dx = targetIndex * width - scrollX
                /** 第二步 调用 startScroll 办法弹性回滚并刷新页面 */
                mScroller.startScroll(scrollX, 0, dx, 0)
                invalidate()}
        }
        return super.onTouchEvent(event)
    }

    override fun computeScroll() {super.computeScroll()
        /**
         * 第三步 重写 computeScroll 办法,并在其外部实现平滑滚动的逻辑
         */
        if (mScroller.computeScrollOffset()) {scrollTo(mScroller.currX, mScroller.currY)
            postInvalidate()}
    }
}

下面代码很简略,通过 2 种形式解决了内部拦截法抵触,别离是:

  • 依据垂直或程度的间隔来判断
  • 依据第二点坐标 – 第一点坐标 如果差值大于 TouchSlop 就认为是在左右滑动

当然咱们也能够用外部拦截法来解决,依照咱们后面对外部拦截法的剖析,咱们只须要批改自定义 RecylerView 的 散发事件 dispatchTouchEvent 办法中的父容器的拦挡逻辑,上面请看代码实现:

class MyRecyclerView(context: Context, attrs: AttributeSet?) : RecyclerView(context, attrs) {
    /**
     * 别离记录咱们上次滑动的坐标
     */
    private var mLastX = 0;
    private var mLastY = 0;

    constructor(context: Context) : this(context, null)


    /**
     * 重写散发事件
     */
    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {val x = ev.getX().toInt()
        val y = ev.getY().toInt()

        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
            var par =    parent as ScrollerViewPager
                // 申请父类不要拦挡事件
                par.requestDisallowInterceptTouchEvent(true)
                Log.d("dispatchTouchEvent", "---》子 ACTION_DOWN");
            }
            MotionEvent.ACTION_MOVE -> {
                val deltaX = x - mLastX
                val deltaY = y - mLastY

                if (Math.abs(deltaX) > Math.abs(deltaY)){
                    var par =    parent as ScrollerViewPager
                    Log.d("dispatchTouchEvent", "dx:" + deltaX + "dy:" + deltaY);
                    // 交于父类来解决
                    par.requestDisallowInterceptTouchEvent(false)
                }
            }
            MotionEvent.ACTION_UP -> {}}
        mLastX = x
        mLastY = y
        return super.dispatchTouchEvent(ev)
    }

}

还须要改父类 onInterceptTouchEvent 办法

    /**
     * 子类申请父类也叫做外部拦截法
     */
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        interceptor = false
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                // 拿到手指按下相当于屏幕的坐标
                mDownX = ev.getRawX()
                mLastMoveX = mDownX
                if (!mScroller.isFinished) {mScroller.abortAnimation()
                    interceptor = true
                }
                Log.d("dispatchTouchEvent", "--->onInterceptTouchEvent,    ACTION_DOWN");
            }
            MotionEvent.ACTION_MOVE -> {
                // 拿到以后挪动的 x 坐标
                mMoveX = ev.getRawX()
                // 拿到差值
                mLastMoveX = mMoveX
                  // 父类耗费挪动事件,那么本身 onTouchEvent 会被调用
                interceptor = true
                Log.d("dispatchTouchEvent", "--->onInterceptTouchEvent,    ACTION_MOVE");
            }
        }
        return interceptor
    }
<?xml version="1.0" encoding="utf-8"?>
<com.devyk.customview.sample_1.ScrollerViewPager // 父节点
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        // 子节点
    <com.devyk.customview.sample_1.MyRecyclerView
            android:id="@+id/recyclerView" 
                        android:layout_width="match_parent"
            android:layout_height="match_parent">

    </com.devyk.customview.sample_1.MyRecyclerView>

    <com.devyk.customview.sample_1.MyRecyclerView
            android:id="@+id/recyclerView2" 
                        android:layout_width="match_parent"
            android:layout_height="match_parent">

    </com.devyk.customview.sample_1.MyRecyclerView>

    <com.devyk.customview.sample_1.MyRecyclerView
            android:id="@+id/recyclerView3" 
                        android:layout_width="match_parent"
            android:layout_height="match_parent">

    </com.devyk.customview.sample_1.MyRecyclerView>
</com.devyk.customview.sample_1.ScrollerViewPager>

这里解释一下下面代码的含意,首先在 MyRecylerView 中重写 dispatchTouchEvent 事件散发事件,别离对 DOWN, MOVE 做解决。

**DOWN: ** 当咱们手指按下的时候会执行到 ViewGroup 的 dispatchTouchEvent 办法,并且会执行 ViewGroup 的 onInterceptTouchEvent 拦挡事件办法,因为在 ScrollerViewPager 中重写了 onInterceptTouchEvent 事件,能够看到下面 DOWN 只有再滑动没有完结的状况下事件会由父类拦挡,那么个别状况下返回的就是 false 父类不拦挡,当父类不拦挡 DOWN 事件的时候,子节点 MyRecylerView 的 dispatchTouchEvent 的 DOWN 事件就会被触发,大家留神看,在 DOWN 事件中,我调用了以后根节点 ScrollerViewPager 的 requestDisallowInterceptTouchEvent(true) 办法,其意思就是不让父类执行 onInterceptTouchEvent 办法。

MOVE: 当咱们手指滑动的时候因为咱们申请父类不拦挡子节点事件,ViewGroup 的 onInterceptTouchEvent 就不会执行,当初就执行到子节点的 MOVE 办法,如果以后按下的 x,y 坐标减去上一次 x,y 坐标 只有 deltaX 的绝对值 > deltaY 那么就认为是在 左右滑动,当初就要拦挡子节点 MOVE 事件交于父节点来解决,从而在 ScrollerViewPager 就能够了左右滑动。反之就认为在高低滑动,子节点来解决。

能够看到外部拦截法比较复杂,不仅要批改子节点外部代码,还要批改父节点办法,其稳固和可维护性显著不如内部拦截法,所以还是举荐大家应用内部拦截法来解决工夫抵触。

上面看一个 APP 罕用性能,侧滑删除实现,个别侧滑是由一个 RecyclerView + 侧滑自定义 ViewGroup 来实现:

实战

参考该 Demo 中的实现,从中你能够学到自定义 ViewGroup、滑动抵触解决等技术。

[图片上传失败 …(image-acb847-1636703567722)]

本文转自 https://juejin.cn/post/6844904002753150983,如有侵权,请分割删除。

更多 Android 系列教程上传在 bilibili:** https://space.bilibili.com/686960634

退出移动版