自从RecyclerView出现之后,由于其灵活性受到越来越多人的青睐,其主要的作用就是显示类型一致的大量数据,这跟之前的ListView、GridView的作用是一致的。当数据量很大时,此时需要实现分页显示。针对ListView,有人开发了XListView来实现下拉刷新和上拉加载框架,效果不错,很受人欢迎。那么,RecyclerView是如何实现刷新功能呢?
RecyclerView与SwipeRefreshLayout组合,可以实现下拉刷新,这是谷歌支持的方法,但是上拉加载谷歌就没有对应的控件支持。通过对XListView的原理分析之后,我觉得可以完全按照XListView的原理来实现RecyclerView的下拉刷新和上拉加载,而且效果还是完全一致的。
我们先来看一下效果图。
下拉刷新 上拉加载更多 点击加载更多
首先,我们要实现上图的效果,前提要先理解XListView的原理。在这里我们感谢赵凯强博主给的原理分析:
http://blog.youkuaiyun.com/zhaokaiqiang1992/article/details/42392731
。在这里我们只对XListView的原理进行简单的概括。
XListView的主要原理实现:
XListView的主要原理实现:
1、XListView主要包含了三部分:XListView、XListViewFooter、XListViewHeader。
2、XListViewHeader包含了正常、准备刷新、正在加载三种状态。手在屏幕上滑动时根据手滑动的距离通过setVisiableHeight()方法设置Header的布局高度属性来达到拉伸和收缩的效果。
3、XListViewFooter跟XListView的差不多,只不过它是设置BottomMargin来实现而已。
4、XListViewHeader添加到头部,XlistViewFooter添加到尾部,总的item数目为内容主体的数目加上2(例如:原来item为20条,加上Header和Footer之后就是22)。整个过程的动画都是在这两个布局上改变的。
5、手在屏幕上滑动时,根据手指的移动距离,设置setVisiableHeight()改变Header的下拉变化,设置BottomMargin改变Footer的上拉效果。当手机离开屏幕时,调用Scroller.startScroll()来实现布局的回弹,同时调用更新或者加载更多的方法。
为了使RecyclerView具有XListView的一样的效果,同时减少工作量,我们在XListView的基础上修改,修改后的RecyclerView命名为XRecyclerView。
首先我们来确定要修改的部分:
1、不修改XListViewHeader和XListViewFooter的代码(源码100%原用),只将文件名修改成XRecyclerViewHeader和XRecyclerViewFooter。
2、重组RecyclerView的适配器,添加头尾布局到主体布局上。
3、根据需求处理滑动事件,使之更适合开发的需要。
4、在XRecyclerView中,唯独只有WrapAdapter是新增的,其他的部分大部分是保留XListView代码,同时只是修改个别逻辑。
添加XRecyclerViewHeader和XRecyclerViewFooter到主体布局
我们创建XRecyclerView类,继承RecyclerView。由于ListView与RecyclerView的适配机制不一样,我们不能按照XListView的方法添加headerView和footerView,所以我们通过重组适配器Adapter来实现XRecyclerView的头尾布局的添加。
同XListView一样,在XRecyclerView中的initView(),方法中实例化XRecyclerViewHeader和XRecyclerViewFooter,并获取Header的高度。代码如下:
private void initView(Context context) {
scroller = new Scroller(context, new DecelerateInterpolator());
//实例化Header和Footer
headerView = new XRecyclerViewHeader(context);
footerView = new XRecyclerViewFooter(context);
//获取Header的高度
headerViewContent = (RelativeLayout) headerView.findViewById(R.id.xlistview_header_content);
headerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@SuppressWarnings("deprecation")
@Override
public void onGlobalLayout() {
headerHeight = headerViewContent.getHeight();
getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
}
重写XRecyclerView中的setAdapter(Adapter adapter)方法,并在方法中重构适配器,重构适配器的类名为WrapAdapter,同时把实例化的headerView和footerView传入适配器WrapAdapter。代码如下:
@Override
public void setAdapter(Adapter adapter) {
mWrapAdapter = new WrapAdapter(this,headerView, footerView, adapter);
super.setAdapter(mWrapAdapter);
}
先看一下WraptAdapter类。
public class WrapAdapter extends RecyclerView.Adapter
{
private static final int TYPE_REFRESH_HEADER = -5; //添加刷新头
private static final int TYPE_NORMAL = 0;
private static final int TYPE_FOOTER = -3;//添加上拉加载布局
private RecyclerView.Adapter adapter;
private XRecyclerViewHeader mHeaderViews;
private XRecyclerViewFooter mFootView;
private XRecyclerView recyclerView;
public WrapAdapter(XRecyclerView recyclerView,XRecyclerViewHeader headerViews, XRecyclerViewFooter footView, RecyclerView.Adapter adapter) {
this.adapter = adapter;
this.mHeaderViews = headerViews;
this.mFootView = footView;
this.recyclerView = recyclerView;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_REFRESH_HEADER) {
return new SimpleViewHolder(mHeaderViews);
} else if (viewType == TYPE_FOOTER) {
return new SimpleViewHolder(mFootView);
}
return adapter.onCreateViewHolder(parent, viewType);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (isHeader(position)) {
return;
}
int adjPosition = position - 1;
int adapterCount;
if (adapter != null) {
adapterCount = adapter.getItemCount();
if (adjPosition < adapterCount) {
adapter.onBindViewHolder(holder, adjPosition);
return;
}
}
}
@Override
public int getItemCount() {
int count = 2;
if (adapter != null) {
count = 2 + adapter.getItemCount();
}
//判断是都为列表数为零.则隐藏头部和尾部的item
if(count == 2){
mHeaderViews.setVisibility(View.GONE);
mFootView.setVisibility(View.GONE);
} else {
mHeaderViews.setVisibility(View.VISIBLE);
//如果设置了加载更多为不可为时,隐藏尾部
if(recyclerView.isEnableLoadMore()){
mFootView.setVisibility(View.VISIBLE);
} else {
//如果设置了不能下拉刷新,则itemCount减少1
count--;
mFootView.setVisibility(View.GONE);
}
}
return count;
}
@Override
public int getItemViewType(int position) {
//如果position是0,返回刷新的标志位
if (position == 0) {
return TYPE_REFRESH_HEADER;
}
//如果position是footerView的位置时,返回加载更多的标志位
if (isFooter(position)) {
return TYPE_FOOTER;
}
//否则返回正常的标志位
int adjPosition = position - 1;
int adapterCount;
if (adapter != null) {
adapterCount = adapter.getItemCount();
if (adjPosition < adapterCount) {
return adapter.getItemViewType(adjPosition);
}
}
return TYPE_NORMAL;
}
@Override
public long getItemId(int position) {
if (adapter != null && position >= 1) {
int adjPosition = position - 1;
int adapterCount = adapter.getItemCount();
if (adjPosition < adapterCount) {
return adapter.getItemId(adjPosition);
}
}
return -1;
}
private class SimpleViewHolder extends RecyclerView.ViewHolder {
public SimpleViewHolder(View itemView) {
super(itemView);
}
}
public boolean isHeader(int position) {
return position >= 0 && position < 1;
}
public boolean isFooter(int position) {
return position < getItemCount() && position >= getItemCount() - 1 && recyclerView.isEnableLoadMore();
}
}
接下来我们对WraptAdapter进行分析。
getItemCount()方法:
@Override
public int getItemCount() {
int count = 2;
if (adapter != null) {
count = 2 + adapter.getItemCount();
}
//判断是都为列表数为零.则隐藏头部和尾部的item
if(count == 2){
mHeaderViews.setVisibility(View.GONE);
mFootView.setVisibility(View.GONE);
} else {
mHeaderViews.setVisibility(View.VISIBLE);
//如果设置了加载更多为不可为时,隐藏尾部
if(recyclerView.isEnableLoadMore()){
mFootView.setVisibility(View.VISIBLE);
} else {
//如果设置了不能下拉刷新,则itemCount减少1
count--;
mFootView.setVisibility(View.GONE);
}
}
return count;
}
通过代码我们发现,item的总数目是itemCount加上2(即count+2)。当count为2的时候,此时只是包含了头部和尾部的两个item,是没有主体数据显示,此时我们隐藏掉头部和尾部。如果数据count大于2时,是有主体数据显示,此时我们要判断我们的XRecyclerView是否设置上拉加载更多,如果没有设置,则隐藏footerView,count也要在count+2的基础上减去(即count + 1),此设置是为了适用于不需要上拉刷新的使用。
getItemViewType()方法:
@Override
public int getItemViewType(int position) {
//如果position是0,返回刷新的标志位
if (position == 0) {
return TYPE_REFRESH_HEADER;
}
//如果position是footerView的位置时,返回加载更多的标志位
if (isFooter(position)) {
return TYPE_FOOTER;
}
//否则返回正常的标志位
int adjPosition = position - 1;
int adapterCount;
if (adapter != null) {
adapterCount = adapter.getItemCount();
if (adjPosition < adapterCount) {
return adapter.getItemViewType(adjPosition);
}
}
return TYPE_NORMAL;
}
在这个方法,在header和footer位置处返回对应的标识,用于在onCreateViewHolder(ViewGroun parent,int viewType)识别,然后加载对应的布局,至此就将头尾布局添加到XRecyclerView中。
isHeader()和isFooter()两个方法是判断当前的item是否属于头部或者尾部的位置,尾部还特别判断了是否有开启加载更多的条件。
XRecyclerView上下滑动的动画实现
根据赵凯强博主的讲述中,我们知道XListView的上下滑动的动画的效果是根据手势的滑动距离来设定header布局的高度或者footer布局是与底部的距离来实现的,所以XRecyclerView的实现方法也是一样。在这里我们直接重写XRecyclerView的onTouchEvent(MotionEvent ev)方法。先贴上代码。
@Override
public boolean onTouchEvent(MotionEvent ev) {
LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
int totalItemCount = getLayoutManager().getItemCount();
//如果列表条数为零(放弃头尾两个item),则不处理
if(totalItemCount == 2){
return false;
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
isEvent = true;
// 记录按下的坐标
lastY = ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
// 计算移动距离
float deltaY = ev.getRawY() - lastY;
//这里的处理是为了防止与item的点击事件其冲突
if(!isEvent){
isEvent = true;
deltaY = 0;
}
lastY = ev.getRawY();
//有一个在加载数据的时候,另外一个不加载数据
if(!isRefreashing && !isLoadingMore) {
// 是第一项并且标题已经显示或者是在下拉
if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0 && (headerView.getVisiableHeight() > 0 || deltaY > 0) && enableRefresh) {
updateHeaderHeight(deltaY / OFFSET_RADIO);
} else if (layoutManager.findLastCompletelyVisibleItemPosition() == totalItemCount - 1 && (footerView.getBottomMargin() > 0 || deltaY < 0) && enableLoadMore) {
updateFooterHeight(-deltaY / OFFSET_RADIO);
}
}
break;
case MotionEvent.ACTION_UP:
isEvent = false;
if (layoutManager.findFirstVisibleItemPosition() == 0) {
if (enableRefresh && headerView.getVisiableHeight() > headerHeight) {
isRefreashing = true;
headerView.setState(XRecyclerViewHeader.STATE_REFRESHING);
if (mXRecyclerViewListener != null) {
mXRecyclerViewListener.onRefresh();
}
}
resetHeaderHeight();
} else if (layoutManager.findLastVisibleItemPosition() == totalItemCount - 1) {
if (enableLoadMore && footerView.getBottomMargin() > PULL_LOAD_MORE_DELTA) {
startLoadMore();
}
resetFooterHeight();
}
break;
}
return super.onTouchEvent(ev);
}
首先我们判断整个列表是否有数据,有数据才会对列表数据处理。当我们的手指点击到屏幕的时候,先记下当前点击的Y坐标,手指滑动的时候,记下滑动的距离,此距离是当前滑动触发的位置减去上一次触发的位置的距离差。if(isRefreshing && !isLoadingMore)是处理如果正在刷新操作或者加载更多数据的请求时,禁止触发加载数据或者刷新数据事件。
在满足无触发刷新或者加载数据的情况下,根据layoutManager.findFirstCompletelyVisibleItemPosition(),判断第一个完全可见的item是不是Position为0(即为头部布局),同时开启下拉刷新的功能时,调用updateHeaderHeight(deltay/OFFSET_RADIO)方法改变头部的得高度到达动态下来的效果。
private void updateHeaderHeight(float delta) {
headerView.setVisiableHeight((int) delta + headerView.getVisiableHeight());
// 未处于刷新状态,更新箭头
if (enableRefresh && !isRefreashing) {
if (headerView.getVisiableHeight() > headerHeight) {
headerView.setState(XRecyclerViewHeader.STATE_READY);
} else {
headerView.setState(XRecyclerViewHeader.STATE_NORMAL);
}
}
}
从该方法我们可以看到,如果下拉滑动到footerView高度的时候,会改变headerView的状态,变成刷新状态。
相同的,上拉的原理是类似的。但是有个注意点是我们这边用是findFirstCompletelyVisibleItemPosition()方法而不是findFirstVisibleItemPosition()方法,如果用的是后者的方法,会出现上拉或者下拉的时候,出现布局悬停的情况。
当手指放开的时候,先从headerView分析。根据滑动的距离和触发刷新条件来判断,如果条件不满足,则直接调用resetHeaderHeight()方法,实现headerView的回弹,当满足刷新的条件时候,则回弹到headerView触顶,同时触发刷新数据方法,数据刷新结束则回弹隐藏。
回弹代码:
private void resetHeaderHeight() {
// 当前的可见高度
int height = headerView.getVisiableHeight();
// 如果正在刷新并且高度没有完全展示
if ((isRefreashing && height <= headerHeight) || (height == 0)) {
return;
}
// 默认会回滚到header的位置
int finalHeight = 0;
// 如果是正在刷新状态,则回滚到header的高度
if (isRefreashing && height > headerHeight) {
finalHeight = headerHeight;
}
mScrollBack = SCROLLBACK_HEADER;
// 回滚到指定位置
scroller.startScroll(0, height, 0, finalHeight - height,
SCROLL_DURATION);
invalidate();
}
从代码上可以看出:当手指放开时,会根据刷新状态赋值给finalHeight,如果如果条件满足,则finalHeight设置为headerView的高度,否则设置为0,然后设置scroller的参数,并在computeScroll获取的currY()位置,然后设置headerView的高度,动态实现回弹。
在整个的手势滑动中,我们这边有一个标志位
isEvent,该表位为是为了防止在手点击到屏幕上时,item的点击事件与滑动事件冲突。
footerView的实现方式也是如出一辙。至此,我们主要原理已经分析了,更详细的请看源码。
本来想免费分享的,但是这边的资源分没有0的,只能设置最低的1分,如果没有积分的,请留下。
footerView的实现方式也是如出一辙。至此,我们主要原理已经分析了,更详细的请看源码。
本来想免费分享的,但是这边的资源分没有0的,只能设置最低的1分,如果没有积分的,请留下。