3.View的事件体系

View的位置主要是由它的四个顶点来决定,分别对应于View的四个属性:top,left,right,bottom,这些坐标都是相对于View的父容器来说的,因此它是一种相对坐标。从Android3.0开始,View增加了额外的几个参数,x,y,translationX和translationY,其中x和y是View左上角的坐标,而translationX和translationY是View左上角相对于父容器的偏移量。这几个参数也是相对于父容器的坐标。x = left + translationX, y = top + translationY。View在平移过程中,top和left表示的是原始左上角的位置信息,其值并不会发生改变,次数发生改变的是x,y,translationX和translationY这四个参数。

通过MotionEvent对象我们可以得到点击事件发生的x和y坐标,为此,系统提供了两组方法:getX/getY和getRawX/getRawY。它们的区别其实很简单,getX/getY返回的是相对于当前View左上角的x和y坐标,而getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。

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

通过三种方式可以实现View的滑动:

  • 通过View本身通过的scrollTo/scrollBy方法来实现滑动
  • 通过动画给View施加平移效果来实现滑动
  • 通过改变View的LayoutParams使得View重新布局从而实现滑动

在滑动过程中,mScrollX总是等于View左边缘和View内容左边缘在水平方向的距离,mScrollY总是等于View上边缘和View内容上边缘在竖直方向上的距离。View边缘是指View的位置,由四个顶点组成,而View内容边缘是指View中内容的边缘,scrollTo/scrollBy只能改变View内容的位置而不能改变View在布局中的位置。

使用动画来移动View,主要是操作View的translationX和translationY属性。

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

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

解决滑动冲突的方式:外部拦截法和内部拦截法。

  • 外部拦截法,所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,这种方法的伪代码如下所示:
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;
    default:
      break;
  }
  mLastXIntercept = x;
  mLastYIntercept = y;
  return intercepted;
}

在ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交给父容器处理,这个时候事件没法再传递给子元素了;其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false;最后是ACTION_UP事件,这里必须要返回false,因为ACTION_UP事件本身没有太多意义。考虑一种情况,假设事件交由子元素处理,如果父容器在ACTION_UP时返回了true,就会导致子元素无法接收到ACTION_UP事件,这个时候子元素中的onClick事件就无法触发。但是父容器比较特殊,一旦它开始拦截任何一个事件。

  • 内部拦截法,内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法稍显复杂。它的伪码如下,我们需要重写子元素的dispatchTouchEvent方法:
// 子元素
public boolean dispatchTouchEvent(MotionEvent event) {
  int x = (int)event.getX();
  int y = (int)event.getY();
  switch(event.getAction()) {
    case MotionEvent.ACTION_DOWN:
      parent.requestDisallowInterceptTouchEvent(true);
      break;
    case MotionEvent.ACTION_MOVE:
      int deltaX = x - mLastX;
      int deltaY = y - mLastY;
      if (父容器需要此类点击事件) {
        parent.requestDisallowInterceptTouchEvent(false);
      }
      break;
    case MotionEvent.ACTION_UP:
      break;
    default:
      break;
  }
  mLastX = x;
  mLastY = y;
  return super.dispatchTouchEvent(event);
}

// 父容器
public boolean onInterceptTouchEvent(MotionEvent event) {
  if(event.getAction() == MotionEvent.ACTION_DOWN) {
    return false;
  } else {
    return true;
  }
}

results matching ""

    No results matching ""