天天看点

安卓下拉刷新的一种实现

本文主要是基于ViewGroup来实现平滑下拉和弹性回归的PTR控件

总体思想

根布局选择ViewGroup,添加header、body、footer三个子元素。通过重写onMeasure、onLayout来确定控件大小以及位置,通过dispatchTouchEvent来控制事件传递以及滑动,通过Scroller和computeScroll来实现弹性回归

本文仅从下拉刷新的角度分析

待解决的问题

  1. header的滑动
  2. 弹性滑动
  3. 解决滑动冲突

ViewGroup的选择

本文没有通过继承已有的ViewGroup实现类,而是直接继承ViewGroup来实现PTR控件的,这样有利于控件的扩展以及个性化

继承ViewGroup需要重写几个重要的方法。

onMeasure

确定自身以及每个子元素的大小

onLayout

确定每个子元素的位置

dispatchTouchEvent

获取事件,来控制滑动

computeScroll

配合Scroller实现弹性滑动

事件分发

事件分发基本原则伪代码
public boolean dispatchTouchEvent(MotionEvent event) {
    boolean consume = false;
    if (onInterceptTouchEvent(event)) {
      consume = onTouchEvent(event);
    }else {
      consume = child.dispatchTouchEvent(event);
    }
    return consume;
  }
           

想要控制自身的滑动,就必须获取到所有的move事件,同时,不能影响子元素有效的事件接收。其中很显然onInterceptTouchEvent以及onTouchEvent不一定会执行到(onInterceptTouchEvent是拦截一次后,改事件序列的后续事件都会被拦截,从而不会触发onInterceptTouchEvent的调用),因此本文选择的是dispatchTouchEvent

View滑动

  1. scrollTo
  2. scrollBy

以上两种方式都可以实现View的滑动,但是是瞬间的滑动。

如何解决弹性滑动?

弹性滑动其实就是把长距离的滑动分割为若干个短距离的连续滑动,基于这种思想,我们可以在move事件时来完成短距离的滑动,从而达到平滑滑动的效果;在header回弹时也是基于这种分割的思想

具体实现

测量定位

重写onMeasure方法来确定大小,为后续定位也做准备

@Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int mode = MeasureSpec.getMode(heightMeasureSpec);
    int size = MeasureSpec.getSize(heightMeasureSpec);
    // 增加高度,本文仅添加header的高度
    int height = MeasureSpec.makeMeasureSpec(size + MAX_HEADER_SIZE, mode);
    // 设置ViewGroupz自身的高度
    super.onMeasure(widthMeasureSpec, height);
    int headerSpec = MeasureSpec.makeMeasureSpec(MAX_HEADER_SIZE, mode);
    // 设置header的高度
    header.measure(widthMeasureSpec, headerSpec);
    // 设置body的高度
    body.measure(widthMeasureSpec, heightMeasureSpec);
  }
           

重写onLayout方法来确定位置,同时需要将Headery隐藏,相当于设置header的marginTop属性

@Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int paddingLeft = getPaddingLeft();
    int paddingRight = getPaddingRight();
    int paddingTop = getPaddingTop();
    int paddingBottom = getPaddingBottom();
    int measuredHeight = getMeasuredHeight();

    // 相当于设置header margin = -MAX_HEADER_SIZE
    header.layout(l + paddingLeft, -MAX_HEADER_SIZE, r - paddingRight, 0);

    body.layout(l + paddingLeft, paddingTop, r - paddingRight,
        measuredHeight - paddingBottom);
  }
           
事件消费和传递

重写dispatchTouchEvent事件来捕捉move事件,由于有自己来控制子元素的事件接收,所以重写了

onInterceptTouchEvent

方法,返回true,拦截所有的事件

@Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    return true;
  }
           

在dispatchTouchEvent方法中完成滑动,具体了以参考:

@Override
  public boolean dispatchTouchEvent(MotionEvent event) {
    int scrollY = getScrollY();
    float rawY = event.getRawY();
    int action = event.getAction();
    switch (action) {
      case MotionEvent.ACTION_DOWN:
        mLastMovePoint = mTouchDownPoint = rawY;
        isDragging = true;
        break;
      case MotionEvent.ACTION_MOVE:
        //  body控件是否能够向下滑动
        boolean canScrollVertically = mBodyView.getView().canScrollVertically(-1);
        float v = rawY - mLastMovePoint;
        if (v > 0) {
          mDirection = ORIENTATION_DOWN;
        } else {
          mDirection = ORIENTATION_UP;
        }
        // 触发可识别的最小滑动距离
        if (Math.abs(rawY - mTouchDownPoint) > mTouchSlot) {

          if (mDirection == ORIENTATION_UP) {
            if (scrollY < 0) {
              boolean reachTop = scrollY + Math.abs(v) >= 0;
              float dis = reachTop ? -scrollY : Math.abs(v);
              scrollBy(0, (int) dis);
              mLastMovePoint = rawY;
              isDragging = true;
              if (reachTop) {
                // 重新定位Body的DownEvent,防止跳跃性的滑动
                MotionEvent obtain = MotionEvent.obtain(event);
                obtain.setAction(MotionEvent.ACTION_DOWN);
                dispatchEventToBody(obtain);
              }
              return true;
            }
          } else if (mDirection == ORIENTATION_DOWN && scrollY > -MAX_SCROLL_SIZE) {
            if (!canScrollVertically) {
              float dis = scrollY - Math.abs(v) > -MAX_SCROLL_SIZE ? -Math.abs(v) : -MAX_SCROLL_SIZE - scrollY;
              scrollBy(0, (int) dis);
              mLastMovePoint = rawY;
              isDragging = true;
              return true;
            }
          }
        }
        mLastMovePoint = rawY;
        isDragging = true;
        dispatchEventToBody(event);
        break;
      case MotionEvent.ACTION_UP:
        mLastMovePoint = 0;
        isDragging = false;
        scrollToPosition();
        mTouchDownPoint = 0;
        break;
      case MotionEvent.ACTION_CANCEL:
        mLastMovePoint = 0;
        isDragging = false;
        mTouchDownPoint = 0;
        break;
      default:
        break;
    }
    // 分发事件
    if (action != MotionEvent.ACTION_MOVE) {
      dispatchEventToBody(event);
    }
    return true;
  }
           

以上代码中,主要涉及到几点说明:

  1. 触发滑动的规则:滑动距离大于

    ViewConfiguration.getScaledTouchSlop()

  2. 滑动方向的判定

    mDirection

  3. 是否拖拽的判定

    isDragging

  4. 滑动距离的判定

    v

  5. Body控件能否向下滑动的判定,处理滑动优先级

    canScrollVertically

  6. 子元素的事件分发逻辑

    dispatchEventToBody

其中是否下发事件到Body,主要依据点击事件是否发生在Body区域

private void dispatchEventToBody(MotionEvent event) {
    float evX = event.getRawX();
    float evY = event.getRawY();
    int[] location = new int[2];
    body.getLocationOnScreen(location);
    float x = location[0];
    float y = location[1];
    Log.e("body", "x= " + x + ", y= " + y);
    if (evX >= x && evX <= (x + body.getWidth()) && evY > y && evY < (y + body.getHeight())) {
      body.dispatchTouchEvent(event);
    }
  }
           
松手后的回弹

主要通过

Scroller

和回调方法

computeScroll

来实现

其中平滑滑动的触发条件为:

/**
   * @param start 当前位置的scrollY
   * @param end 目的位置的scrollY
   * @param duration 时长
   */
   private void smoothScroll(int start, int end, int duration) {
    int dis = end - start;
    mScroller.startScroll(0, start, 0, dis, duration);
    invalidate();
  }
           

Scroller相当于一个差值器,控制滑动距离的细分,通过

invalidate

来触发自身的重绘,在

computeScroll

中进行连续的滑动

/**
   * 所有滑动中的状态均在此处理
   */
  @Override
  public void computeScroll() {
    //手指已经离开,并且触发过差值器,则回弹到指定位置
    if (!isDragging && mScroller.computeScrollOffset()) {
      // 通过以下三行代码实现弹性回弹
      int currY = mScroller.getCurrY();
      scrollTo(0, currY);
      postInvalidate();
    }
  }
           

总结

  1. 弹性滑动是对长距离滑动的多次分割
  2. 触发重绘可以把多次微小的滑动串联起来
  3. 处理子元素和自身的滑动冲突(难点)
  4. 子元素跳跃性的滑动

链接

安卓下拉刷新的一种实现GitHub