轉(zhuǎn)載注明出處:http://www.itdecent.cn/p/4607129c9efa
1. 簡介
好長時間沒有寫博客了,一來是工作忙,抽不出空,二來是迷上了王者榮耀?,F(xiàn)在正好趕上項(xiàng)目空閑期,寫一篇關(guān)于下拉刷新的文章,個人覺得上來加載更多功能使用場景非常少,而且沒有必要做的那么麻煩,文章最后會提一下加載更多的實(shí)現(xiàn)。
最近項(xiàng)目中遇見了下拉刷新的需求,正好研究了一下,分享一下自己的心得。
主要參考文章或工程:
郭霖大神—Android下拉刷新完全解析,教你如何一分鐘實(shí)現(xiàn)下拉刷新功能
這三篇文章各自提供了實(shí)現(xiàn)下拉刷新的思路,文章會分別介紹這三種實(shí)現(xiàn)方式的優(yōu)劣。文章中會涉及到點(diǎn)擊事件分發(fā)知識,大家可以查看這篇文章Android事件分發(fā)機(jī)制詳解。自己寫對三種實(shí)現(xiàn)做了部分優(yōu)化,寫了demo,地址鏈接
2. 分析
下拉刷新主要分為兩部分,一部分是刷新頭部Header,一部分是內(nèi)容展示區(qū)域,一般是列表。通過某些方法,來控制刷新頭部Header的展示范圍,達(dá)到下拉刷新的效果,如下圖。

做下拉刷新之前,分析一下下拉刷新場景以及達(dá)到的效果,常見的下拉刷新最少有四種狀態(tài)
- 正常狀態(tài),下拉刷新頭部不展示,用戶可以正常操作列表
- 下拉狀態(tài),用戶下拉列表,但是沒有到達(dá)刷新時機(jī),松開手后,刷新頭部會自動隱藏
- 松開刷新狀態(tài),到達(dá)這個狀態(tài)時候,刷新頭部是完全展示的,用戶松開手,即可刷新,如果下拉距離過大,列表會自動上移,完整的露出刷新頭部,頭部顯示刷新中文案。
- 刷新中狀態(tài),請求數(shù)據(jù)的刷新態(tài),在這種狀態(tài)下,根據(jù)交互需求有不同的實(shí)現(xiàn)。
- 刷新中狀態(tài),用戶不能操作列表
- 刷新中狀態(tài),可以滑動和操作列表,但刷新頭部一直置頂
- 刷新中狀態(tài),可以滑動和操作列表,刷新頭部會隨著列表的滑動而一起滑動,參考欣慰微博下拉刷新。(大部分下拉刷新的交互效果)
前三種狀態(tài)會根據(jù)用戶手勢的移動相互切換,大部分下拉刷新中狀態(tài)交互是第三種,以新浪微博為參考藍(lán)本,本文最終實(shí)現(xiàn)的效果也是以這個效果為目標(biāo)。
3. 第一個例子鏈接
郭霖大神文章篇幅寫的比較多,很多可以不用關(guān)心,關(guān)于下拉刷新的核心代碼在ListView的OnTouchListener中,是通過修改Header的MarginTop值控制Header顯示可見范圍,到達(dá)下拉刷新的效果。缺點(diǎn)就是,每次更改Header的MarginTop值時候,會觸發(fā)父布局重新onMeasure()/onLayout()方法,如果ListView中Item內(nèi)容比較復(fù)雜,有卡頓現(xiàn)象,同時沒有處理刷新中狀態(tài)點(diǎn)擊事件,如果要處理,需要額外添加復(fù)雜的邏輯。
3.1 第一個例子實(shí)現(xiàn)過程
3.1.1 初始化Header
父布局中包含下拉刷新的Header和ListView,在父布局的構(gòu)造方法中實(shí)例化Header,并放入父布中。
public PtrFirstRefreshableView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* 實(shí)例化刷新頭部并將刷新頭部添加的父布局
*/
private void init() {
mHeader = new PtrFirstRefreshHeader(getContext());
touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
addView(mHeader, 0);
}
3.1.2 隱藏Header
在父布局中onLayout()方法中,設(shè)置Header的topMarigin,隱藏Header,設(shè)置ListView的點(diǎn)擊監(jiān)聽器,記錄一個標(biāo)簽isLayouted保證只設(shè)置一次。
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
// 如果是第一次Layout, 做一些設(shè)置
if (changed && !isLayouted) {
isLayouted = true;
// 設(shè)置刷新頭部MarginTop, 隱藏刷新頭部
mHeaderHeight = mHeader.getHeight();
mHeaderLayoutParams = (LayoutParams) mHeader.getLayoutParams();
mHeader.setTopMargin(-mHeaderHeight);
// 設(shè)置ListView的事件監(jiān)聽
mListView = (ListView) getChildAt(1);
mListView.setOnTouchListener(this);
}
}
3.1.3 處理點(diǎn)擊事件
這個地方邏輯復(fù)雜一些,獲取用戶點(diǎn)擊事件后,調(diào)用checkTopShow()方法檢查當(dāng)前是否需要處理點(diǎn)擊事件,如果ListView的第一個Item展示,且頂部距離父布局為0,則可以下拉刷新。
在DOWN事件中記錄用戶起始位置,注意一定要通過getRawY()獲取手指相對屏幕的位置,而不是通過getY()獲取手指相對ListView的位置,因?yàn)長istView會隨著手指滑動而滑動,如果用getY()獲取位置會有偏差。
在MOVE事件中,如果用戶手指向上滑動,且刷新頭部是完全隱藏的,不做處理;如果當(dāng)時非刷新中狀態(tài),根據(jù)頭部MarginTop的值更改當(dāng)前刷新狀態(tài),同時更改刷新頭部MarginTop。
在UP事件中,用戶松開手,如果當(dāng)前狀態(tài)是下拉狀態(tài),則隱藏刷新頭部;如果當(dāng)前狀態(tài)是松開刷新狀態(tài),則更改狀態(tài)為刷新中狀態(tài),同是隱藏多余margin,僅顯示完整的刷新頭部,同時調(diào)用回調(diào)監(jiān)聽(在RefreshingTask類中)。
在整個過程中,如果當(dāng)前狀態(tài)處于下拉狀態(tài)或者松開刷新狀態(tài),設(shè)置ListView屬性,讓ListView失去焦點(diǎn),否則那點(diǎn)擊Item會一直處于點(diǎn)擊狀態(tài)。
public boolean onTouch(View v, MotionEvent event) {
checkTopShow(event);
if (ableToPull) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownY = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
float yMove = event.getRawY();
int distance = (int) (yMove - mDownY);
// 如果手指向上滑動,并且下拉頭是完全隱藏的,不處理
if (distance <= 0 && mHeader.getTopMargin() <= -mHeaderHeight) {
return false;
}
if (distance < touchSlop) {
return false;
}
if (mStatus != STATUS_REFRESHING) {
if (mHeader.getTopMargin() > 0) {
mStatus = STATUS_RELEASE_TO_REFRESH;
} else {
mStatus = STATUS_PULL_TO_REFRESH;
}
// 通過偏移下拉頭的topMargin值,來實(shí)現(xiàn)下拉效果
int topMargin = (distance / 2) - mHeaderHeight;
mHeader.setTopMargin(topMargin);
// 更新刷新頭部圓環(huán)動畫
mHeader.updateCircle(Math.abs(distance * 1f / 2f / mHeaderHeight));
}
break;
case MotionEvent.ACTION_UP:
default:
if (mStatus == STATUS_RELEASE_TO_REFRESH) {
// 松手時如果是釋放立即刷新狀態(tài),就去調(diào)用正在刷新的任務(wù)
mStatus = STATUS_REFRESHING;
updateHeaderView();
new RefreshingTask().execute();
mHeader.startLoading();
} else if (mStatus == STATUS_PULL_TO_REFRESH) {
// 松手時如果是下拉狀態(tài),就去調(diào)用隱藏下拉頭的任務(wù)
mStatus = STATUS_NORMAL;
updateHeaderView();
new HideHeaderTask().execute();
}
break;
}
if (mStatus == STATUS_PULL_TO_REFRESH ||
mStatus == STATUS_RELEASE_TO_REFRESH) {
mListView.setPressed(false);
mListView.setFocusable(false);
mListView.setFocusableInTouchMode(false);
updateHeaderView();
return true;
}
}
return false;
}
private void checkTopShow(MotionEvent event) {
View firstChild = mListView.getChildAt(0);
if (firstChild != null) {
// 如果列表第一個item可見且距離ListView頂部為0,則說明ListView已經(jīng)到最頂部,此時可以下拉刷新
int firstVisiblePos = mListView.getFirstVisiblePosition();
if (firstVisiblePos == 0 && firstChild.getTop() == 0) {
ableToPull = true;
} else {
if (mHeader.getTopMargin() != -mHeaderHeight) {
mHeader.setTopMargin(-mHeaderHeight);
}
ableToPull = false;
}
} else {
ableToPull = true;
}
}
3.1.4 隱藏頭部
在用戶手指離開屏幕時候,會根據(jù)當(dāng)前狀態(tài)選擇是隱藏頭部還是僅展示頭部,僅以隱藏頭部為例,代碼如下。關(guān)于AsyncTask的使用可以查看AsyncTask 第一篇使用篇
class HideHeaderTask extends AsyncTask<Void, Integer, Integer> {
@Override
protected Integer doInBackground(Void... params) {
int topMargin = mHeaderLayoutParams.topMargin;
while (true) {
topMargin = topMargin + SCROLL_SPEED;
if (topMargin <= -mHeaderHeight) {
topMargin = -mHeaderHeight;
break;
}
publishProgress(topMargin);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return topMargin;
}
@Override
protected void onProgressUpdate(Integer... topMargin) {
mHeader.setTopMargin(topMargin[0]);
}
@Override
protected void onPostExecute(Integer topMargin) {
mHeader.setTopMargin(topMargin);
}
}
3.1.5 請求完畢恢復(fù)原狀態(tài)
網(wǎng)絡(luò)請求完成后,要隱藏刷新頭部,同時恢復(fù)原狀態(tài)。
public void finishRefreshing() {
mStatus = STATUS_NORMAL;
new HideHeaderTask().execute();
mHeader.stopLoading();
}
3.2 第一個例子實(shí)現(xiàn)效果
最終的實(shí)現(xiàn)效果如下:

3.3 第一個例子總結(jié)
該方案思路清晰,不需要對ListView進(jìn)行拓展。缺點(diǎn)也比較明顯,如果ListView中Item過于復(fù)雜,會有卡頓現(xiàn)象,而且代碼中并沒有對刷新中狀態(tài)的點(diǎn)擊事件進(jìn)行處理,如果在刷新中狀態(tài)中,滑動布局,會將刷新頭部隱藏,在完成請求之前,無法將頭部下拉展出,要對此進(jìn)行修復(fù),需要添加額外的邏輯。不推薦。
4. 第二個例子鏈接
原文章是使用scrollTo()/scrollBy()方法實(shí)現(xiàn)下拉刷新,默認(rèn)控件向上位移一段距離,正好將刷新頭部隱藏。然后根據(jù)用戶的手勢通過scrollBy()方法將刷新頭部逐漸展示出來。因?yàn)槭褂?code>scrollTo()/scrollBy()來移動控件,是移動父布局中所有的子控件,如果邏輯處理不當(dāng)會出現(xiàn)子控件部分移出父布局的情況,子控件顯示出現(xiàn)問題。原文章實(shí)現(xiàn)很簡單,下面實(shí)例的代碼是做過優(yōu)化后的代碼實(shí)現(xiàn)。
4.1 第二個例子實(shí)現(xiàn)過程
4.1.1 初始化Header
實(shí)例化Header,并將其添加至父布局。
public PtrSecondRefreshableView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mHeader = new PtrSecondRefreshHeader(context);
addView(mHeader, 0);
mScroller = new Scroller(getContext());
}
4.1.2 隱藏Header
在父布局的onMeasure()方法中測量子View的大小,在onLayout()方法中將刷新頭部向上偏移,達(dá)到隱藏Header效果。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 測量子View大小
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mLayoutContentHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child == mHeader) { // 如果是刷新頭部,向上偏移
child.layout(0, 0 - child.getMeasuredHeight(), child.getMeasuredWidth(), 0);
} else {
child.layout(0, mLayoutContentHeight, child.getMeasuredWidth(), mLayoutContentHeight + child.getMeasuredHeight());
mLayoutContentHeight += child.getMeasuredHeight();
}
}
}
4.1.3 攔截事件
在父布局的onTouchEvent中設(shè)置Header的可見范圍,所以用戶手勢在操作屏幕時候,在某些情況下父布局需要攔截點(diǎn)擊事件。
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercept = false;
if(mStatus == STATUS_REFRESHING) {
return false;
}
// 記錄此次觸摸事件的y坐標(biāo)
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercept = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (y > mLastMoveY) { // 下滑操作
View child = getChildAt(1);
if (child instanceof AdapterView) {
AdapterView adapterChild = (AdapterView) child;
// 判斷AbsListView是否已經(jīng)到達(dá)內(nèi)容最頂部(如果已經(jīng)到達(dá)最頂部,就攔截事件,自己處理滑動)
if (adapterChild.getFirstVisiblePosition() == 0
|| adapterChild.getChildAt(0).getTop() == 0) {
intercept = true;
}
}
}
break;
}
case MotionEvent.ACTION_UP: {
intercept = false;
break;
}
}
mLastMoveY = y;
return intercept;
}
4.1.4 處理點(diǎn)擊事件
重寫父布局的onTouchEvent(),根據(jù)用戶手勢做出相應(yīng)展示效果。
public boolean onTouchEvent(MotionEvent event) {
float nowY = event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mLastMoveY = nowY;
break;
case MotionEvent.ACTION_MOVE:
float distance = mLastMoveY - nowY;
if(distance < 0) { // 如果是向下滑動,移動子View
// 頭部沒有完全展示
if(Math.abs(getScrollY()) <= mHeader.getMeasuredHeight()) {
mStatus = STATUS_PULL_TO_REFRESH;
scrollBy(0, (int) (distance * DRAG_COEFFICIENT_NORMAL));
} else { // 頭部已經(jīng)完全展示
scrollBy(0, (int) (distance * DRAG_COEFFICIENT_LIMIT));
mHeader.updateText(R.string.ptr_refresh_release);
mStatus = STATUS_RELEASE_TO_REFRESH;
}
} else { // 如果是向上滑動,移動子View
if(getScrollY() < 0) {
scrollBy(0, (int) distance);
}
if(Math.abs(getScrollY()) <= mHeader.getMeasuredHeight()) {
mStatus = STATUS_PULL_TO_REFRESH;
mHeader.updateText(R.string.ptr_refresh_normal);
}
}
// 更新刷新頭部動畫
mHeader.updateCircle(Math.abs(getScrollY() * 1f/ mHeader.getMeasuredHeight()));
break;
case MotionEvent.ACTION_UP:
default:
if(mStatus == STATUS_RELEASE_TO_REFRESH) { // 用戶松開手后,如果是松開刷新狀態(tài),則回彈顯示完整Header,并刷新數(shù)據(jù)
mHeader.updateText(R.string.ptr_refresh_refreshing);
mHeader.startLoading();
mStatus = STATUS_REFRESHING;
// 刷新狀態(tài)回調(diào)
if(mListener != null) {
mListener.onRefresh();
}
mScroller.startScroll(0, getScrollY(), 0, -(getScrollY() + mHeader.getMeasuredHeight()), 200);
invalidate();
} else if(mStatus == STATUS_PULL_TO_REFRESH){ // 用戶松開手后,如果是下拉刷新狀態(tài),則隱藏Header
mScroller.startScroll(0, getScrollY(), 0, -getScrollY(), 200);
invalidate();
mHeader.updateText(R.string.ptr_refresh_normal);
mStatus = STATUS_NORMAL;
}
break;
}
mLastMoveY = nowY;
return true;
}
4.1.5 處理刷新中狀態(tài)
在onInterceptTouchEvent()方法中,如果是刷新中狀態(tài),攔截事件,會導(dǎo)致用戶無法操作ListView;如果不攔截事件,則事件會傳遞到ListView,這樣當(dāng)用戶滾動列表ListView時候,刷新頭部會一直懸浮在頂部。所以需要在dispatchTouchEvent()方法中處理刷新中狀態(tài)。
public boolean dispatchTouchEvent(MotionEvent event) {
int nowY = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastDownY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
if(mStatus == STATUS_REFRESHING) {
float distance = mLastDownY - nowY;
// 如果手勢向下滑動且列表中第一個Item可見,向下移動全部子View
if(distance < 0
&& mListView.getFirstVisiblePosition() == 0
&& mListView.getChildAt(0).getTop()==0) {
scrollBy(0, (int) (distance * DRAG_COEFFICIENT_LIMIT));
isListViewMove = true;
mLastDownY = nowY;
return true;
} else { // 如果手勢向上滑動
if(getScrollY() < 0) { // 當(dāng)Header沒有完全隱藏,移動全部子View;當(dāng)Header完全隱藏,將事件傳遞給ListView
if(getScrollY() + distance > 0) {
scrollBy(0, 0);
} else {
scrollBy(0, (int) distance);
}
mLastDownY = nowY;
isListViewMove = true;
return true;
}
}
}
break;
case MotionEvent.ACTION_UP:
default:
// 用戶抬起手,如果子View通過scrollBy移動過
if(isListViewMove) {
isListViewMove = false;
// 如果子View向下移動,向下移動距離大于Header高度,則自動回彈,顯示完整Header
if(getScrollY() < 0 && Math.abs(getScrollY()) > mHeader.getMeasuredHeight()) {
mScroller.startScroll(0, getScrollY(), 0, -(getScrollY() + mHeader.getMeasuredHeight()), 200);
invalidate();
}
return true;
}
isListViewMove = false;
break;
}
return super.dispatchTouchEvent(event);
}
4.1.6 請求完畢恢復(fù)原狀態(tài)
請求完成后,隱藏Header,恢復(fù)原狀態(tài)。
public void finishRefresh() {
if(!mScroller.isFinished()) {
mScroller.abortAnimation();
}
scrollTo(0, 0);
mHeader.updateText(R.string.ptr_refresh_normal);
mHeader.stopLoading();
mStatus = STATUS_NORMAL;
}
4.2 第二個例子最終實(shí)現(xiàn)效果
最終效果如下圖:

4.3 第二個例子總結(jié)
涉及點(diǎn)擊事件的三個方法dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()都有對點(diǎn)擊事件不同的處理邏輯。雖然能勉強(qiáng)到達(dá)文章開頭提到的效果,但是在零界點(diǎn),特別是刷新頭部Header剛好隱藏的零界點(diǎn),會有卡頓現(xiàn)象,加上處理邏輯比較復(fù)雜。不推薦。
5. 第三個例子鏈接
第三個例子是很久的開源項(xiàng)目,不同于前兩種實(shí)現(xiàn)方式,前面兩種都是自定義一個父布局,然后將刷新頭部和列表放入其中,第三個例子是直接將刷新頭部放在列表ListView的頭部,然后動態(tài)的設(shè)置刷新頭部的高度,達(dá)到下拉刷新的效果。
5.1 實(shí)現(xiàn)過程
5.1.1 初始化Header
在ListView的構(gòu)造函數(shù)中,初始化Heade作為ListView的HeaderView,這里有兩點(diǎn)要注意,一是Header的布局文件,因?yàn)橐獎討B(tài)設(shè)置Header的高度,所以布局文件需要嵌套一層,外面一層動態(tài)設(shè)置高度,里面一層包容所有的Header布局,高度不變;二是因?yàn)槌跏糎eader是不顯示的,想要獲取Header的真正高度,要在所有的View初始化以后才能獲取。
public PtrThirdRefreshableView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mScroller = new Scroller(context, new DecelerateInterpolator());
// 初始化Header,在初始化時候設(shè)置高度為0
mHeader = new PtrThirdRefreshHeader(context);
mHeaderContainer = mHeader.findViewById(R.id.dgp_header_container);
addHeaderView(mHeader);
mHeader.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// 獲取Header的完全展示時候的高度
getViewTreeObserver().removeGlobalOnLayoutListener(this);
mHeaderViewHeight = mHeaderContainer.getHeight();
mHeader.setContentHeight(mHeaderViewHeight);
}
});
}
5.1.2 處理點(diǎn)擊事件
在MOVE事件中,根據(jù)當(dāng)前狀態(tài),動態(tài)更新刷新Header的高度;在UP事件中根據(jù)當(dāng)前Header展示高度,來做相應(yīng)處理。
public boolean onTouchEvent(MotionEvent ev) {
if (mLastY == -1) {
mLastY = ev.getRawY();
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
final float deltaY = ev.getRawY() - mLastY;
mLastY = ev.getRawY();
// 如果列表中第一個Item是可見的, 且Header的部分可見或者向下滑動,則動態(tài)設(shè)置Header高度
if (getFirstVisiblePosition() == 0
&& (mHeader.getShowHeight() > 0 || deltaY > 0)) {
updateHeaderHeight(deltaY * OFFSET_RADIO);
return true;
}
break;
default:
mLastY = -1;
// 用戶松開手時候,如果列表第一個Item可以見
if (getFirstVisiblePosition() == 0) {
// 如果Header展示的高度大于Header的真正高度,則可刷新
if (mHeader.getShowHeight() > mHeaderViewHeight) {
mPullRefreshing = true;
mHeader.updateText(R.string.ptr_refresh_refreshing);
mHeader.startLoading();
if (mRefreshListener != null) {
mRefreshListener.onRefresh();
}
}
// 根據(jù)當(dāng)前情況重置Header高度
resetHeaderHeight();
return true;
}
break;
}
return super.onTouchEvent(ev);
}
/**
* 動態(tài)更新Header高度
*
* @param delta
*/
private void updateHeaderHeight(float delta) {
mHeader.setShowHeight((int) (delta + mHeader.getShowHeight()));
if (!mPullRefreshing) {
if (mHeader.getShowHeight() > mHeaderViewHeight) {
mHeader.updateText(R.string.ptr_refresh_release);
} else {
mHeader.updateText(R.string.ptr_refresh_normal);
}
}
setSelection(0);
}
5.1.3 請求完畢恢復(fù)原狀態(tài)
請求完畢后,恢復(fù)原狀態(tài),這里沒有使用設(shè)置Header的高度來隱藏Header,為了移動平滑通過Scroller將Header移動到屏幕外,不顯示在屏幕中,達(dá)到隱藏的目的。使用Scroller需要復(fù)寫computeScroll()方法,才能移動。
public void finishRefresh() {
if (mPullRefreshing == true) {
mPullRefreshing = false;
resetHeaderHeight();
mHeader.stopLoading();
}
}
private void resetHeaderHeight() {
int height = mHeader.getShowHeight();
if (height == 0)
return;
if (mPullRefreshing && height <= mHeaderViewHeight) {
return;
}
int finalHeight = 0;
// 如果當(dāng)前是刷新中狀態(tài),且Header的展示高度要大于Header的真實(shí)高度,則滑動列表,完整展示Header,否則隱藏Header
if (mPullRefreshing && height > mHeaderViewHeight) {
finalHeight = mHeaderViewHeight;
}
mScrollBack = SCROLL_BACK_HEADER;
mScroller.startScroll(0, height, 0, finalHeight - height,
SCROLL_DURATION);
invalidate();
}
/**
* 使用了Scroller, 需要復(fù)寫該方法
*/
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
if (mScrollBack == SCROLL_BACK_HEADER) {
mHeader.setShowHeight(mScroller.getCurrY());
}
postInvalidate();
}
super.computeScroll();
}
5.2 第三個例子最終實(shí)現(xiàn)效果
最終效果如下圖:

5.3 第三個例子總結(jié)
相比前兩種實(shí)現(xiàn)方法,第三種是最簡單最方便的實(shí)現(xiàn)方式,而且完全不用考慮刷新中狀態(tài)的點(diǎn)擊事件處理,唯一的缺點(diǎn)可能要對某些手機(jī)做一些適配,個人比較推薦。
6. 上拉加載
這三個例子中,后面兩個例子都實(shí)現(xiàn)了上拉加載更多,而在市面上大部分應(yīng)用沒有上拉加載,看得出在實(shí)際場景中,上拉加載更多的使用頻率不高。以大量使用列表的應(yīng)用新浪微博為例子,滑動到列表最下方,繼續(xù)向上拉時候不會像下拉刷新一樣,有一定拉伸的彈簧效果,而是直接在加載了。我猜測這種實(shí)現(xiàn)是在Adapter中,當(dāng)滑動到最后一個Item時候,直接返回一個加載中的View,同時請求數(shù)據(jù),當(dāng)用戶看見這個View時候,其實(shí)請求已經(jīng)發(fā)出去了(部分應(yīng)用是設(shè)置一個按鈕,然用戶手動點(diǎn)擊請求數(shù)據(jù))。
7. 總結(jié)
下拉刷新開源庫很多,上面列舉出的幾種實(shí)現(xiàn)可能不是最優(yōu)的,個人認(rèn)為最好的下拉刷新庫是這個下拉刷新庫,它基本支持所有的布局。但是在選擇使用哪一個開源庫的時候,并不是實(shí)現(xiàn)的最全最好的那個,而是最貼合實(shí)際業(yè)務(wù)的庫。
在做下拉刷新時候,因?yàn)闆]有去對這個功能做出具體分析,走了很多彎路,浪費(fèi)很多時間,以此為戒。
最后附上三個例子工程地址:https://github.com/Kyogirante/PtrDemo