1. 程式人生 > >自定義一個更好用的SwipeRefreshLayout(彈力拉伸效果詳解)(轉載)

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

dsc drag 常數 lane swipe loading 數據改變 高中數學 tca

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

前言

熟悉SwipeRefreshLayout的同學一定知道,SwipeRefreshLayout是android裏面專為RecyclerView,NestedScrollView提供下拉刷新動畫的一個控件。可是在使用過程中有些局限性,例如只支持上述控件,不支持ListView,GridView等,另外下拉的動畫效果很難更改,而且不支持上拉加載……在很多場景的情況下往往不符合我們的需求。

今天為大家分享的是一個支持上拉下拉加載的控件,代碼並非純原創,改造自github作者baoyz的PullRefreshLayout

(有印象最早看到的側滑刪除好像也是他寫的),也參考了一些官方SwipeRefreshLayout的源碼和網上的一些資料,為了尊重原作者,我還是將其命名為——PullRefreshLayout。

效果如下:

技術分享

原理

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

拖拽彈力效果

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

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,就是彈力效果的關鍵啦

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

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

extraMove 的變化同tensionSlingshotPercent

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對函數圖像如下:

技術分享

其實看到這個圖,我想大家就基本上知道具體出來的效果了,再後面就是一些位移的操作,大家可以看文章最後面源碼,值得註意的是,之前都是分析scrollTop > 0 的情況,也就是下拉操作,上拉targetY要取負的,而且上拉下拉都是走這套邏輯,所以計算extraOS的時候scrollTop加上了絕對值

loading動畫

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

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

技術分享

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

技術分享

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位置

技術分享

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

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