android 開發(fā)中下拉刷新和加載更多有很多優(yōu)秀的三方庫,大多數(shù)三方庫都是基于listview,gridview,scrollview做的擴(kuò)展,而基于recyclerview實(shí)現(xiàn)的不多,如果你的項(xiàng)目中使用了pullTorefresh這種三方庫,又想在使用recyclerview頁面實(shí)現(xiàn)和它統(tǒng)一的刷新效果,這篇文章或許能幫到你。
??先來看下效果圖:

??這個(gè)效果圖展示的是staggerlayoutmanager下的效果,其他兩個(gè)layoutManager下類似,這里就不貼圖了。
??整個(gè)實(shí)現(xiàn)是基于RecyclerView分欄顯示和touch事件的處理。
RecyclerView分欄顯示處理
使用過RecyclerView的小伙伴們對它都應(yīng)該很熟悉了,這個(gè)案例中,分欄處理分為3個(gè)部分,分為頭部下拉刷新,普通item,加載更多部分,具體代碼如下:
BaseRefreshRecyclerViewAdapter.java
@Override
public int getItemViewType(int position) {
if (position == 0) {
return VIEW_TYPE_REFRESH_HEADER;
} else if (position == 1 + data.size()) {
return VIEW_TYPE_REFRESH_FOOTER;
}
return VIEW_TYPE_ITEM;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
RecyclerView.ViewHolder viewHolder = null;
switch (viewType) {
case VIEW_TYPE_REFRESH_HEADER:
View headerView = View
.inflate(parent.getContext(), R.layout.view_refresh_header, null);
this.headerView = headerView;
viewHolder = new RefreshHeaderViewHolder(headerView);
break;
case VIEW_TYPE_ITEM:
viewHolder = onCreateItemViewHolder(parent);
break;
case VIEW_TYPE_REFRESH_FOOTER:
View footerView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.view_refresh_footer, parent, false);
viewHolder = new RefreshFooterViewHolder(footerView);
break;
}
return viewHolder;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
int itemViewType = getItemViewType(position);
switch (itemViewType) {
case VIEW_TYPE_ITEM:
onBindItemViewHolder(holder, position - 1);
break;
case VIEW_TYPE_REFRESH_HEADER:
prepareHeaderView(holder);
break;
case VIEW_TYPE_REFRESH_FOOTER:
prepareFooterView(holder);
break;
}
}
這段代碼功能相信大家都很熟悉,在這里,我將VIEW_TYPE_ITEM類型view具體的createViewholder和onBindViewHolder交給子類去實(shí)現(xiàn),在子類中你依然可以根據(jù)需求進(jìn)行分欄處理。
??這里有2點(diǎn)需要注意,分欄后普通item可能需要占據(jù)多個(gè)span,沒有占據(jù)整個(gè)屏幕,而header和footer部分需要占據(jù)整個(gè)屏幕寬度。具體處理如下:
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager != null && layoutManager instanceof GridLayoutManager) {
final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return getItemViewType(position) == VIEW_TYPE_REFRESH_HEADER || getItemViewType(position) == VIEW_TYPE_REFRESH_FOOTER
? gridLayoutManager.getSpanCount() : 1;
}
});
}
}
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
View itemView = holder.itemView;
ViewGroup.LayoutParams lp = itemView.getLayoutParams();
if (lp == null) {
return;
}
if (holder instanceof RefreshHeaderViewHolder || holder instanceof RefreshFooterViewHolder) {
if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {
StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
p.setFullSpan(true);
}
}
}
linelayoutmanager情況下不需要處理。這里順便提下,有的小伙伴遇到recyclerView需要添加頭部,普通item部分是分為多個(gè)item一排時(shí),會使用srollview嵌套recyclerView的方式處理,其實(shí)不需要,上面的代碼完全可以解決你的需求。本人不喜歡嵌套這種方式,因?yàn)槲也粫?O(∩_∩)O~,并且計(jì)算高度會讓性能下降。
??第二個(gè)需要注意的地方是我們要在正確的位置將header部分的高度計(jì)算出來為后面的處理做準(zhǔn)備,這里我選擇在createheaderviewHolder的時(shí)候處理:
headerView.post(new Runnable() {
@Override
public void run() {
headerViewMeasuredHeight = headerView.getMeasuredHeight();
setHeaderPadding();
}
});
Touch事件的處理
這部分處理是為了完成類似pulltorefresh的頭部刷新效果,所有的處理在BaseRefreshRecyclerView中完成,BaseRefreshRecyclerView繼承于RecyclerView。重寫dispatchTouchEvent方法。
public boolean dispatchTouchEvent(MotionEvent e) {
if (!refreshAble) {
return super.dispatchTouchEvent(e);
}
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
startY = e.getY();
headerRefreshHeight = mAdapter.getHeaderRefreshHeight();
break;
case MotionEvent.ACTION_MOVE:
if (currentState == STATE_LOADING) {
break;
}
float tmpY = e.getY();
if (currentState == STATE_PULL_TO_REFRESH) {
if ((tmpY - startY) / ranY <= this.headerRefreshHeight) {
currentDist = (int) ((tmpY - startY) / ranY);
mAdapter.setHeaderPadding((int) ((tmpY - startY) / ranY - this.headerRefreshHeight));
initAnimationHideHeader();
} else if (firstCompletelyVisibleItemPosition >= 0 && firstCompletelyVisibleItemPosition <= 1) {
currentState = STATE_RELASE_TO_REFRESH;
changeWightState();
}
}
if (currentState == STATE_RELASE_TO_REFRESH) {
changeWightState();
currentDist = (int) ((tmpY - startY) / ranY - this.headerRefreshHeight);
mAdapter.setHeaderPadding(currentDist);
}
break;
case MotionEvent.ACTION_UP:
if (currentState == STATE_LOADING) {
break;
}
if (currentState == STATE_PULL_TO_REFRESH) {
if (animator_hide_header == null) {
initAnimationHideHeader();
}
animator_hide_header.start();
}
if (currentState == STATE_RELASE_TO_REFRESH) {
currentState = STATE_LOADING;
changeWightState();
View view = getLayoutManager().getChildAt(0);
if (view.getTop() <= 5) {
onRefresh();
initAnimaionRelasetoRefresh();
} else {
currentDist = -view.getTop();
animator_hide_header.start();
currentState = STATE_PULL_TO_REFRESH;
}
}
break;
}
return super.onTouchEvent(e);
}
具體流程分為兩種情況:
?1.下拉距離小于header高度,手指放開,不刷新。
?2.下拉距離從小于header高度到大于,放手,完成一次完整刷新過程。
??這里應(yīng)該需要添加一種情況,下拉距離從大于變?yōu)樾∮?,不刷新,沒寫明白,放棄了。
??在不同的頭部狀態(tài)調(diào)用changeWightState()方法,改變頭部狀態(tài),這個(gè)方法中也是調(diào)用adapter提供的方法去改變狀態(tài)。如果你實(shí)現(xiàn)了自己的頭部效果,你可以在adapter中將setHeaderState()方法改寫。
??為了添加一個(gè)阻尼效果,我將手指距離除以了一個(gè)系數(shù)ranY,你可以自己調(diào)整。header部分實(shí)現(xiàn)擴(kuò)大是通過設(shè)置padding實(shí)現(xiàn)的,通過調(diào)用如下方法:mAdapter.setHeaderPadding(currentDist)動態(tài)實(shí)現(xiàn)頭部高度變化。手指放開,頭部回縮時(shí),為了讓頭部彈性的回歸,我給他添加了一個(gè)屬性動畫效果:
private void initAnimaionRelasetoRefresh() {
ValueAnimator animator_relase_torefresh = ValueAnimator.ofInt(currentDist, 0);
animator_relase_torefresh.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAdapter.setHeaderPadding((Integer) valueAnimator.getAnimatedValue());
}
});
animator_relase_torefresh.setDuration(400);
animator_relase_torefresh.start();
}
private void initAnimationRefreshOver() {
ValueAnimator animator_refresh_over = ValueAnimator.ofInt(0, -headerRefreshHeight);
animator_refresh_over.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAdapter.setHeaderPadding((Integer) valueAnimator.getAnimatedValue());
}
});
animator_refresh_over.setDuration(200);
animator_refresh_over.start();
}
private void initAnimationHideHeader() {
animator_hide_header = ValueAnimator.ofInt(-currentDist, -headerRefreshHeight);
animator_hide_header.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAdapter.setHeaderPadding((Integer) valueAnimator.getAnimatedValue());
}
});
animator_hide_header.setDuration(100);
}
這樣頭部回縮動作會顯得平滑。
加載更多
加載更多這部分基于對recyclerView滑動監(jiān)聽的處理,沒有什么難點(diǎn),代碼如下:
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
LayoutManager layoutManager = getLayoutManager();
int lastVisibleItemPosition = 0;
if (layoutManager instanceof LinearLayoutManager) {
lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
firstCompletelyVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstCompletelyVisibleItemPosition();
}
if (layoutManager instanceof GridLayoutManager) {
lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
firstCompletelyVisibleItemPosition = ((GridLayoutManager) layoutManager).findFirstCompletelyVisibleItemPosition();
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
int[] last = null;
int[] first = null;
if (!hasInit) {
last = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];
first = new int[last.length];
hasInit = true;
}
int[] lastVisibleItemPositions = ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(last);
int[] firstCompletelyVisibleItemPositions = ((StaggeredGridLayoutManager) layoutManager).findFirstCompletelyVisibleItemPositions(first);
firstCompletelyVisibleItemPosition = firstCompletelyVisibleItemPositions[0];
for (int i : lastVisibleItemPositions) {
lastVisibleItemPosition = i > lastVisibleItemPosition ? i : lastVisibleItemPosition;
}
}
if (lastVisibleItemPosition == mAdapter.getItemCount() - 1) {
mAdapter.setFooterVisible(true);
layoutManager.scrollToPosition(mAdapter.getItemCount());
onLoadMore();
}
}
在這里,需要注意的是在獲取StaggeredGridLayoutManager下最后一個(gè)可見的位置時(shí),需要將保存在lastVisibleItemPositions[]中的所有數(shù)據(jù)進(jìn)行比較,找出最大的那個(gè)位置。
ItemDecoration的處理
你可能需要普通item四周間隙一致,而頭部和尾部沒有空隙,就像文章開頭的一樣,你可能會在item布局中添加padding,并且在recyclerView中添加padding,這樣是行不通的,你會發(fā)現(xiàn)item間隙一致了,但是header部分也會有padding。解決方法就是自己繼承RecyclerView.ItemDecoration類,這里我的寫法如下:
@Override
public void getItemOffsets(Rect outRect, View view,
RecyclerView parent, RecyclerView.State state) {
// set header and footer space zero
outRect.bottom = space;
if (parent.getChildLayoutPosition(view) == 0
|| parent.getChildLayoutPosition(view) == parent.getAdapter().getItemCount()) {
outRect.top = 0;
outRect.left = 0;
outRect.right = 0;
} else if (parent.getLayoutManager() instanceof StaggeredGridLayoutManager) {
StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
int spanIndex = lp.getSpanIndex();
if (spanIndex == span-1) {
outRect.left = space;
outRect.right = space;
} else {
outRect.left = space;
outRect.right = 0;
}
} else if (parent.getLayoutManager() instanceof GridLayoutManager){
if (parent.getChildLayoutPosition(view) % span == 0) {
outRect.left = space;
outRect.right = space;
} else {
outRect.left = space;
outRect.right = 0;
}
}
}
這里需要注意的是在StaggeredGridLayoutManager中不能和GridLayoutManager中那樣通過parent.getChildLayoutPosition(view) % span來判斷item位置,而需要根據(jù)
StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
int spanIndex = lp.getSpanIndex();
來獲取位置,spanIndex從左到右的值依次為0,1.... ,span-1;所以我們在設(shè)置outRect的左右間隙時(shí),只需要關(guān)心最右邊位置的view,將它左右設(shè)為space,而其他view只設(shè)置left為space,上下間隙只需要將bottom設(shè)置為span就可以了,這樣上下左右間隙就一致了,當(dāng)然這排除了頭部和尾部。
使用方法
這些示例代碼你只需要修改少許部分就能實(shí)現(xiàn)一個(gè)屬于自己的封裝抽取,當(dāng)然自己實(shí)現(xiàn)也是很容易的。
??- 修改header和footer布局文件,初始化布局文件中的控件,注意一點(diǎn),header布局中最外層使用RelativeLayout,因?yàn)樵趇nflate的時(shí)候沒有將布局加入parent,會讓頭部布局在linelayoutmanager中變成wrapcontent。
??- 在BaseRefreshRecyclerViewAdapter中修改setHeaderState()方法設(shè)置不同狀態(tài)下header的state。修改setFooterRefreshFailState()方法,實(shí)現(xiàn)在加載更多失敗后的狀態(tài)。
??- 最后在使用的時(shí)候,需要adapter繼承BaseRefreshRecyclerViewAdapter。
使用示例:
final BaseRefreshRecyclerView rcv_test = (BaseRefreshRecyclerView) findViewById(R.id.rcv_test);
final TestRecyclerViewAdapter madapter = new TestRecyclerViewAdapter();
StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
rcv_test.setLayoutManager(staggeredGridLayoutManager);
rcv_test.addItemDecoration(new SimpleItemDecoration(20,3));
staggeredGridLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
ArrayList list = new ArrayList();
for (int i = 0; i < 15; i++) {
list.add(i);
}
madapter.setData(list);
rcv_test.setAdapter(madapter);
rcv_test.setOnRefreshAndLoadMoreListener(new BaseRefreshRecyclerView.OnRefreshAndLoadMoreListener() {
@Override
public void onRefresh() {
Toast.makeText(MainActivity.this, "Refreshing", Toast.LENGTH_SHORT).show();
rcv_test.postDelayed(new Runnable() {
@Override
public void run() {
rcv_test.completeRefresh();
}
}, 3000);
}
@Override
public void onLoadMore() {
rcv_test.postDelayed(new Runnable() {
@Override
public void run() {
List data = madapter.getData();
for (int i = 0; i < 10; i++) {
data.add(i * 1000);
}
madapter.setData(data);
madapter.notifyDataSetChanged();
rcv_test.completeLoadMore();
}
}, 3000);
}
});
完整代碼在這里。作者android菜鳥10個(gè)月,難免代碼寫的很渣,多擔(dān)待,輕噴。