android 开发中下拉刷新和加载更多有很多优秀的三方库,大多数三方库都是基于listview,gridview,scrollview做的扩展,而基于recyclerview实现的不多,如果你的项目中使用了pullTorefresh这种三方库,又想在使用recyclerview页面实现和它统一的刷新效果,这篇文章或许能帮到你。
先来看下效果图:
这个效果图展示的是staggerlayoutmanager下的效果,其他两个layoutManager下类似,这里就不贴图了。
整个实现是基于RecyclerView分栏显示和touch事件的处理。
RecyclerView分栏显示处理
使用过RecyclerView的小伙伴们对它都应该很熟悉了,这个案例中,分栏处理分为3个部分,分为头部下拉刷新,普通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交给子类去实现,在子类中你依然可以根据需求进行分栏处理。
这里有2点需要注意,分栏后普通item可能需要占据多个span,没有占据整个屏幕,而header和footer部分需要占据整个屏幕宽度。具体处理如下:
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部分是分为多个item一排时,会使用srollview嵌套recyclerView的方式处理,其实不需要,上面的代码完全可以解决你的需求。本人不喜欢嵌套这种方式,因为我不会 O(∩_∩)O~,并且计算高度会让性能下降。
第二个需要注意的地方是我们要在正确的位置将header部分的高度计算出来为后面的处理做准备,这里我选择在createheaderviewHolder的时候处理:
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高度到大于,放手,完成一次完整刷新过程。
这里应该需要添加一种情况,下拉距离从大于变为小于,不刷新,没写明白,放弃了。
在不同的头部状态调用changeWightState()方法,改变头部状态,这个方法中也是调用adapter提供的方法去改变状态。如果你实现了自己的头部效果,你可以在adapter中将setHeaderState()方法改写。
为了添加一个阻尼效果,我将手指距离除以了一个系数ranY,你可以自己调整。header部分实现扩大是通过设置padding实现的,通过调用如下方法:mAdapter.setHeaderPadding(currentDist)动态实现头部高度变化。手指放开,头部回缩时,为了让头部弹性的回归,我给他添加了一个属性动画效果:
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滑动监听的处理,没有什么难点,代码如下:
@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下最后一个可见的位置时,需要将保存在lastVisibleItemPositions[]中的所有数据进行比较,找出最大的那个位置。
ItemDecoration的处理
你可能需要普通item四周间隙一致,而头部和尾部没有空隙,就像文章开头的一样,你可能会在item布局中添加padding,并且在recyclerView中添加padding,这样是行不通的,你会发现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位置,而需要根据
StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
int spanIndex = lp.getSpanIndex();
来获取位置,spanIndex从左到右的值依次为0,1.... ,span-1;所以我们在设置outRect的左右间隙时,只需要关心最右边位置的view,将它左右设为space,而其他view只设置left为space,上下间隙只需要将bottom设置为span就可以了,这样上下左右间隙就一致了,当然这排除了头部和尾部。
使用方法
这些示例代码你只需要修改少许部分就能实现一个属于自己的封装抽取,当然自己实现也是很容易的。
- 修改header和footer布局文件,初始化布局文件中的控件,注意一点,header布局中最外层使用RelativeLayout,因为在inflate的时候没有将布局加入parent,会让头部布局在linelayoutmanager中变成wrapcontent。
- 在BaseRefreshRecyclerViewAdapter中修改setHeaderState()方法设置不同状态下header的state。修改setFooterRefreshFailState()方法,实现在加载更多失败后的状态。
- 最后在使用的时候,需要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个月,难免代码写的很渣,多担待,轻喷。
网友评论