最近忙成狗...
前面都有文章寫(xiě)怎么去實(shí)現(xiàn)加載下拉刷新和加載更多,這篇文章會(huì)接著講,所以沒(méi)有看的,可以先看看喲。
Android 自定義View UC下拉刷新效果(一)
Android 自定義View UC下拉刷新效果(二)
Android 自定義View UC下拉刷新效果(三)

現(xiàn)在MD的設(shè)計(jì)風(fēng)格逐漸在被接受,Android 對(duì)應(yīng)的嵌套滑動(dòng)機(jī)制下的
CoordinatorLayout AppBarLayout CollapsingToolbarLayout 等都在被大量使用了。
其實(shí),我覺(jué)得 SwipeRefreshLayout 這種刷新效果就很好了,簡(jiǎn)約又不簡(jiǎn)單。一看就知道是 Android 的。但是,總會(huì)有老板或者產(chǎn)品要么是根本就不玩 Android 的,要么就是要標(biāo)新立異的,一定要弄一個(gè)怎樣怎樣炫酷的下拉刷新效果。那你怎么辦,吐完槽還是要擼代碼咯?,F(xiàn)在就業(yè)形勢(shì)這么嚴(yán)峻,你還敢不老實(shí)??哈哈,開(kāi)個(gè)玩笑。
所以說(shuō),支持嵌套滑動(dòng)的下拉刷新加載更多是迫在眉睫啊。
對(duì)比 iOS, Android 不是天然支持下拉刷新這種東西的,之前的文章已經(jīng)討論過(guò),現(xiàn)在的下拉刷新效果其實(shí)都是先把 Header 隱藏起來(lái),根據(jù)手勢(shì),將它有展示出來(lái)。前面說(shuō)過(guò)了通過(guò) margin,或者 translationY 的方式。今天講講另外一種方式,就是在 layout() 的時(shí)候控制它的擺放位置,然后通過(guò) scroll() 的方式控制其展示。
先看 onLayout() 的方法:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int contentHeight = 0;
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int childLeft = getPaddingLeft();
int childRight = getPaddingLeft();
int childTop = getPaddingTop();
int childWidth = width - childLeft - childRight;
int childHeight;
View child;
if (header != null) {
child = header;
headerHeight = child.getMeasuredHeight();
//藏起header
child.layout(childLeft, childTop - headerHeight, childLeft + childWidth, childTop);
}
//make sure there is the target!
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
child = mTarget;
childLeft = getPaddingLeft();
childTop = getPaddingTop();
childWidth = width - getPaddingLeft() - getPaddingRight();
childHeight = height - getPaddingTop() - getPaddingBottom();
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
contentHeight += child.getMeasuredHeight();
if (footer != null) {
child = footer;
footHeight = child.getMeasuredHeight();
child.layout(0, contentHeight, child.getMeasuredWidth(), contentHeight + footHeight);
}
// 滾動(dòng)到bottom
bottomScroll = contentHeight - getMeasuredHeight();
}
這樣之后,下拉刷新布局和加載更多的布局都隱藏起來(lái)了。因?yàn)槭且蛟鞂?zhuān)屬的下來(lái)刷新和加載更多嘛,所以,這里的 Header 和 Footer 都是咱們自己定義然后傳進(jìn)去的。 mTarget 顧名思義就是需要滾動(dòng)的的控件了,比如說(shuō)是 RecyclerView 或者巴拉巴拉了。
嵌套滑動(dòng)機(jī)制
之前都聊過(guò)這個(gè)了,大概意思就是,在這個(gè)之前, Android 的事件分發(fā)j簡(jiǎn)單來(lái)說(shuō)就是從 ViewGroup 到 View,如果 View 不消費(fèi),那么又返回給 ViewGroup 讓它消費(fèi)。這里就存在一個(gè)問(wèn)題:「餓死了爸爸,撐死了孩紙」。如果這個(gè)事件我們可以讓 View 它爸消費(fèi)一點(diǎn),剩下的很多都自己消費(fèi),這樣是不是最好了?所以,嵌套滑動(dòng)的機(jī)制就出現(xiàn)了。
這樣,在事件傳遞到 View 后,在 View 消費(fèi)事件之前,總會(huì)關(guān)心的問(wèn)問(wèn)它爸爸,你到底要來(lái)一點(diǎn)兒不?都是一家人,千萬(wàn)別客氣。然后父子倆就其樂(lè)融融,誰(shuí)都餓不死了。
這里就有了 NestedScrollingParent , NestedScrollingChild 這兩個(gè)接口??催@個(gè)名字都知道,這個(gè)就是爸爸和孩紙的關(guān)系嘛。
這里再來(lái)看我們要實(shí)現(xiàn)的刷新,它是 RecyclerView NestedScrollView
的爸爸,但是有時(shí) CoordinatorLayout 等的孩紙,所以,它就是又當(dāng)?shù)质呛⒓?,它孩紙的事件在?wèn)它需要先消費(fèi)點(diǎn)兒不的時(shí)候它作為孩紙也要按照慣例去問(wèn)問(wèn) CoordinatorLayout 它爸爸是不是要來(lái)一點(diǎn)兒,嗯,滿(mǎn)滿(mǎn)的和諧社會(huì)啊。好了,說(shuō)的這么形象了,應(yīng)該沒(méi)有誰(shuí)不懂吧。
還是先來(lái)看看 RecyclerView 的代碼吧。
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id " +
mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
//劃重點(diǎn),先問(wèn)問(wèn)爸爸要不要消費(fèi)
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
在自己消費(fèi)之前,先問(wèn)爸爸要不要消費(fèi)。如果消費(fèi)了,這個(gè)dispatchNestedPreScroll() 將會(huì)返回 true , 然后就將爸爸消費(fèi)了的值減出去,剩下就是自己可以使用的了。到這里,事件是到 RecyclerView 了 ,但是它壓根還沒(méi)有消費(fèi)任何事件。
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
到這里,RecyclerView 終于調(diào)用了 scrollByInternal() 開(kāi)始消費(fèi)事件啦。接著看看這個(gè)方法的詳細(xì)代碼。
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0, unconsumedY = 0;
int consumedX = 0, consumedY = 0;
consumePendingUpdateOperations();
...
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
....
return consumedX != 0 || consumedY != 0;
}
滑動(dòng)的還是,又得調(diào)用 dispatchNestedScroll() 方法來(lái)問(wèn)問(wèn)爸爸,我真的可以消費(fèi)了哇?然后事件有傳給了爸爸,爸爸如果消費(fèi)了,又返回 true,那么又需要將已經(jīng)消費(fèi)了減掉,然后才是自己可以消費(fèi)的了。
這說(shuō)明了啥? RecyclerView 真是個(gè)孝順聽(tīng)話(huà)的好孩紙啊,不過(guò)也好慘啊,到嘴的肉的被會(huì)被爸爸吃了,心疼它一秒鐘。
好了,到我們的下拉刷新這里,第一點(diǎn),我們也要當(dāng)孩紙,但是我們不能像 RecyclerView 那么聽(tīng)話(huà)孝順,我們的原則是只要事件孩紙傳遞給我了,我要優(yōu)先消費(fèi),而不是優(yōu)先去問(wèn)爸爸要不要消費(fèi),如果自己不能消費(fèi),或者已經(jīng)吃飽了,那么這個(gè)事件才去詢(xún)問(wèn)爸爸要不要消費(fèi)。
private final int[] mParentScrollConsumed = new int[2];
private final int[] mParentOffsetInWindow = new int[2];
private int mTotalUnconsumed;
private int mTotalUnconsumedLoadMore;
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
// If we are in the middle of consuming, a scroll, then we want to move the spinner back up
// before allowing the list to scroll
if (refreshEnable && header != null && !isRefreshing && currentStatus == STATE_REFRESH && dy > 0 && mTotalUnconsumed > 0) {
mTotalUnconsumed -= dy;
if (mTotalUnconsumed <= 0) {//over
mTotalUnconsumed = 0;
dy = (int) (-getScrollY() / DRAG_RATE);
}
goToRefresh(-dy);
consumed[1] = dy;
}
if (loadEnable && !isLoading && footer != null && dy < 0 && getScrollY() >= bottomScroll && mTotalUnconsumedLoadMore > 0 && currentStatus == STATE_LOADMORE) {
mTotalUnconsumedLoadMore += dy;
goToLoad(dy);
consumed[1] = dy;
}
// 最后,我們?cè)偃?wèn)問(wèn)爸爸需要消費(fèi)事件不。
final int[] parentConsumed = mParentScrollConsumed;
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}
}
請(qǐng)注意,上面這個(gè)都是針對(duì)在下拉后回退的情況(onNestedPreScroll()方法中的情況。),意思就是說(shuō),如果我現(xiàn)在 Header已經(jīng)拉出來(lái)了,然后又向上滑動(dòng),這個(gè)時(shí)候肯定要優(yōu)先先隱藏 Header,然后剩下的才讓CoordinatorLayout 等爸爸去消費(fèi)。
但是但是如果是在下來(lái)的過(guò)程呢?這個(gè)時(shí)候還是應(yīng)該先問(wèn)問(wèn)爸爸要不要消費(fèi),因?yàn)槟悴荒茉谙吕念^都出現(xiàn)完了之后再去把 AppBarLayout 等展開(kāi)吧,肯定是先先展開(kāi)它們,最后才是下來(lái)刷新。
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
// Dispatch up to the nested parent first
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
mParentOffsetInWindow);
final int dy = dyUnconsumed + mParentOffsetInWindow[1];
if (refreshEnable && header != null && dy < 0 && !isRefreshing && !canChildScrollUp()) {
mTotalUnconsumed += Math.abs(dy);
if (currentStatus == STATE_DEFAULT || mTotalUnconsumed != 0)
currentStatus = STATE_REFRESH;
goToRefresh(Math.abs(dy));
}
if (loadEnable && (isAutoLoad || footer != null) && getScrollY() >= bottomScroll && dy > 0 && !isLoading && mTotalUnconsumedLoadMore <= 4 * footHeight) {
mTotalUnconsumedLoadMore += dy;
if (currentStatus == STATE_DEFAULT || mTotalUnconsumedLoadMore != 0)
currentStatus = STATE_LOADMORE;
goToLoad(dy);
}
}
到這里,嵌套機(jī)制基本上就說(shuō)完了。對(duì)于加載更多,原理類(lèi)似,不在分析代碼。
具體滑動(dòng)
至于具體的滑動(dòng),就是上面的兩個(gè)方法所示,一個(gè) goToRefresh() 用于下拉,一個(gè) goToLoad() 用于加載更多,最后都是調(diào)用 scrollBy() 的方法。
狀態(tài)回調(diào)
因?yàn)?Header 和 Footer 都是傳入的嘛,所以在滑動(dòng)的過(guò)程中,需要把相關(guān)狀態(tài)通知到吧,然后就可以顯示出 下拉刷新、松開(kāi)刷新、正在刷新等狀態(tài)了吧。
public interface HeaderListener {
void onRefreshBefore(int scrollY, int headerHeight);
void onRefreshAfter(int scrollY, int headerHeight);
void onRefreshReady(int scrollY, int headerHeight);
void onRefreshing(int scrollY, int headerHeight);
void onRefreshComplete(int scrollY, int headerHeight, boolean isRefreshSuccess);
void onRefreshCancel(int scrollY, int headerHeight);
int getRefreshHeight();
}
這里來(lái)定義了 HeaderListener 的接口,這個(gè)就需要對(duì)應(yīng)的 Header 來(lái)實(shí)現(xiàn),然后就可以得到是是的狀態(tài)回調(diào)了。 getRefreshHeight() 該方法定義什么高度就可以執(zhí)行松手刷新了。系統(tǒng)默認(rèn)的是 Header 的高度。
布局
<com.lovejjfg.powerrefresh.PowerRefreshLayout
android:id="@+id/refresh_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.lovejjfg.demo.MainActivity">
</android.support.v7.widget.RecyclerView>
</com.lovejjfg.powerrefresh.PowerRefreshLayout>
相關(guān)方法調(diào)用
mRefreshLayout.setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh() {
mRefreshLayout.postDelayed(new Runnable() {
@Override
public void run() {
//刷新完畢
mRefreshLayout.stopRefresh(true);
}
}, 1000);
}
@Override
public void onLoadMore() {
mRefreshLayout.postDelayed(new Runnable() {
@Override
public void run() {
mRefreshLayout.stopLoadMore(true);
List<String> mlist = new ArrayList<>();
for (int i = 0; i < 10; i++) {
mlist.add("nice" + i);
}
mAdapter.appendList(mlist);
//是否還能加載更多
mRefreshLayout.setLoadEnable(mAdapter.getList().size() < 50);
}
}, 1000);
}
});
CircleHeaderView header = new CircleHeaderView(getContext());
FootView footView = new FootView(getContext());
//添加 header
mRefreshLayout.addHeader(header);
//添加 footer
mRefreshLayout.addFooter(footView);
如果進(jìn)入頁(yè)面需要顯示出下拉刷新,可以調(diào)用:
mRefreshLayout.setAutoRefresh(true);
如果不需要Footer,但是需要加載更多,可以調(diào)用:
mRefreshLayout.setAutoLoadMore(true);
刷新完畢Header會(huì)馬上隱藏上去,如果需要延遲的話(huà),可以調(diào)用:
mRefreshLayout.stopRefresh(true,5000);
一般項(xiàng)目的下拉刷新加載更多就是一套,每次都去調(diào)用 addHeader() 或者 addFooter() 就會(huì)很煩躁了,這時(shí)候你應(yīng)該考慮繼承自PowerRefreshLayout,將 Header 和 Footer 直接初始化好。
項(xiàng)目地址
辛苦擼了這么久,點(diǎn)個(gè)start 唄。
https://github.com/lovejjfg/PowerRefresh
