<<Android 开发艺术探索>> Chapter 3

View的事件体系

View的基础

  1. view位置参数

    • View的位置主要由它的四个顶点来决定,分别对应于View的四个属性:topleftrightbottom,其中top是左上角纵坐标,left是左上角横坐标,right是右下角横坐标,bottom是右下角纵坐标, 这四个参数的坐标值都是View相对于父View的.
      View的宽高和坐标的关系:

      width = right - left;
      height = bottom - top;
      

      如何得到这四个参数:

      Left = getLeft();
      Right = getRight();
      Top = getTop();
      Bottom = getBottom();
      
    • 从Android 3.0开始,view增加了xytranslationXtranslationY四个参数,这几个参数也是相对于父容器的坐标. x和y是左上角的坐标,而translationX和translationY是view左上角相对于父容器的偏移量,默认值都是0.

      x = left + translationX
      y = top + translationY
      

      View在平移过程中改变的是x, y, translationX, translationY这四个参数, lefttop等是原始左上角的位置信息, 其值不会随着平移改变.

      移动前

      移动后

      setX()内部也是调用的setTranslationX()
      setLeft()方法系统不建议我们人为调用, 因为left属性在layout()时系统会随时更改

    • View在滑动其内容时更改的是它的mScrollX mScrollY这两个参数
      mScrollX的值总是等于View左边缘和View内容左边缘在水平方向的距离
      mScrollY的值总是等于View上边缘和View内容上边缘在垂直方向的距离
      scrollTo()scrollBy()内部其实就是更改这两个参数.

  2. MotionEvent和TouchSlop

    • MotionEvent
      在手指触摸屏幕后所产生的一系列事件中,典型的事件类型有:

      1. ACTION_DOWN ----- 手指刚接触屏幕
      2. ACTION_MOVE ----- 手指在屏幕上移动
      3. ACTION_UP ----- 手机从屏幕上松开的一瞬间

      正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,考虑如下几种情况:

      1. 点击屏幕后离开松开,事件序列为 DOWN -> UP
      2. 点击屏幕滑动一会再松开,事件序列为DOWN->MOVE->...->UP

      通过MotionEvent对象我们可以得到点击事件发生的x和y坐标,getX/getY返回的是相对于当前View左上角的x和y坐标,getRawX和getRawY是相对于手机屏幕左上角的x和y坐标。

    • TouchSlop
      TouchSlope是系统所能识别出的可以被认为是滑动的最小距离,获取方式是ViewConfiguration.get(getContext()).getScaledTouchSlope()

  3. VelocityTracker、GestureDetector和Scroller

    1. VelocityTracker
      用于追踪手指在滑动过程中的速度,包括水平和垂直方向上的速度.
      VelocityTracker的使用方式:

      //初始化
      VelocityTracker mVelocityTracker = VelocityTracker.obtain();
      //在onTouchEvent方法中
      mVelocityTracker.addMovement(event);
      //获取速度
      mVelocityTracker.computeCurrentVelocity(1000);
      float xVelocity = mVelocityTracker.getXVelocity();//一般在MotionEvent.ACTION_UP的时候调用
      //重置和回收
      mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的时候调用
      mVelocityTracker.recycle(); //一般在onDetachedFromWindow中调用
      
    2. GestureDetector
      手势检测,用于辅助检测用户的点击、滑动、长按、双击等行为.我们通过查看源码,发现在GestureDetector类中封装了两个接口和一个内部类:


      GestureDetector

      分别为OnGestureListenerOnDoubleTapListener两种listener.
      SimpleOnGestureListener实现了上述两种listener, 但是内部的实现方法都为null, 使用时根据个人需要来实现对应的方法.
      GestureDetector使用方式:

      GestureDetector mGestureDetector = new GestureDetector(new SimpleOnGestureListener () {
          //实现需要用到的方法
      });
      mGestureDetector.setIsLongPressEnabled(false);//解决长按屏幕后无法拖动的现象.
      
      boolean consume = mGestureDetector.onTouchEvent(event);//一般在onTouchEvent中接管event
      return consume;
      

      OnGestureListenerOnDoubleTapListener接口具体如下:

      public interface OnGestureListener {
          boolean onDown(MotionEvent e);  //手指刚刚触碰屏幕的一瞬间, 由一个ACTION_DOWN触发
          void onShowPress(MotionEvent e); //手指轻轻触碰屏幕, 尚未松开或拖动, 由一个ACTION_DOWN触,它和onDown的区别是它强调的是没有松开或者拖动的状态
          boolean onSingleTapUp(MotionEvent e); //单击行为, 伴随着一个ACTION_UP而触发
          boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY); //手指按下屏幕并拖动, 由一个ACTION_DOWN和多个ACTION_MOVE组成,这是拖动行为
          void onLongPress(MotionEvent e); //用户长久的按着屏幕不放,即长按
          boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); //快速滑动行为,由一个ACTION_DOWN,多个ACTION_MOVE,一个ACTION_UP触发
      }
      
      public interface OnDoubleTapListener {
          boolean onSingleTapConfirmed(MotionEvent e); //严格的单击行为, 即这只可能是单击而不可能是双击中的一次单击
          boolean onDoubleTap(MotionEvent e); //双击行为,它不可能和onSingleTapConfirmed共存
          boolean onDoubleTapEvent(MotionEvent e); //表示发生了双击行为, 在双击期间ACTION_DOWN,ACTION_MOVE,ACTION_UP均会触发此回调
      }
      

      在日常开发中,比较常用的有: onSingleTapUp(单击)onFling(快速滑动)onScroll(拖动)onLongPress(长按)onDoubleTap(双击).
      建议:如果只是监听滑动相关的事件在onTouchEvent中实现;如果要监听双击这种行为的话,那么就使用GestureDetector

    3. Scroller
      弹性滑动对象,用于实现View的弹性滑动。Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合使用才能共同完成这个功能。
      Scroller使用方式

      Scroller scroller = new Scroller(mContext);
      
      // 缓慢滚动到指定位置
      private void smoothScrollTo(int destX, int destY) {
         int scrollX = getScrollX();
         int delta = destX - scrollX;
         //1000ms内滑动到destX的位置
         mScroller.startScroll(scrollX, 0, delta, 0, 1000);
         invalidate();
      }
      
      @Override
      public void computeScroll() {
         if(mScroller.computeScrollOffset()) {
             scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
             postInvalidate();
         }
      }
      

      原理:invalidate()方法会触发computeScroll()方法, 然后我们重写了computeScroll()在里面调用scrollTo来让View移动到Scroller计算过后的位置, 然后再次触发invalidate()方法, 直到Scroller计算完成。


View的滑动

  1. 使用scrollTo或scrollBy
    scrollTo()是基于所传参数的绝对滑动, scrollBy()是基于目前所在位置的相对滑动.
    scrollTo()scrollBy()只能改变View内容的位置, 不能改变View在布局中的位置.

  2. 使用动画
    android中动画分为三种:View动画 帧动画 属性动画.
    我们通过View动画属性动画都可以完成View的滑动, 使用动画主要操作的是ViewtranslationXtranslationY这两个属性(因为setX()内部其实调用的时setTranslationX()).

    使用上我们需要注意以下两点:

    • view动画操作的是控件的影像而不是view的位置参数(它不会移动view的本身也不会移动view的内容),尽管我们在视觉上看到了滑动的效果,但实际上view的位置却不曾发生改变。这点可以从如果我们不设置view的控件参数fillAftrer为true的时候,那么当动画完成后,View会瞬间恢复到动画前的效果就可以看得出来。而且,即便我们设置了fillAfter参数为true。也只是相当于把view投影到移动的位置,但当我们再要执行点击操作的时候,却是不能发生响应的。因为view的位置不会发生改变。它的真身仍在原始位置上。
    • view的属性动画可以解决上面的问题, 但是它无法兼容3.0以下的版本.
  3. 通过改变布局参数
    通过改变布局参数的方式来实现滑动,实际上改变的是LayoutParams参数,如果我们想要滑动某个控件,则直接通过修改LayoutParams参数来实现,这个方法最为简单暴力,但操作较为复杂,需要根据不同的情况去做不同的处理。使用方法如下(以移动一个Button为例):

    Button button = (Button) findViewById(R.id.btn_changeparams);
    MarginLayoutParams params = (MarginLayoutParams) button.getLayoutParams();
    params.width += 100;
    params.leftMargin +=100;
    button.requestLayout();
    
  4. 三种滑动方式对比:

    • scrollTo/scrollBy: 操作简单,适合对View内容的滑动
    • 动画: 操作简单,主要适用于没有交互的View和实现复杂的动画效果
    • 改变布局参数: 操作稍微复杂,适用于有交互的View

View弹性滑动

  1. 使用Scroller
    上面已经介绍过了Scroller的原理和使用方法

  2. 使用动画
    采用这种方法除了能完成弹性滑动以外,还可以实现其他动画效果,我们完全可以在onAnimationUpdate方法中加上我们想要的其他操作。

  3. 使用延时策略
    使用延时策略来实现弹性滑动,它的核心思想是通过发送一系列延时消息从而达到一种渐进式的效果,具体来说可以使用HandlersendEmptyMessageDelayed(xxx)viewpostDelayed()方法,也可以使用线程的sleep方法。

    private Handler = new Handler(){
        public void handleMwssage(Message msg){
            switch(msg.what){
                case  MOVE_VIEW:
                //move view step
                handle.sendEmptyMessageDelayed(MOVE_VIEW,1000);
                break;
            }
        }
    };
    

View的事件分发机制

  1. 点击事件的传递规则
    所谓点击事件的事件分发,其实就是对MotionEvent的分发过程。当一个MotionEvent产生之后,系统需要将其传递给某个具体的View,比如Button控件,并且被这个View所消耗。整个事件分发过程由三个方法完成,分别是:

    • dispatchTouchEvent(MotionEvent event)
      /**
       * 这个方法用来进行事件的分发,当MotionEvent事件传递到当前View时,便会触发当前View的这个方法,
       * 返回的结果受当前View的onTouchEvent和下级的dispatchTouchEvent方法的影响,表示是否消耗该MotionEvent。
       * true表示被当前View所消耗,false则表示事件未被消耗。
       */
      public boolean dispatchTouchEvent(MotionEvent event);
      
    • onInterceptTouchEvent(MotionEvent event)
      /**
       * 这个方法在dispatchTouchEvent方法内部调用,用来判断是否拦截某个事件,
       * 如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会再被调用,
       * 返回结果表示是否拦截当前事件。
       */
      public boolean onInterceptTouchEvent(MotionEvent event);
      
    • onTouchEvent(MotionEvent event)
      /**
       * 这个方法在dispatchTouchEvent方法内部调用,用来处理点击事件,
       * 返回结果表示是否消耗当前事件,如果不消耗(ACTION_DOWN),则在同一事件序列中,当前View无法再次接收到该事件。
       */
       public boolean onTouchEvent(MotionEvent event);
      

    以上三者的关系可以用伪代码进行表示:

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

    对于一个根ViewGroup来说,当产生点击事件后,首先会传递给它,此时调用它的dispatchTouchEvent
    方法,如果dispatchTouchEvent方法中的onInterceptTouchEvent(event)返回true,则表示这个ViewGroup要消耗当前事件,于是调用ViewGroupOnTouchEvent(event)方法。而如果onInterceptTouchEvent(event)返回的是false,则将该event交给这个当前View的子元素的dispatchTouchEvent去处理。如此递归,直到事件被最终处理掉。

    当一个点击事件产生后,它的传递顺序如下:Activity -> Window -> View
    Activity是怎么接收到点击事件的请参考这篇文章
    当顶级View接收到该事件后,就会将其按照事件分发机制去分发该事件,也即从父容器到子容器间层层传递,直到在某一个阶段事件被消耗完毕。但在这里存在另一个问题:如果最底层的子元素并没有消耗点击事件,怎么办?为解决这个问题,系统做了以下的措施:如果一个View的onTouchEvent方法返回的是false,那么该view的父容器的onTouchEvent方法也会被调用,以此类推,若该点击事件没有任何元素去消耗,那么最终仍是会由Activity进行处理

    关于事件传递的机制,有以下结论:

    1. 同一个事件序列是指从手指接触到屏幕的那一刻起,到手指离开屏幕的那一刻结束。期间以Down为开始,中间含有数量不等(可以为0)的MOVE,最终则以UP结束。
    2. 正常情况下,一个事件序列只能被一个View拦截且进行消耗。
    3. 某个View一旦决定拦截事件序列,那么这一个事件序列只能由它来处理(只要在这个view进行拦截之前没有其他view对这个事件序列进行拦截),并且它的onInterceptTouchEvent方法也不会再被调用。
    4. 某个View一旦开始处理事件序列,如果它不消耗ACTION_DOWN事件(OnTouchEvent返回false),那么同一个事件序列中的其他事件都不会由它来处理,而是直接将其交由父元素去处理。并且当前view是无法再次接收到该事件的。
    5. 如果View不消耗除了ACTION_DOWN之外的其他事件,那么这个点击事件就会消失,并且父元素的OnTouchEvent方法也不会被调用,同时,当前View可以持续收到后续的事件,最终这些消失的点击事件会交由Activity进行处理。
    6. ViewGroup不拦截任何事件。Android源码中ViewGrouponInterceptTouchEvent方法默认返回false
    7. Android源码中,View并没有onInterceptTouchEvent方法,一旦有点击事件传递给它。那么它的OnTouchEvent方法就会被调用。
    8. viewOnTouchEvent默认会消耗该事件(默认返回true),除非它是不可点击的(clickablelongclickable同时为false)。
    9. viewenable属性不影响onTouchEvent的默认放回值。即便该viewdisable状态的,但只要它的clickablelongClickable有一个为true,那么它的返回值就为true
    10. onclick会发生的前提是当前View是可点击的,并且它接收到了ACTION_DOWNACTION_UP事件。
    11. 事件传递过程是由外向内的,及事件总是先传递给父元素。然后再有父元素去分发给子元素。但通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的分发过程,但ACTION_DOWN事件除外。
  2. 从源码去看事件分发机制:

    • Activity分发
      从上面我们知道,每个MotionEvent都是最先交由Activity进行的,那么我们来看看Activity中的dispatchTouchEvent方法

       public boolean dispatchTouchEvent(MotionEvent ev) {
          if (ev.getAction() == MotionEvent.ACTION_DOWN) {
              onUserInteraction();
          }
          if (getWindow().superDispatchTouchEvent(ev)) {
              return true;
          }
          return onTouchEvent(ev);
      }
      
    • Window分发
      我们可以看到Activity其实是将点击事件交给了Window进行下一步处理, 但是Window类其实是一个抽象类, 它里面的superDispatchTouchEvent()方法是一个抽象方法.
      所以我们需要去它的唯一实现类PhoneWindow中去查看superDispatchTouchEvent()是如何实现的.

      //PhoneWindow中的superDispatchTouchEvent()方法
      public boolean superDispatchTouchEvent(MotionEvent event){
          return mDecor.superDispatchTouchEvent(event);
      }
      

      这里的mDecor其实就是DecorView,那么DecorView是什么呢?我们来看

      private final class DecorView extends FrameLayout implements RootViewSurfaceTacker{
          private DecorView mDecor;
          @override
          public final View getDecorView(){
              if(mDecor == null){
                  installDecor();
              }
              return mDecor;
          }
      }
      

      我们知道,通过(ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0);这种方式可以获取Activity的所预设置的View,而这个mDector显然就是返回的对象。也就是说,这里的DecorView是顶级View(ViewGroup),内部有titlebarcontentParent两个子元素,contentParentidcontent,而我们设置的main.xml布局则是contentParent里面的一个子元素。那么,当事件传递到DecorView这里的时候,因为DecorView继承了FrameLayout且还是父View,所以最终的事件会传送到我们在setContentView()所设置的顶级View中。

    • ViewGroup分发
      那么,现在事件已经传递到顶级View(一个ViewGroup)了,接下来又该是怎样的呢?逻辑思路如下:

      顶级View调用dispatchTouchEvent方法
      if 顶级view需要拦截事件(onInterceptTouchEvent方法返回true)
        处理点击事件
      else
        把事件传递给子元素进行处理
      

      根据这个,我们先来看一下ViewGroup对点击事件的分发过程,其主要体现在dispatchTouchEvent方法中。因为这个方法比较长,分段说明,先看下面一段:

      public boolean dispatchTouchEvent(MotionEvent ev) {
          //....省略
          final boolean intercepted;
          if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    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会在两种情况下判断是否拦截当前事件:一是事件类型为ACTION_DOWN,二则是mFirstTouchTarget != null。在这里,mFirstTouchTarget是什么意思呢? 可以这么理解:当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素。也就是说,当ViewGroup不拦截事件并且把事件交给子元素处理时,则mFirstTouchTarget != null。反之,如果ViewFroup拦截了这个事件,则mFirstTouchTarget != null就不成立, 所以当ACTION_MOVEACTION_UP事件到来时, actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != nullfalse, ViewGroup将会直接拦截事件而不会再次调用它自己的onInterceptTouchEvent(ev)方法,并且同一序列中的其他事件会交由它处理(前提是事件到达它之前没有被拦截)。对上面第3条结论的验证

      当然,事实无绝对,此处有一个特殊情况,就是FLAG _DISALLOW _INTERCEPT这个标志位,它是通过requestDisallowInterceptTouchEvent()方法来设置的,一般用于子View中。它一旦被设置,ViewGroup则将无法拦截除了ACTION _DOWN以外的其他点击事件。为什么是除了ACTION_DOWN以外呢?

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

      在这段源码中,ViewGroup会在ACTION_DOWN事件到来时做重置状态的操作,而在 resetTouchState方法中会对FLAG _DISALLOW _INTERCEPT进行重置,因此子View调用requestDisallowInterceptTouchEvent方法时并不能影响ViewGroupACTION _DOWN的影响。
      接着我们再看当ViewGroup不拦截事件的时候。事件会向下分发,交由它的子View进行处理的过程:

      public boolean dispatchTouchEvent(MotionEvent ev) {
        // 省略...View的LONG_CLICKABLE属性默认为false,而CLICKABLE的属性则和具体的View有关。通过setClickable和setLongClickable方法可以修改这两个值。此外,在setOnClickListener中也会自动将CLICKABLE属性改为true,而setOnLongClickListener则将LONG _CLICKABLE设置为true。
        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);
              // 如果一个child没有播放动画&&点击事件落在了它的区域内
              if (!canViewReceivePointerEvents(child)
                      || !isTransformedTouchPointInView(x, y, child, null)) {
                  continue;
              }
              // 省略...
              if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                  // 省略...
                  // 这个child消耗了这个点击事件, 对mFirstTouchTarget赋值
                  newTouchTarget = addTouchTarget(child, idBitsToAssign);
                  alreadyDispatchedToNewTouchTarget = true;
                  break;
              }
          }
          // 省略...
          if (mFirstTouchTarget == null) {
              // 没有子View消耗了点击事件
              handled = dispatchTransformedTouchEvent(ev, canceled, null,
                      TouchTarget.ALL_POINTER_IDS);
          }
      }
      

      从源码中,我们可以发现它的过程如下:首先遍历ViewGroup的所有子元素,然后判定子元素是否能够接收到点击事件(子元素是否在播动画或者点击事件的坐标是否落在子元素的区域内)。如果某个子元素满足这两个条件,那么事件就会交由它来处理。可以看到,dispatchTransformedTouchEvent方法实际上调用的就是子元素的dispatchTouchEvent方法。怎么看的呢?在这个方法的内部,有这么一段:

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

      返回上一段源码,如果子元素的dispatchTouchEvent(event)方法返回true,那么我们就不需考虑事件在子元素是怎么派发的,那么mFirstTouchTarget就会被赋值,同时跳出for循环。从源码中抽取相关部分见下:

      newTouchTarget = addTouchTarget(child, idBitsToAssign);
      alreadyDispatchedToNewTouchTarget = true;
      break;
      

      有人说,这段代码并没有对mFirstTouchTarget的赋值,因为它实际上出现在addTouchTarget方法中,源码如下:

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

      从这个方法的内部结构可以看出,mFirstTouchTarget是以一种单链表结构,它的赋值与否直接影响到了ViewGroup的拦截策略。

      接下来我们再次返回最初的源码中, 如果遍历所有的子元素事件后都没有被合适地处理,这包含两种情况:一是ViewGroup中没有子元素,二则是子元素处理了点击事件,但是在dispatchTouchEvent方法中返回了false在这两种情况下,ViewGroup会调用它自己的onTouchEvent()处理点击事件

      if (mFirstTouchTarget == null) {
          // 没有子View消耗了点击事件
          handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
      }
      

      注意这一段源码的第三个参数childnull,从前面的分析就可以知道,它会调用super.dispatchTouchEvent(event),很显然,这里就从ViewGroup转到了ViewdispatchTouchEvent(event)
      在随后我们对ViewdispatchTouchEvent(event)分析中我们会发现, ViewdispatchTouchEvent(event)会调用onTouchEvent()方法.

      注意:在这时View的dispatchTouchEvent()中其实调用的是ViewGroup中的onTouchEvent()方法.
      因此当一个ViewGroupACTION_DOWN事件没有被子View消耗时, 这个ViewGroup本身的onTouchEvent()就会被调用来处理这个点击事件(对上面第4条结论的验证)

      这时你们可能会奇怪, 为什么我们在ViewdispatchTouchEvent()方法中调用ViewGroup中的onTouchEvent()方法.

      我们来看下面这段代码:

      public class A {
          public void AA() {
              System.out.println("A.AA");
              BB();
          }
      
          public void BB() {
              System.out.println("A.BB");
          }
      }
      
      public class B extends A {
          @Override
          public void AA() {
              System.out.println("B.AA");
          }
      
          @Override
          public void BB() {
              System.out.println("B.BB");
          }
      
          public void CC() {
              super.AA();
          }
      }
      

      我们定义两个类A和B, A和B中都有AABB方法, 并且输出不同的Log, 那么此时我们执行new B().CC()会输出什么结果呢?
      答案是:

      A.AA
      B.BB
      

      是不是猜错了?
      为什么会是这样的结果呢, 因为我们是在B中调用的super.AA(), 因此在A的AA()方法中我们调用this其实拿到的是一个B的引用, 如下图

      Screenshot from 2018-03-08 17:19:40.png

      所以在A的AA()方法中我们会执行B的BB()方法.

      现在是不是就明白了, 为什么我们在View的dispatchTouchEvent()中调用的是ViewGroup中的onTouchEvent()方法了? 因为View的dispatchTouchEvent()是通过ViewGroup调起来的.

    • View分发
      接下来我们回过头继续看ViewdispatchTouchEvent()方法

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

      View对点击事件的处理过程比较简单,因为View是一个单独的元素,因此无法向下传递事件。所以它只能自己处理事件。从上面的源码可以看出View对点击事件的处理过程:首先判断有没有设置onTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,由此可见OnTouchListener方法的优先级高于onTouchEvent

      接下来,分析onTouchEvent的实现。先看当View处于不可用状态下点击事件的处理过程:

      public boolean onTouchEvent(MotionEvent event) {
          // 省略...
          if ((viewFlags & ENABLED_MASK) == DISABLED) {
              if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                 setPressed(false);
              }
              // A disabled view that is clickable still consumes the touch
              // events, it just doesn't respond to them.
              return (((viewFlags & CLICKABLE) == CLICKABLE ||
                      (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
          }
          // 省略...
      }
      

      很显然,不可用状态下的view照样会消耗点击事件,尽管它看起来不可用。

      接着,再来看一下onTouchEvent方法中对点击事件的具体处理:

      public boolean onTouchEvent(MotionEvent event) {
          // 省略...
          if (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
              switch (event.getAction()) {
                  case MotionEvent.ACTION_UP:
                      // 省略...
                      if (mPerformClick == null) {
                          mPerformClick = new PerformClick();
                      }
                      if (!post(mPerformClick)) {
                          performClick();
                      }
                      // 省略...
                      break;
                  case MotionEvent.ACTION_DOWN:
                      // 省略...
                      break;
                  case MotionEvent.ACTION_CANCEL:
                      // 省略...
                      break;
                  case MotionEvent.ACTION_MOVE:
                      // 省略...
                      break;
              }
              return true;
          }
      }
      

      从源码来看,只要ViewCLICKABLELONG_CLICKABLE有一个为true,那么它就将消耗这个事件,即onTouchEvent返回true, 不管它是不是DISABLE状态。
      而当MOTION_UP事件发生时,则触发performClick()方法,如果View设置了onClickListener,那么performClick()方法内部会调用它的onClick方法

      ViewLONG_CLICKABLE属性默认为false,而CLICKABLE的属性则和具体的View有关。通过setClickablesetLongClickable方法可以修改这两个值。此外,在setOnClickListener中也会自动将CLICKABLE属性改为true,而setOnLongClickListener则将LONG_CLICKABLE设置为true


view的滑动冲突

Android中的滑动冲突是比较常见的一个问题,只要在界面中内外两层同时滑动的时候,就会产生滑动。意即有一个占主导地位的View抢着去执行滑动操作,从而带来非常差的用户体验。常见的滑动冲突场景分为如下三种:

  • 场景一:外部滑动方向与内部滑动方向不一致,主要是将ViewPager和Fragment配合使用所形成的页面滑动效果。在这个效果中,可以通过左右滑动来切换页面,而每个页面内部往往又是一个Listview。这种情况下本来是很容易发生滑动冲突的,但ViewPager内部处理了这种滑动冲突,所以如果使用ViewPager,则无需担心这个问题。但如果使用的是Scroller,则必须手动处理滑动冲突了。否则后果就是内外两层只能有一层能够滑动。
    处理规则:当用户左右滑动时,需要让外部的View拦截点击事件。当用户上下滑动时,需要让内部View拦截点击事件。这个时候我们就可以根据它们的特征来解决滑动冲突。具体来说是:根据滑动的方向判断到底由什么来拦截事件。

  • 场景二:外部滑动和内部滑动方向一致,比如ScrollView嵌套ListView,或者是ScrollView嵌套自己。表现在要么只能有一层能够滑动,要么两者滑动起来显得十分卡顿。
    处理规则:从业务上寻找突破点,比如业务上有规定:当处于某种状态时需要外部View处理用户的操作,而处理另一种状态时则让内部View处理用户的操作。

  • 场景三:上面两种情况的嵌套。
    处理规则:同场景二

滑动冲突的解决方式:

针对场景一的滑动冲突,有两种处理滑动的解决方式:

  • 外部拦截法:
    所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这个方法需要重写父容器的onInterceptTouchEvent()方法。伪代码如下所示:
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted=false;
        int x=(int)event.getX();
        int y=(int)event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                intercepted=false;
                break;
            case MotionEvent.ACTION_MOVE:
                if(父容器需要当前点击事件){
                   intercepted=true;
                }else {
                    intercepted=false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted=false;
                break;
            default:
                break;
        }
        mLastXIntercept=x;
        mLastYIntercept=y;
        return intercepted;
    }
    
  • 内部拦截法:
    内部拦截法是指父容器不拦截任何事件,所有的事件传递给子元素,如果子元素需要此事件就直接消耗掉,如果不需要则交由父容器处理。需要配合requestDisallowInterceptTouchEvent()方法才能正常工作。伪代码如下:
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x=(int)event.getX();
        int y=(int)event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX=x-mLastX;
                int deltaY=y-mLastY;
                if(父容器需要当前点击事件){
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        mLastX=x;
        mLastY=y;
        return super.dispatchTouchEvent(event);
    }
    
    另外,为了使父容器不接收ACTION_DOWN事件,我们需要对父类进行一下修改:
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        int action=event.getAction();
        if (action==MotionEvent.ACTION_DOWN){
            return false;
        }else{
            return true;
        }
    }
    
    以上两种方式,是针对场景一而得出的通用的解决方法。对于场景二和场景三而言,只需改变相关的滑动规则的逻辑即可。
    注意:因为内部拦截法的操作较为复杂,因此推荐采用外部拦截法来处理常见的滑动冲突。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容