自定義一個更好用的SwipeRefreshLayout(彈力拉伸效果詳解)

前言

熟悉SwipeRefreshLayout的同窗必定知道,SwipeRefreshLayout是android裏面專爲RecyclerView,NestedScrollView提供下拉刷新動畫的一個控件。但是在使用過程當中有些侷限性,例如只支持上述控件,不支持ListView,GridView等,另外下拉的動畫效果很難更改,並且不支持上拉加載……在不少場景的狀況下每每不符合咱們的需求。android

今天爲你們分享的是一個支持上拉下拉加載的控件,代碼並不是純原創,改造自github做者baoyz的PullRefreshLayout (有印象最先看到的側滑刪除好像也是他寫的),也參考了一些官方SwipeRefreshLayout的源碼和網上的一些資料,爲了尊重原做者,我仍是將其命名爲——PullRefreshLayout。git

效果以下:github

clipboard.png

原理

實際上是一個ViewGroup,經過對手勢的處理,使子控件實現拉動的動畫效果,並再加上兩個子控件,上拉的loading和下拉的loading(把loading用控件來封裝能夠很方便的更改動畫,真是貼心~),在處理手勢拉動的時候,通知他們顯示出對應的效果。代碼很長,有不少小細節須要注意,在這裏我只介紹幾個關鍵的位置,源代碼會發在文章的最後。ide

拖拽彈力效果

你們能夠看到,拖拽的時候,是有個彈力效果的,也就是說當拖拽的距離大於某個值,拖動的位移就會慢慢減少,最後會變得拖不動
看上去有點酷炫,其實實現起來就是高中數學知識啦,看下關鍵代碼函數

final float scrollTop = yDiff * DRAG_RATE;
float originalDragPercent = scrollTop / mTotalDragDistance;
mDragPercent = Math.min(1f, Math.abs(originalDragPercent));//拖動的百分比
float extraOS = Math.abs(scrollTop) - mTotalDragDistance;//彈簧效果的位移
float slingshotDist = mSpinnerFinalOffset;
//當彈簧效果位移小余0時,tensionSlingshotPercent爲0,不然取彈簧位移於總高度的比值,最大爲2
float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2) / slingshotDist);
//對稱軸爲tensionSlingshotPercent = 2的二次函數,0到2遞增
float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow((tensionSlingshotPercent / 4), 2)) * 2f;
float extraMove = (slingshotDist) * tensionPercent * 2;
targetY = (int) ((slingshotDist * mDragPercent) + extraMove);

解釋下幾個參數
yDiff —— 根據手勢算出的滑動位移
scrollTop —— yDiff乘上一個固定的比率(如今是0.5),能夠用來調節「彈簧」的彈性係數
mTotalDragDistance —— 當進度顯示100%時的位移
originalDragPercent —— 根據scrollTop與mTotalDragDistance的比值
mDragPercent —— 因爲originalDragPercent可能大於1,因此mDragPercent纔是拖動的百分比
slingshotDist —— 超過100%後能夠被容許拖動的最大距離的二分之一,也是一個常數(如今值 = mSpinnerFinalOffset = mTotalDragDistance)
extraMove —— 彈力距離
targetY —— 想要移動到的目標位置動畫

啊!? 被發現有兩個個參數沒解釋,哈哈,至於tensionSlingshotPercent和tensionPercent,就是彈力效果的關鍵啦this

extraOS 在scrollTop>=0時,是從-mTotalDragDistance開始線性遞增的,在scrollTop = mTotalDragDistance時,extraOS = 0spa

tensionSlingshotPercent 在scrollTop從0到mTotalDragDistance階段,始終爲0,在smTotalDragDistance到3*mTotalDragDistance階段,線性遞增,以後一直爲2code

extraMove 的變化同tensionSlingshotPercentorm

tensionPercent 是個二次函數,一樣映射到scrollTop的變化,在scrollTop從0到mTotalDragDistance階段,始終爲0,在mTotalDragDistance到3mTotalDragDistance階段,二次函數遞增,在3mTotalDragDistance以後恆爲0.5

而targetY,在scrollTop從0到mTotalDragDistance階段,也就是mDragPercent從0到1,extramMove始終爲0,而後二次函數遞增,在scrollTop > 3*mTotalDragDistance 變爲恆值

總結下來targetY相對於scrollTop對函數圖像以下:

clipboard.png

其實看到這個圖,我想你們就基本上知道具體出來的效果了,再後面就是一些位移的操做,你們能夠看文章最後面源碼,值得注意的是,以前都是分析scrollTop > 0 的狀況,也就是下拉操做,上拉targetY要取負的,並且上拉下拉都是走這套邏輯,因此計算extraOS的時候scrollTop加上了絕對值

loading動畫

在前文中咱們說過,loading效果實際上是交給兩個子控件完成的,這樣有利於更改loading的動畫效果。那麼,具體是怎麼實現的呢?

在代碼中咱們能夠看到以下幾個對象,其中mRefreshView和mLoadView就是咱們所說的loading控件,但它們只是一個容器,只控制顯隱,而具體的動畫實現是交給對應的mRefreshDrawable和mLoadDrawable;

clipboard.png

那咱們選取其中一個進行分析,在初始化函數中,能夠看到以下代碼

clipboard.png

setRefreshDrawable方法走進去,發現其實就是把一個Drawable對象賦給mRefreshView,那咱們來看一下傳入的參數PlaneDrawable,這個是我寫的那個火箭飛行的動畫效果,代碼很簡單,可是咱們發現PlaneDrawable是繼承了一個叫RefreshDrawable的類,對,它纔是將動畫效果解耦於PullRefreshLayout的關鍵、

咱們看下它的代碼

import android.content.Context;
import android.graphics.ColorFilter;
import android.graphics.PixelFormat;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;

/**
 * Created by baoyz on 14/10/29.
 */
public abstract class RefreshDrawable extends Drawable implements Drawable.Callback, Animatable {

private PullRefreshLayout mRefreshLayout;

public RefreshDrawable(Context context, PullRefreshLayout layout) {
    mRefreshLayout = layout;
}

public Context getContext(){
    return mRefreshLayout != null ? mRefreshLayout.getContext() : null;
}

public PullRefreshLayout getRefreshLayout(){
    return mRefreshLayout;
}

public abstract void setPercent(float percent);
public abstract void setColorSchemeColors(int[] colorSchemeColors);

public abstract void offsetTopAndBottom(int offset);

@Override
public void invalidateDrawable(Drawable who) {
    final Callback callback = getCallback();
    if (callback != null) {
        callback.invalidateDrawable(this);
    }
}

@Override
public void scheduleDrawable(Drawable who, Runnable what, long when) {
    final Callback callback = getCallback();
    if (callback != null) {
        callback.scheduleDrawable(this, what, when);
    }
}

@Override
public void unscheduleDrawable(Drawable who, Runnable what) {
    final Callback callback = getCallback();
    if (callback != null) {
        callback.unscheduleDrawable(this, what);
    }
}

@Override
public int getOpacity() {
    return PixelFormat.TRANSLUCENT;
}

@Override
public void setAlpha(int alpha) {

}

@Override
public void setColorFilter(ColorFilter cf) {

}

}

它是一個抽象類,抽象方法有

public abstract void setPercent(float percent);
public abstract void setColorSchemeColors(int[] colorSchemeColors);

public abstract void offsetTopAndBottom(int offset);

爲了代碼簡潔,主題顏色我沒有用,還剩下setPercent和offsetTopAndBottom,這兩個方法會分別在PullRefreshLayout裏面拉動的進度改變和被拉動目標控件位移變化時被調用。這樣,咱們想更改動畫效果就簡單了,直接寫一個類,繼承至RefreshDrawable,而後在對應的setPercent和offsetTopAndBottom裏面作出相應的動畫數據改變,就如PlaneDrawable那樣,而後再調用PullRefreshLayout的setRefreshDrawable或setLoadDrawable方法進行設值,是否是很方便?

同時顯示兩個動畫處理

在添加上拉加載效果時,我發現,假如你先下拉而後在不鬆手的狀況下再上拉,那就會同時出現兩個loading動畫,然而此時list還不在底部,也就是不該該顯示上拉loading效果的。這是因爲在拉動時,判斷子控件是否能夠向上滑動的那個方法會返回false,那麼如何解決這個問題呢?
用一個變量mLastDirection儲存本次動畫的,若是下次的動畫與本次不一樣,則不進行下次動畫,並在ACTION_UP和ACTION_CANCEL時,判斷被拉動目標控件的top位置

clipboard.png

好了,廢話很少說啦,源碼的地址https://github.com/SIdQi/Pull...

相關文章
相關標籤/搜索