天天看點

安卓下拉重新整理的一種實作

本文主要是基于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