Android SwipeRefreshLayout要點詳解

SwipRefreshLayout是google提供的support v4包下面的下拉刷新控件,他繼承自ViewGroup,內部可以放幾乎所有的滾動控件。This layout should be made the parent of the view that will be refreshed as a result of the gesture and can only support one direct child.

本文不涉及到具體的使用,因為這個控件已經爛大街了,在很多標榜material design設計的app中都標配這個活潑的小球,在這樣的情況下,出現了向美團,京東等,下拉出現更有趣動畫的實現,比如

recruit-lifestyle/WaveSwipeRefreshLayout水滴下拉刷新…

WaveRefreshForAndroid這個是基于Android-PullToRefresh修改的而成的水波紋下拉刷新…可能作者主攻ios,所以ios的效果看起來好看點WaveRefresh…

我們會發現,他們好像各有各的炫酷狂拽吊炸天,但是有好像和親兄弟一樣,都是通過下拉動作,觸發一系列的動畫和動作。下面就以他們的爹爹SwipRefreshLayout來分析他們是如何實現的,了解了原來,想在SwipRefreshLayout上定制一個自己的下拉庫也就易如反掌了。

SwipeRefreshLayout extends ViewGroup

再炫的控件也是要繼承基礎的View,既然是繼承ViewGroup,那么他肯定要實現下面倆個方法

  • onMeasure
  • onLayout

找到對應的代碼

onMeasure

其實SwipRefreshLayout中只有來個子View,一個是類似listview,還有個是它自己添加的circleView,在onMeasure中計算childView的測量值以及模式,以及設置自己的寬和高,

@Override
   public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //判斷內部控件是否是空,如果是空,就去找到,這里其實就是找mTarget=listview
       if (mTarget == null) {
           ensureTarget();
       }
       if (mTarget == null) {
           return;
       }
     //測量listview大小,去掉padding
       mTarget.measure(MeasureSpec.makeMeasureSpec(
               getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
               MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
               getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
        //測量小圓球大小,絕對大小
      //mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER_LARGE * metrics.density);
        //mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density);
       mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY),
               MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY));
       //mCurrentTargetOffsetTop 小圓球的上邊Y軸坐標
       if (!mUsingCustomStart && !mOriginalOffsetCalculated) {
           mOriginalOffsetCalculated = true;
           mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight();
       }
       mCircleViewIndex = -1;
       // Get the index of the circleview.
       for (int index = 0; index < getChildCount(); index++) {
           if (getChildAt(index) == mCircleView) {
               mCircleViewIndex = index;
               break;
           }
       }
   }

onLayout

在onLayout中對子view固定位置,忽然想到,下拉的時候也就是位置在改變,所以每次重新繪制的時候,onlayout中肯定有一個變量,在決定著下拉的高度,仔細看下,果然有個變量mCurrentTargetOffsetTop,如果我們找到了這個變量的變化的觸發方法,也就是找到了下拉刷新核心秘密。

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//獲取自己的寬高
    final int width = getMeasuredWidth();
    final int height = getMeasuredHeight();
    if (getChildCount() == 0) {
        return;
    }
    //同樣的代碼,確定內部的view
    if (mTarget == null) {
        ensureTarget();
    }
    if (mTarget == null) {
        return;
    }
    //這里感覺有點多余,childRight不就是width-getPaddingRight()么,老外的想法也真奇怪
    final View child = mTarget;
    final int childLeft = getPaddingLeft();
    final int childTop = getPaddingTop();
    final int childWidth = width - getPaddingLeft() - getPaddingRight();
    final int childHeight = height - getPaddingTop() - getPaddingBottom();
    child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
    int circleWidth = mCircleView.getMeasuredWidth();
    int circleHeight = mCircleView.getMeasuredHeight();
    //關鍵的一個參數,mCurrentTargetOffsetTop決定著小圓球下拉過程中的高度
    mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
            (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
}

盜圖來顯示

View的事件分發

View的繪制過程中倆個最重要的方法找到了,那么下面就是下拉過程中的事件分發了。先介紹一下事件分發最關鍵的幾個點

  • dispatchTouchEvent(MotionEvent ev)
  • onInterceptTouchEvent(MotionEvent ev)
  • onTouchEvent(MotionEvent ev)
    這三者的關系我之前一直搞不清,最后在任玉剛的《Android開發藝術探索中》在看到下面這段代碼才算明白,這是一段偉大的代碼,簡單幾行,事件傳遞的奧義表達的淋漓盡致 。
public boolean dispatchTouchEvent(MotionEvent ev){
    boolean consume=false;
        if(onInterceptTouchEvent(ev)){
            consume=onTouchEvent(ev);
        }else{
            consume=child.dispatchTouchEvnet(ev);
        }
  return consume;
}

一個滑動事件傳過來,首先SwipRefreshLayout在onInterceptTouchEvent中決定要不要攔截當前事件,如果不攔截就分發給listview,如果攔截那么它的onTouchEvent會處理對應的事件.

onInterceptTouchEvent

public boolean onInterceptTouchEvent(MotionEvent ev) {
       ensureTarget();
       final int action = MotionEventCompat.getActionMasked(ev);
        ....
    一些邊際情況判斷
    ....    

       switch (action) {
           case MotionEvent.ACTION_DOWN:
               setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
                //多指觸摸的時候,獲取第一個
               mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
               mIsBeingDragged = false;
               final float initialDownY = getMotionEventY(ev, mActivePointerId);
               if (initialDownY == -1) {
                   return false;
               }
            //記錄下按下的位置,老套路了
               mInitialDownY = initialDownY;
               break;

           case MotionEvent.ACTION_MOVE:
               final float y = getMotionEventY(ev, mActivePointerId);
               if (y == -1) {
                   return false;
               }
            //最后的位置,和按下的位置獲取滑動距離yDiff
               final float yDiff = y - mInitialDownY;
            //滑動距離大于最小滑動距離時候攔截這個事件
               if (yDiff > mTouchSlop && !mIsBeingDragged) {
                   mInitialMotionY = mInitialDownY + mTouchSlop;
                   mIsBeingDragged = true;
                   mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
               }
               break;

       }

       return mIsBeingDragged;
   }

onTouchEvent

public boolean onTouchEvent(MotionEvent ev) {

    。。。
       各種情況判斷
       。。。
       switch (action) {
           case MotionEvent.ACTION_MOVE: {
               final float y = MotionEventCompat.getY(ev, pointerIndex);
                //關鍵的來了,move事件傳遞到這里,當前Y-初始位置,再乘以阻尼系數.5f,得到一個距離overscrollTop,傳到了moveSpinner中,那moveSpinner(overscrollTop)肯定就是觸發滑動的關鍵方法了
               final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
               if (mIsBeingDragged) {
                   if (overscrollTop > 0) {
                       moveSpinner(overscrollTop);
                   } else {
                       return false;
                   }
               }
               break;
           }
           case MotionEvent.ACTION_UP: {
               pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
               if (pointerIndex < 0) {
                   Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
                   return false;
               }

               final float y = MotionEventCompat.getY(ev, pointerIndex);
               final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
               mIsBeingDragged = false;
                //手指抬起來的時候,在finishSpinner中判斷要觸發刷新onRefresh還是顯示個動畫就彈回去
               finishSpinner(overscrollTop);
               mActivePointerId = INVALID_POINTER;
               return false;
           }
       }

       return true;
   }

moveSpinner

這個方法,在各種計算之后,設置mCircleView的scale和alpha,然后又設置了圓球中間的mProgress的角度,并沒有更新mCurrentTargetOffsetTop ,最后調用了setTargetOffsetTopAndBottom方法,接著看

private void moveSpinner(float overscrollTop) {

     // where 1.0f is a full circle
     if (mCircleView.getVisibility() != View.VISIBLE) {
         mCircleView.setVisibility(View.VISIBLE);
     }
     if (!mScale) {
         ViewCompat.setScaleX(mCircleView, 1f);
         ViewCompat.setScaleY(mCircleView, 1f);
     }

     float strokeStart = adjustedPercent * .8f;
     mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart));
     mProgress.setArrowScale(Math.min(1f, adjustedPercent));

     float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;
     mProgress.setProgressRotation(rotation);
     setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
 }

setTargetOffsetTopAndBottom

將mCircleView 顯示出來,設置offset,也就是會觸發mCircleView的ondraw,然后mCurrentTargetOffsetTop變量再次被賦值,如果api<11的時候手動觸發刷新,這樣下一次swiprefreshlayout執行 onmeasure和onlayout的時候,就知道circleview在哪里,多大。=""

private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) {
       mCircleView.bringToFront();
       mCircleView.offsetTopAndBottom(offset);
       mCurrentTargetOffsetTop = mCircleView.getTop();
       if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) {
           invalidate();
       }
   }

MotionEvent重復下去,mCurrentTargetOffsetTop不斷更新位置,SwipRefreshLayout不斷的draw,小圓球跟著手指移動的動畫就完成了。

那么如果要自己定制這樣的動畫,怎么做?

首先流程不變,circleView要換成自己要的View,moveSpinner方法要大改,子view根據overscrollTop,計算出百分百,阻尼,進度來等等數據一步一步的設置,ACTION_UP的時候,還有改造finishSpinner設置手指抬起的時候,View的顯示邏輯.這樣簡單的定制就完成了。

 

來自: http://xujinyang.github.io/2016/06/19/SwipeRefreshLayout要點詳解/

 

 本文由用戶 BriSchuster 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!