View的事件体系

View 基础知识

  • View 是 Android 中所有控件的基类。
  • View的位置参数:top、left、right、bottom。
  • 从 Android3.0 开始,新增参数:x、y、translationX、translationY(x、y是 View 左上角的坐标;translationX、translationY 是 View 左上角相对于父容器的偏移量)。

    x = left + translationX
    y = top + translationY
    View 在平移的过程中,top 和 left 不会改变,发生改变的是 x、y、translationX、translationY。

  • MotionEvent - 手指接触屏幕所产生的一系列事件。

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

  • 通过 MotionEvent 对象,我们可以获取点击事件的 x y 坐标。

    getX/getY - 相对于当前 View 左上角的 x 和 y 坐标
    getRawX/getRawY - 相对于屏幕左上角的 x 和 y 坐标

  • ToushSlop - 系统所能识别出的被认为是滑动的最小距离。

    获取方式:ViewConfiguration.get(ctx).getScaledTouchSlop();

  • VelocityTracker - 用于追踪手指在滑动过程中的速度,包括水平和垂直方向上的速度。
    速度计算公式: 速度 = (终点位置 - 起点位置) / 时间段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //初始化
    VelocityTracker mVelocityTracker = VelocityTracker.obtain();
    //在onTouchEvent方法中
    mVelocityTracker.addMovement(event);
    //获取速度
    mVelocityTracker.computeCurrentVelocity(1000);
    float xVelocity = mVelocityTracker.getXVelocity();//水平速度
    float yVelocity = mVelocityTracker.getYVelocity();//垂直速度
    //重置和回收
    mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的时候调用
    mVelocityTracker.recycle(); //一般在onDetachedFromWindow中调用
  • GestureDetector - 手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。

    如果只是监听滑动相关的事件在 onTouchEvent 中实现;如果要监听双击这种行为的话,那么就使用GestureDetector

  • Scroller - 弹性滑动对象,用于实现 View 的弹性滑动。

View 的滑动

  • 实现 View 的滑动有以下3种常见的方式:

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

  • 使用 srcollTo/srcollBy 实现滑动,只能改变 View 内容的位置而不能改变 View 在布局中的位置。

    scrollBy 是基于当前位置的相对滑动,而 scrollTo 是基于所传参数的绝对滑动。通过 View 的getScrollX 和 getScrollY 方法可以得到滑动的距离。

  • 使用动画来实现滑动,主要是操作 View 的 translationX 和 translationY 属性。

    在 Android3.0 以前的系统上,View 动画和属性动画,新位置均无法触发点击事件,同时,老位置仍然可以触发单击事件。从3.0开始,属性动画的单击事件触发位置为移动后的位置,View 动画仍然在原位置。为了兼容3.0以下,推荐使用nineoldandroids

弹性滑动

  • 使用 Scroller

    Scroller的 工作原理: Scroller 本身并不能实现 View 的滑动,它需要配合 View 的 computeScroll 方法才能完成弹性滑动的效果,它不断地让 View 重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔 Scroller 就可以得出 View 的当前的滑动位置,知道了滑动位置就可以通过 scrollTo 方法来完成 View 的滑动。就这样, View 的每一次重绘都会导致 View 进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Scroller mScroller = new Scroller(mContext);

    //缓慢移动到指定位置
    private void smoothScrollBy(int dx, int dy) {
    int scrollX = getScrollX();
    int deltaX = dx - scrollX;
    //1000ms内滑向dx
    mScroller.startScroll(scrollX, 0, deltaX, 0, 1000);
    invalidate();
    }

    @Override
    public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
    scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
    postInvalidate();
    }
    }
  • 使用动画来实现弹性滑动

    动画本身就是一个渐近的过程。

  • 使用延时策略来实现弹性滑动

    延时策略的核心思想是通过发送一系列延时消息从而达到一种渐进式的效果,具体来说可以使用 Handler 或 View 的 postDelayed 方法,也可以使用线程的 sleep 方法。

View 的事件分发机制

  • 事件分发过程的三个重要方法

    • public boolean dispatchTouchEvent(MotionEvent ev)
      用来进行事件的分发。如果事件能够传递给当前 View ,那么此方法一定会被调用,返回结果受当前 View 的 onTouchEvent 和下级 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。

    • public boolean onInterceptTouchEvent(MotionEvent event)
      在 dispatchTouchEvent 方法内部调用,用来判断是否拦截某个事件,如果当前 View 拦截了某个事件,那么在同一个事件序列当中,此方法不会再被调用,返回结果表示是否拦截当前事件。
      若返回值为 True ,事件会传递到自己的 onTouchEvent() ;
      若返回值为 False ,传递到子 View 的 dispatchTouchEvent() 。

    • public boolean onTouchEvent(MotionEvent event)
      在 dispatchTouchEvent 方法内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件。
      若返回值为 True ,事件由自己处理,后续事件序列让其处理;
      若返回值为 False ,自己不消耗事件,向上返回让其他的父容器的 onTouchEvent 接受处理。

    • 三个方法的关系可以用下面的伪代码表示:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public boolean dispatchTouchEvent(MotionEvent ev) {
      boolean consume = false;
      if (onInterceptTouchEvent(ev)) {
      consume = onTouchEvent(ev);
      } else {
      consume = child.dispatchTouchEvent(ev);
      }
      return consume;
      }
  • OnTouchListener 的优先级比 onTouchEvent 要高
    如果给一个 View 设置了 OnTouchListener,那么 OnTouchListener 中的 onTouch 方法会被回调。这时事件如何处理还要看 onTouch 的返回值,如果返回 false ,那么当前 View 的 onTouchEvent 方法会被调用;如果返回 true ,那么 onTouchEvent 方法将不会被调用。
    在 onTouchEvent 方法中,如果当前 View 设置了 OnClickListener ,那么它的 onClick 方法会被调用,所以 OnClickListener 的优先级最低。

  • 当一个点击事件发生之后,传递过程遵循如下顺序:Activity -> Window -> View
    如果一个 View 的 onTouchEvent 方法返回 false,那么它的父容器的 onTouchEvent 方法将会被调用,依此类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传递给 Activity 处理(调用 Activity 的 onTouchEvent 方法)。
  • 正常情况下,一个事件序列只能被一个 View 拦截并消耗
    因为一旦某个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,并且该元素的 onInterceptTouchEvent 方法不会再被调用了。
  • 某个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件,那么同一事件序列的其他事件都不会再交给它来处理,并且事件将重新交给它的父容器去处理(调用父容器的 onTouchEvent 方法);如果它消耗 ACTION_DOWN 事件,但是不消耗其他类型事件,那么这个点击事件会消失,父容器的 onTouchEvent 方法不会被调用,当前 View 依然可以收到后续的事件,但是这些事件最后都会传递给 Activity 处理。
  • ViewGroup 默认不拦截任何事件,因为它的 onInterceptTouchEvent 方法默认返回 false。View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。
  • View 的 onTouchEvent 默认都会消耗事件(返回true),除非它是不可点击的(clickable 和 longClickable 都为 false )。View 的 longClickable 默认是 false 的,clickable 则不一定,Button 默认是 true,而 TextView 默认是 false。
  • View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕一个 View 是 disable 状态,只要它的 clickable 或者 longClickable 有一个是 true,那么它的 onTouchEvent 就会返回 true。
  • 事件传递过程总是先传递给父元素,然后再由父元素分发给子 View ,通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外,即当面对 ACTION_DOWN 事件时,ViewGroup 总是会调用自己的 onInterceptTouchEvent 方法来询问自己是否要拦截事件。
    ViewGroup 的 dispatchTouchEvent 方法中有一个标志位 FLAG_DISALLOW_INTERCEPT,这个标志位就是通过子 View 调用 requestDisallowInterceptTouchEvent 方法来设置的,一旦设置为 true,那么 ViewGroup 不会拦截该事件。
  • 以上结论均可以在书中的源码解析部分得到解释。Window 的实现类为 PhoneWindow,获取 Activity 的 contentView 的方法:
    1
    ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);

View 的滑动冲突

  • 常见的滑动冲突的场景:
    • 外部滑动方向和内部滑动方向不一致
    • 外部滑动方向和内部滑动方向一致
    • 上面两种情况的嵌套
  • 滑动冲突处理规则
    可以根据滑动距离和水平方向形成的夹角;或者根绝水平和竖直方向滑动的距离差;或者两个方向上的速度差等
  • 解决方式

    • 外部拦截法:点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要就不拦截。该方法需要重写父容器的 onInterceptTouchEvent 方法,在内部做相应的拦截即可,其他均不需要做修改。
      伪代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      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: {
      int deltaX = x - mLastXIntercept;
      int deltaY = y - mLastYIntercept;
      if (父容器需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
      intercepted = true;
      } else {
      intercepted = false;
      }
      break;
      }
      case MotionEvent.ACTION_UP: {
      intercepted = false;
      break;
      }
      default:
      break;
      }

      mLastXIntercept = x;
      mLastYIntercept = y;

      return intercepted;
      }
    • 内部拦截法:父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器来处理。这种方法和Android中的事件分发机制不一致,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作。
      伪代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      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 (当前view需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
      getParent().requestDisallowInterceptTouchEvent(false);
      }
      break;
      }
      case MotionEvent.ACTION_UP: {
      break;
      }
      default:
      break;
      }

      mLastX = x;
      mLastY = y;
      return super.dispatchTouchEvent(event);
      }

本章节配套源码戳我

本人在学习和记录笔记的过程中参考了这里,他山之石可以攻玉,感谢前辈!