項目地址:TwinklingRefreshLayout 本文分析版本:0a9b613
1.簡介
以下是 TwinklingRefreshLayout 的官方介紹:
TwinklingRefreshLayout 延伸了 Google 的 SwipeRefreshLayout 的思想,不在列表控件上動刀,而是使用一個 ViewGroup 來包含列表控件,以保持其較低的耦合性和較高的通用性。其主要特性有:
- 支持 RecyclerView杏瞻、ScrollView调限、AbsListView 系列(ListView、GridView)矾屯、WebView 以及其它可以獲取到 scrollY 的控件
- 支持加載更多
- 默認支持 越界回彈,隨手勢速度有不同的效果
- 可開啟沒有刷新控件的純凈越界回彈模式
- setOnRefreshListener 中擁有大量可以回調(diào)的方法
- 將 Header 和 Footer 抽象成了接口,并回調(diào)了滑動過程中的系數(shù),方便實現(xiàn)個性化的 Header 和 Footer
2.使用方法
下面以 scrollView 為例子對下拉刷新和上拉加載來做分析
1初厚、在 XML 文件中聲明
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/inc_toolbar" />
<com.lcodecore.tkrefreshlayout.TwinklingRefreshLayout
android:id="@+id/refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#afbfff"
android:overScrollMode="never">
.
.
.
</ScrollView>
</com.lcodecore.tkrefreshlayout.TwinklingRefreshLayout>
</LinearLayout>
在 XML 文件中只要在滑動控件 ScrollView 外嵌套 TwinklingRefreshLayout 就可以了件蚕,TwinklingRefreshLayout 包含很多自定義的屬性在下面的分析中我們會看到。
2、在 JAVA 文件中設(shè)置回調(diào)
refreshLayout.setOnRefreshListener(new RefreshListenerAdapter() {
@Override
public void onRefresh(final TwinklingRefreshLayout refreshLayout) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
refreshLayout.finishRefreshing();
}
}, 4000);
}
});
TwinklingRefreshLayout 項目中給出了一個控件狀態(tài)的回調(diào)監(jiān)聽排作,上面的代碼中只監(jiān)聽了 onRefresh 的狀態(tài)牵啦,我們來看下 RefreshListenerAdapter 的實現(xiàn),看看有多少狀態(tài)纽绍。
public abstract class RefreshListenerAdapter implements PullListener {
@Override
public void onPullingDown(TwinklingRefreshLayout refreshLayout, float fraction) {
}
@Override
public void onPullingUp(TwinklingRefreshLayout refreshLayout, float fraction) {
}
@Override
public void onPullDownReleasing(TwinklingRefreshLayout refreshLayout, float fraction) {
}
@Override
public void onPullUpReleasing(TwinklingRefreshLayout refreshLayout, float fraction) {
}
@Override
public void onRefresh(TwinklingRefreshLayout refreshLayout) {
}
@Override
public void onLoadMore(TwinklingRefreshLayout refreshLayout) {
}
@Override
public void onFinishRefresh() {
}
@Override
public void onFinishLoadMore() {
}
}
RefreshListenerAdapter 是一個抽象類蕾久,實現(xiàn)了 PullListener 接口:
public interface PullListener {
/**
* 下拉中
*/
void onPullingDown(TwinklingRefreshLayout refreshLayout, float fraction);
/**
* 上拉
*/
void onPullingUp(TwinklingRefreshLayout refreshLayout, float fraction);
/**
* 下拉松開
*/
void onPullDownReleasing(TwinklingRefreshLayout refreshLayout, float fraction);
/**
* 上拉松開
*/
void onPullUpReleasing(TwinklingRefreshLayout refreshLayout, float fraction);
/**
* 刷新中。拌夏。僧著。
*/
void onRefresh(TwinklingRefreshLayout refreshLayout);
/**
* 加載更多中
*/
void onLoadMore(TwinklingRefreshLayout refreshLayout);
/**
* 手動調(diào)用finishRefresh或者finishLoadmore之后的回調(diào)
*/
void onFinishRefresh();
void onFinishLoadMore();
}
RefreshListenerAdapter 實現(xiàn)了 PullListener 接口變成一個抽象類,這讓使用者更加的靈活障簿,只關(guān)注使用到的狀態(tài)回調(diào)盹愚,而不需要所有的狀態(tài)回調(diào),抽象類可以做到這一點站故,只覆寫需要的函數(shù)皆怕,而接口不一樣,繼承了接口就必須覆寫接口內(nèi)部所有函數(shù)西篓。這一點很值得學(xué)習(xí)愈腾!
3、核心類 TwinklingRefreshLayout 的分析:
首先看一張官方的類解析圖:
通過圖中可以看到 TwinklingRefreshLayout 包含了一個 HeaderView 和一個 BottomView 岂津,這兩個 View 分別在上拉和下拉的時候顯示出來虱黄。其中內(nèi)部還包含了一個名字叫 CoProcessor 的類, CoProcessor 又包含三個 Processor ,我們來看下 CoProcessor 的構(gòu)造函數(shù)就明白了:
public class CoProcessor {
private RefreshProcessor refreshProcessor;
private OverScrollProcessor overScrollProcessor;
private AnimProcessor animProcessor;
public CoProcessor() {
animProcessor = new AnimProcessor(this);
overScrollProcessor = new OverScrollProcessor(this);
refreshProcessor = new RefreshProcessor(this);
}
......
}
CoProcessor
相當于是一個總的調(diào)度器吮成,把任務(wù)分配給三個不同的Processor
來處理橱乱,從名字上來看AnimProcessor
處理動畫,OverScrollProcessor
處理滑動中的越界回彈粱甫,RefreshProcessor
處理刷新動作泳叠。 CoProcessor
的初始化工作是在TwinklingRefreshLayout
的構(gòu)造函數(shù)中完成。接下來看一下CoProcessor
如何調(diào)度的茶宵,首先從攔截事件開始看:
/**
* 攔截事件
*
*
@return return true時,ViewGroup的事件有效,執(zhí)行onTouchEvent事件
* return false時,事件向下傳遞,onTouchEvent無效 */
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = cp.interceptTouchEvent(ev);
return intercept || super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
boolean resume = cp.consumeTouchEvent(e);
return resume || super.onTouchEvent(e);
}
cp.interceptTouchEvent(ev)
調(diào)用了refreshProcessor.interceptTouchEvent(ev)
危纫,來看下interceptTouchEvent
做了些什么事:
public boolean interceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mTouchX = ev.getX();
mTouchY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float dx = ev.getX() - mTouchX;
float dy = ev.getY() - mTouchY;
if (Math.abs(dx) <= Math.abs(dy)) {//滑動允許最大角度為45度
if (dy > 0 && !ScrollingUtil.canChildScrollUp(cp.getScrollableView()) && cp.allowPullDown()) {
cp.setStatePTD();
return true;
} else if (dy < 0 && !ScrollingUtil.canChildScrollDown(cp.getScrollableView()) && cp.allowPullUp()) {
cp.setStatePBU();
return true;
}
}
break;
}
return false;
}
ACTION_DOWN
產(chǎn)生時不攔截事件只記錄坐標,當ACTION_MOVE
產(chǎn)生的時候乌庶,計算移動距離dy
种蝶、ScrollingUtil.canChildScrollUp(cp.getScrollableView())
用來判斷子 View 是否還可以下拉,如果條件都滿足安拟,就調(diào)用cp.setStatePTD()
改變狀態(tài),并且返回true
表示攔截事件宵喂,接下去的事件就不下發(fā)給子View糠赦,直接交給onTouchEvent
來處理:
public boolean consumeTouchEvent(MotionEvent e) {
if (cp.isRefreshVisible() || cp.isLoadingVisible()) return false;
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dy = e.getY() - mTouchY;
if (cp.isStatePTD()) {
dy = Math.min(cp.getMaxHeadHeight() * 2, dy);
dy = Math.max(0, dy);
cp.getAnimProcessor().scrollHeadByMove(dy);
} else if (cp.isStatePBU()) {
//加載更多的動作
dy = Math.min(cp.getBottomHeight() * 2, Math.abs(dy));
dy = Math.max(0, dy);
cp.getAnimProcessor().scrollBottomByMove(dy);
}
return true;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (cp.isStatePTD()) {
cp.getAnimProcessor().dealPullDownRelease();
} else if (cp.isStatePBU()) {
cp.getAnimProcessor().dealPullUpRelease();
}
return true;
}
return false;
}
根據(jù)ACTION_MOVE
中的 if 條件計算出 dy 再通過 cp 來調(diào)試 AnimProcessor 做相應(yīng)的動畫,我們來看下scrollHeadByMove
函數(shù)是如何實現(xiàn)頭部的移動的:
public void scrollHeadByMove(float moveY) {
float offsetY = decelerateInterpolator.getInterpolation(moveY / cp.getMaxHeadHeight() / 2) * moveY / 2;
if (cp.getHeader().getVisibility() != VISIBLE) cp.getHeader().setVisibility(VISIBLE);
if (cp.isPureScrollModeOn()) cp.getHeader().setVisibility(GONE);
cp.getHeader().getLayoutParams().height = (int) Math.abs(offsetY);
cp.getHeader().requestLayout();
if (!cp.isOpenFloatRefresh()) {
cp.getContent().setTranslationY(offsetY);
translateExHead((int) offsetY);
}
cp.onPullingDown(offsetY);
}
根據(jù)cp.getAnimProcessor().scrollHeadByMove(dy)
之前的計算可以看出dy
的最大值是cp.getMaxHeadHeight()
的兩倍,所以scrollHeadByMove
函數(shù)中offsetY
的值最大應(yīng)該等于cp.getMaxHeadHeight()
拙泽,這就是為什么你再怎么拖動ScrollView
淌山,上面的HeadView
最多也就顯示這么點。接下去兩個if
判斷是否顯示HeaderView
顾瞻。
//如果沒有設(shè)置懸浮泼疑,那么需要手動對 ScrollView 做移動操作
//因為 ACTION_MOVE 已經(jīng)被 TwinklingRefreshLayout 攔截,ScrollView 無法響應(yīng) ACTION_MOVE荷荤。
if (!cp.isOpenFloatRefresh()) {
cp.getContent().setTranslationY(offsetY);
translateExHead((int) offsetY);
}
繼續(xù)回到consumeTouchEvent
函數(shù)中退渗,ACTION_MOVE
動作完了以后,產(chǎn)生ACTION_UP
調(diào)用cp.getAnimProcessor().dealPullDownRelease()
:
public void dealPullDownRelease() {
if (!cp.isPureScrollModeOn() && getVisibleHeadHeight() >= cp.getHeadHeight() - cp.getTouchSlop()) {
//下拉出的高度大于Head的高度就做刷新操作
animHeadToRefresh();
} else {
//下拉的距離不夠蕴纳,回彈動畫
animHeadBack();
}
}
執(zhí)行animHeadToRefresh
函數(shù)時對于HeadView
做了会油,回到正常高度動畫的操作。動畫完成后會調(diào)用pullListener.onRefresh(TwinklingRefreshLayout.this);
這就是我們在自己的 Activity 可以捕捉到的回調(diào)古毛。
animHeadBack
就是一個收起頭部的動畫翻翩。
至此,一次下拉刷新的操作算是完成了稻薇。上拉加載更多嫂冻,原理是一樣的就是方向變化了,有興趣的可以看下實現(xiàn)塞椎,這邊就不做重復(fù)的分析了桨仿。
目前還沒提到的就是OverScrollProcessor
這個處理器了,這個處理器是用來做越界回彈的忱屑,代碼比較少蹬敲,有興趣的同學(xué)可以自行分析。
4莺戒、總結(jié)
整個項目伴嗡,代碼邏輯很清晰,亮點在于把整個 ViewGroup 的操作从铲,按三個處理器分拆了瘪校,擴展性很好,可以很容易的去理解整個流程名段,這種分拆方式很值得學(xué)習(xí)阱扬!