Android-下拉重新整理(三)-RefreshLayout
時間推移,記得以前寫過關於下拉重新整理的部落格文章
現在回看程式碼,發現這種構建方式存在很大問題,當我專案中期突然需要一個ListView的帶有下拉重新整理功能,這時候,如果用以前的方式,就是用自定義的的RefreshListView去代替原生的ListView,這種體驗非常一般,因為需要修改很多程式碼,而且,針對每一個型別的View,都要去實現一個自定義View。
Google官方對於下拉重新整理自然早就有方案,在support library中加入了SwipeRefreshLayout佈局,當開發者需要下拉重新整理功能的時候,可以讓SwipeRefreshLayout包裹任意目標檢視,實現下拉效果。這時候我們真的體驗到SwipeRefreshLayout的好處,不需要我們去修改自己的業務邏輯程式碼,就能輕鬆為應用加上重新整理效果。於是,改進原來的體驗較差的重新整理庫的想法湧上心頭。
這篇部落格主要是分析SwipeRefreshLayout原理,以及根據原理依樣畫葫蘆,實現類似效果的下拉重新整理佈局。
SwipeRefreshLayout 原理
首先我們看到SwipeRefreshLayout的簡單實用:
<android.support.v4.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>
由於之前部落格的經驗,我們可以簡單總結下拉重新整理的關鍵步驟:
1 對於觸控事件的攔截並處理
2 對於能否繼續上滑或者下滑的判斷
我們看到SwipeRefreshLayout#onInterceptTouchEvent()方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
...
switch (action) {
case MotionEvent.ACTION_DOWN:
...
mIsBeingDragged = false;
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
return false;
}
/* 記錄手指按下的座標Y值 */
mInitialDownY = ev.getY(pointerIndex);
break;
case MotionEvent.ACTION_MOVE:
...
/* 記錄手指滑動的座標Y值 */
final float y = ev.getY(pointerIndex);
/* 通過這裡做判斷是否處於下拉狀態 */
startDragging(y);
break;
...
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
break;
}
return mIsBeingDragged;
}
我們看到關鍵變數mIsBeingDragged,這個變數用來標記當前是否正處於下拉操作,如果當前正在下拉,則攔截觸控事件。這裡主要是根據mIsBeingDragged的值,攔截觸控事件不讓事件傳遞到子檢視,直接交接觸控處理到onTouchEvent()方法裡處理。
@Override
public boolean onTouchEvent(MotionEvent ev) {
...
switch (action) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = ev.getPointerId(0);
mIsBeingDragged = false;
break;
case MotionEvent.ACTION_MOVE: {
...
final float y = ev.getY(pointerIndex);
/* 通過這裡做判斷是否處於下拉狀態 */
startDragging(y);
if (mIsBeingDragged) {
/* 計算手指滑動距離 */
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
if (overscrollTop > 0) {
moveSpinner(overscrollTop);
} else {
return false;
}
}
break;
}
...
case MotionEvent.ACTION_UP: {
...
if (mIsBeingDragged) {
final float y = ev.getY(pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
mIsBeingDragged = false;
finishSpinner(overscrollTop);
}
mActivePointerId = INVALID_POINTER;
return false;
}
case MotionEvent.ACTION_CANCEL:
return false;
}
return true;
}
當觸控事件為MotionEvent.ACTION_MOVE的時候,此時開始滑動的時候,startDragging()方法被持續呼叫,當手指的滑動超過系統規定的滑動距離mTouchSlop,設定當前狀態為下拉拖拽。
@SuppressLint("NewApi")
private void startDragging(float y) {
final float yDiff = y - mInitialDownY;
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mInitialMotionY = mInitialDownY + mTouchSlop;
mIsBeingDragged = true;
mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
}
}
繼續往下走,判斷mIsBeingDragged,此時為滑動狀態,計算當前滑動距離overscrollTop,如果滑動距離大於0,呼叫moveSpinner(overscrollTop)使得Spinner下拉並且旋轉。
if (mIsBeingDragged) {
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
if (overscrollTop > 0) {
moveSpinner(overscrollTop);
} else {
return false;
}
}
觸控事件的處理很簡單,流程主要是先通過onInterceptTouchEvent攔截,再onTouchEvent做下拉處理。
接下來是對於能否繼續上滑或者下滑的判斷。
我們看到SwipeRefreshLayout#canChildScrollUp()程式碼
/**
* @return Whether it is possible for the child view of this layout to
* scroll up. Override this if the child view is a custom view.
*/
public boolean canChildScrollUp() {
if (mChildScrollUpCallback != null) {
return mChildScrollUpCallback.canChildScrollUp(this, mTarget);
}
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mTarget, -1);
}
}
繼續跟蹤ViewCompat.canScrollVertically()
public static boolean canScrollVertically(View v, int direction) {
return IMPL.canScrollVertically(v, direction);
}
繼續跟蹤,我們發現API小於14的時候呼叫的是BaseViewCompatImpl#canScrollVertically()(低版本這裡就不討論了),API大於等於14的時候,呼叫的是ICSViewCompatImpl#canScrollVertically()
看到ICSViewCompatImpl#canScrollVertically()
@Override
public boolean canScrollVertically(View v, int direction) {
return ViewCompatICS.canScrollVertically(v, direction);
}
繼續看ViewCompatICS#canScrollVertically
public static boolean canScrollVertically(View v, int direction) {
return v.canScrollVertically(direction);
}
最後我們發現呼叫的是View#canScrollVertically()
/**
* Check if this view can be scrolled vertically in a certain direction.
*
* @param direction Negative to check scrolling up, positive to check scrolling down.
* @return true if this view can be scrolled in the specified direction, false otherwise.
*/
public boolean canScrollVertically(int direction) {
final int offset = computeVerticalScrollOffset();
final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range - 1;
}
}
其實當API到大於等於14的時候,如果判斷目標View是否可以繼續下拉,都是通過來View#canScrollVertically()判斷的,那麼為什麼通過這個方法就能實現對ListView, TextView, RecyclerView等View的適配呢?
我們發現computeVerticalScrollOffset(),computeVerticalScrollRange()這兩個方法,基本上都被重寫,通過重寫這兩個方法,實現了對檢視是否可下拉的判斷,詳細的可以檢視ListView或者RecyclerView等類的程式碼。
借鑑SwipeRefreshLayout實現下拉重新整理庫
基本瞭解了SwipeRefreshLayout的原理,我們可以模仿它,來實現一個帶有下拉重新整理以及上拉載入更多功能的佈局。
例如如何實現是否可下拉或者是否可上拉的判斷,這裡我們可以直接借鑑SwipeRefreshLayout的程式碼
/**
* 是否可以繼續下拉
* @param targetView
* @return
*/
public static boolean canChildPullDown(View targetView) {
/* 模仿SwipeRefreshLayout */
if (android.os.Build.VERSION.SDK_INT < 14) {
if (targetView instanceof AbsListView) {
final AbsListView absListView = (AbsListView) targetView;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(targetView, -1) || targetView.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(targetView, -1);
}
}
同時上拉也一樣,只需要把一些條件置反就行
/**
* 是否可以繼續上拉
* @param targetView
* @return
*/
public static boolean canChildPullUp(View targetView) {
/* 模仿SwipeRefreshLayout */
if (android.os.Build.VERSION.SDK_INT < 14) {
if (targetView instanceof AbsListView) {
final AbsListView absListView = (AbsListView) targetView;
return absListView.getChildCount() > 0
&& (absListView.getLastVisiblePosition() < absListView.getAdapter().getCount() - 1
|| absListView.getChildAt(absListView.getChildCount() - 1).getBottom() > absListView.getPaddingBottom());
} else {
return ViewCompat.canScrollVertically(targetView, 1);
}
} else {
return ViewCompat.canScrollVertically(targetView, 1);
}
}
其他具體的操作可以看專案原始碼。
下拉重新整理庫RefreshLayout介紹
如何引入
在build.gradle檔案中加入
compile 'com.zero.refreshlayout.library:RefreshLayout:1.0.0'
簡單使用
首先是xml程式碼:
<com.zero.refreshlayout.library.RefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text_view"
android:text="text"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.zero.refreshlayout.library.RefreshLayout>
在java程式碼設定相關的屬性,並且設定監聽器
mRefreshLayout = (RefreshLayout) findViewById(R.id.refresh_layout);
mRefreshLayout.setHeaderView(new TextHeaderView(this));
mRefreshLayout.setFooterView(new TextFooterView(this));
mRefreshLayout.setRefreshListener(new RefreshListener() {
@Override
public void onRefresh() {
MainThreadHandler.execute(new Runnable() {
@Override
public void run() {
mRefreshLayout.finishRefresh();
}
}, 2000);
}
@Override
public void onLoadMore() {
MainThreadHandler.execute(new Runnable() {
@Override
public void run() {
mRefreshLayout.finishLoadMore();
}
}, 2000);
}
});
更多的開放API:
- void finishRefresh()
結束下拉重新整理狀態- void finishLoadMore()
結束上拉載入狀態- void setHeaderViewHeight(int headerViewHeight)
設定HeaderView的高度,如果不設定的話,HeaderView預設為wrap_content- void setFooterViewHeight(int footerViewHeight)
設定FooterView的高度,如果不設定的話,FooterView預設為wrap_content- setHeaderViewMaxPullDistance(int headerViewMaxPullDistance)
設定下拉最大拉伸長度,如果不設定的話,預設為HeaderView高度的2.5倍- setFooterViewMaxPullDistance(int footerViewMaxPullDistance)
設定上拉最大拉伸長度,如果不設定的話,預設為FooterView高度的2.5倍- void setHeaderViewEnable(boolean headerViewEnable)
設定HeaderView是否可用- void setFooterViewEnable(boolean footerViewEnable)
設定FooterView是否可用
個性化使用
1)佈局樣式多樣化
如果你對檢視的佈局樣式不喜歡,這裡暫時提供了另外兩種佈局樣式。
如何切換佈局樣式,只需要呼叫
void setRefreshMode(RefreshMode refreshMode)
預設樣式 RefreshMode.LINEAR
mRefreshLayout.setRefreshMode(RefreshMode.LINEAR);
覆蓋樣式 RefreshMode.COVER_LAYOUT_FRONT (佈局覆蓋在下拉控制元件之前)
mRefreshLayout.setRefreshMode(RefreshMode.COVER_LAYOUT_FRONT);
覆蓋樣式 RefreshMode.COVER_LAYOUT_BEHIND (佈局被下拉控制元件覆蓋)
mRefreshLayout.setRefreshMode(RefreshMode.COVER_LAYOUT_BEHIND);
2) 定製HeaderView以及FooterView
最重要當然是自己定製HeaderView以及FooterView,通過下面簡單的例子我們就能輕鬆看懂
所有的HeaderView都必須繼承AbsHeaderView, 所有的FooterView都必須繼承AbsFooterView,我們看到,這裡實現了一個TextHeaderView類
public class TextHeaderView extends AbsHeaderView {
private TextView mTextView;
public TextHeaderView(Context context) {
super(context);
mTextView = new TextView(context);
mTextView.setBackgroundColor(0xFFFFFFFF);
mTextView.setTextColor(0xFF000000);
mTextView.setTextSize(20);
mTextView.setPadding(50, 50, 50, 50);
mTextView.setGravity(Gravity.CENTER);
mTextView.setText("下拉重新整理");
}
@Override
public View getContentView() {
return mTextView;
}
@Override
public void onPullDown(float fraction) {
mTextView.setText("下拉重新整理");
}
@Override
public void onFinishPullDown(float fraction) {
}
@Override
public void onRefreshing() {
mTextView.setText("重新整理...");
}
}
- AbsHeaderView#onPullDown
HeaderView正在被下拉時呼叫,fraction引數為HeaderView完全顯示的比例- AbsHeaderView#onFinishPullDown
HeaderView被鬆手彈回,fraction引數為HeaderView完全顯示的比例- AbsHeaderView#onRefreshing
HeaderView在處於重新整理狀態
3)預設提供的HeaderView,FooterView主題
如果我們只是想快速使用,可以使用現成的HeaderView以及FooterView, RefreshLayout暫時提供了幾個樣式
mRefreshLayout.setHeaderView(new BezierWaveHeader(this));
mRefreshLayout.setFooterView(new BezierWaveFooter(this));
mRefreshLayout.setHeaderView(new SquareSpreadHeader(this));
mRefreshLayout.setFooterView(new SquareSpreadFooter(this));
更新歷史
- v1.0.1
修復bug
學習不易,可能會有很多問題,歡迎拍磚