RecycleView出来已经有一两个年头了最近在项目中完全替换掉了ListView很有必要的写一篇记录一下使用过程,以便以后温故而知新。
RecycleView的使用场景开始到初始化
RecycleView可以用于展示列表式、网格式、瀑布流式风格的界面而且使用方便,可以这么来讲任何ListView能做到的功能RecycleView它也能做到而且能轻松驾驭比ListView更加强大的功能,从网上找到这么一句话来形容RecycleView再适合不过了。
由于尺寸限制,用户的设备不能一次性展现所有条目,用户需要上下滚动以查看更多条目。滚出可见区域的条目将被回收,并在下一个条目可见的时候被复用。
看一段初始化代码
mRecycleView =(RecyclerView)findViewById(R.id.id_recycleview);
LinearLayoutManager manager = new LinearLayoutManager(this);
mRecycleView.setLayoutManager(manager);
轻松定义了一个RecycleView然后我们传入一个RecyclerView.Adapter后那么一个列表式的界面就出来了。
public class AppRecycleAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Context mContext;
private List<AppBean> beans;
public AppRecycleAdapter(Context mContext, List<AppBean> beans) {
this.mContext = mContext;
this.beans = beans;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new AppViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item_layout, parent, false));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder _holder, int position) {
AppViewHolder holder = (AppViewHolder) _holder;
holder.imageView.setImageDrawable(beans.get(position).appIcon);
holder.textView.setText(beans.get(position).appName);
}
@Override
public int getItemCount() {
return beans.size();
}
public class AppViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;
TextView textView;
public AppViewHolder(View itemView) {
super(itemView);
imageView = (ImageView) itemView.findViewById(R.id.app_icon);
textView = (TextView) itemView.findViewById(R.id.app_name);
}
}
}
在RecyclerView.Adapter中我们需要去重写三个函数并且还需要顶一个ViewHolder,这个ListView中采用ViewHolder实现View复用在本质上是一个道理的。
AppRecycleAdapter adapter = new AppRecycleAdapter(this, beans);
mRecycleView.setAdapter(adapter);
跟ListView设置BaseAdapter一样,来不及想看下效果了:
如果你按照上面的写法你会发现那个淡蓝色的线条并不能显示,我当时就直接在RecycleView的声明xml文件中按照ListView那样声明了一个divider,当然结果并不是我们想象的那样因为RecycleView的divider并不能在xml文件中声明就可以的。
使用RecyclerView.ItemDecoration实现divider功能
看下下面这个方法,没错RecycleView就是通过ItemDecoration来画divider,没错是画出来的不是声明出来的。
mRecycleView.addItemDecoration(decoration);
这个类中我们通常可以去实现以下三个方法来实现各种divider效果:
// 通常用于画divider可以在itemView的左上右下随意画
onDraw(Canvas c, RecyclerView parent, State state)
// 通常用于画需要悬浮在itemView上的蒙层
onDrawOver(Canvas c, RecyclerView parent, State state)
// 类似padding可以理解为通过outRect来设置itemView的padding来标示我们divider或者蒙层画在哪、画多大
getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
上一段demo里面的代码可以更加深刻的理解这些意思。
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
drawVertical(c, parent);
}
private void drawVertical(Canvas c, RecyclerView parent) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int itemCount = parent.getChildCount();
for (int i = 0; i < itemCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
final int top = child.getBottom() + lp.bottomMargin;
final int bottom = top + dividerHeight;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.set(0, 0, 0, dividerHeight);// 设置这个ItemView的哪个方向的padding值。
}
onDraw这个方法会在item绘制时候回调,onDrawOver会在item绘制结束的时候回调,所以onDraw适合用于话divider而onDrawOver适合于画蒙层的效果。经过这么一步之后我们重新运行一下项目就会跟上面的图的效果一样出现了蓝色效果。
两种方式实现RecycleView的点击事件
第一种,采用OnItemTouchListener来实现点击事件。
public void addOnItemTouchListener(OnItemTouchListener listener) {
mOnItemTouchListeners.add(listener);
}
RecycleView通过这个方法来实现点击事件的,但是OnItemTouchListener需要实现的方法太多了,所以官方推荐使用SimpleOnItemTouchListener来实现。
public class AppClickListener extends RecyclerView.SimpleOnItemTouchListener {
private GestureDetectorCompat gestureDetector;
public interface ItemClickListener {
void onItemClick(View view, int position);
}
public AppClickListener(final RecyclerView recyclerView, final ItemClickListener listener) {
gestureDetector = new GestureDetectorCompat(recyclerView.getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapUp(MotionEvent e) {
View childView = recyclerView.findChildViewUnder(e.getX(), e.getY());
if (listener != null) {
listener.onItemClick(childView, recyclerView.getChildAdapterPosition(childView));
}
return true;
}
});
}
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
gestureDetector.onTouchEvent(e);
return super.onInterceptTouchEvent(rv, e);
}
}
我们通过实现GestureDetector.SimpleOnGestureListener中的onSingleTapUp方法拿到用户点击的MotionEvent的x、y轴的值然后通过recyclerView.findChildViewUnder来确定手指点中的是哪个view,有了view之后我们又通过recyclerView.getChildAdapterPosition就可以轻松知道点中的position位置了,最后是不是就可以想干嘛就干嘛了。
第二种,通过给itemView设置点击事件来处理。
直接给出实现过程:
public class AppViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;
TextView textView;
public AppViewHolder(View itemView) {
super(itemView);
imageView = (ImageView) itemView.findViewById(R.id.app_icon);
textView = (TextView) itemView.findViewById(R.id.app_name);
// 另一种设置点击事件..
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = getAdapterPosition();
Toast.makeText(mContext, beans.get(position).appName, Toast.LENGTH_SHORT).show();
}
});
}
}
没错我们就是直接写在ViewHolder中,通过getAdapterPosition这个方法来获取当前点中的position,只要我们拿到position后就什么都好办了。
其实很多觉得还有一种方式直接在RecycleView.Adapter的onBindViewHolder方法中给view设置点击事件,但是这有一个不好的效果每次复用的时候都得重新设置一个OnClickListener那么有上万个item不就有上个点击对象了,这样肯定是不行的,所以我们推崇上面这两种方式,第一种我们只需要new出一个对象通过x、y值来得到position,而第二种只需要new出一个屏幕item数量对象就好。相比之下肯定是设置OnItemTouchListener这种方式是为最佳的。
如何实现达到底部就加载更多
首先我们需要监听什么时候到达底部,没错RecycleView跟ListView一样有一个RecyclerView.OnScrollListener滚动事件我们可以监听到达底部了没。通过获取LayoutManager然后调用LayoutManager中的一系列方法进行比对,来判断是否到达了底部。
findFirstVisibleItemPosition() 返回当前第一个可见Item的position
findFirstCompletelyVisibleItemPosition() 返回当前第一个完全可见Item的position
findLastVisibleItemPosition() 返回当前最后一个可见Item的position
findLastCompletelyVisibleItemPosition() 返回当前最后一个完全可见Item的position
这边demo中采用的是一个优秀的开源库很方便的实现这些功能:
https://github.com/cundong/HeaderAndFooterRecyclerView
这边坐下这个库的源码解读,首先看下如何监听到达底部的实现,把主要代码抠出来其他的代码删除。
public class EndlessRecyclerOnScrollListener extends RecyclerView.OnScrollListener implements OnListLoadNextPageListener {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManagerType == null) {
if (layoutManager instanceof LinearLayoutManager) {
layoutManagerType = LayoutManagerType.LinearLayout;
}
}
switch (layoutManagerType) {
case LinearLayout:
lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
break;
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
currentScrollState = newState;
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
int visibleItemCount = layoutManager.getChildCount();
int totalItemCount = layoutManager.getItemCount();
if ((visibleItemCount > 0 && currentScrollState == RecyclerView.SCROLL_STATE_IDLE && (lastVisibleItemPosition) >= totalItemCount - 1)) {
onLoadNextPage(recyclerView);
}
}
}
在onScroll方法中通过得到的LayoutManager从而得到当前屏幕可见的最后一项的position,然后在onScrollStateChanged方法中进行比对是否满足到达底部的要求,这个类看起来还是比较简单和明了的,主要是运用上面列出来的LayoutManager的api方法。
除了EndlessRecyclerOnScrollListener这个类外还有一个HeaderAndFooterRecyclerViewAdapter也是主要的类,里面主要涉及通知显示的Adapter进行数据刷新,通过setAdapter这个方法把我们需要显示的adapter跟HeaderAndFooterRecyclerViewAdapter这个类进行绑定起来。
public void setAdapter(RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
if (adapter != null) {
if (!(adapter instanceof RecyclerView.Adapter))
throw new RuntimeException("your adapter must be a RecyclerView.Adapter");
}
if (mInnerAdapter != null) {
notifyItemRangeRemoved(getHeaderViewsCount(), mInnerAdapter.getItemCount());
mInnerAdapter.unregisterAdapterDataObserver(mDataObserver);
}
this.mInnerAdapter = adapter;
mInnerAdapter.registerAdapterDataObserver(mDataObserver);
notifyItemRangeInserted(getHeaderViewsCount(), mInnerAdapter.getItemCount());
}
然后通过getItemViewType来区分如果是底部的话就显示加载更多布局。
public int getItemViewType(int position) {
int innerCount = mInnerAdapter.getItemCount();
int headerViewsCountCount = getHeaderViewsCount();
if (position < headerViewsCountCount) {
return TYPE_HEADER_VIEW + position;
} else if (headerViewsCountCount <= position && position < headerViewsCountCount + innerCount) {
int innerItemViewType = mInnerAdapter.getItemViewType(position - headerViewsCountCount);
if(innerItemViewType >= Integer.MAX_VALUE / 2) {
throw new IllegalArgumentException("your adapter's return value of getViewTypeCount() must < Integer.MAX_VALUE / 2");
}
return innerItemViewType + Integer.MAX_VALUE / 2;
} else {
return TYPE_FOOTER_VIEW + position - headerViewsCountCount - innerCount;
}
}
这些还是比较好理解的,那么如何控制显示呢?可以从代码调用处看起然后一行一行点进去查看源码就能理解了。
AppRecycleAdapter adapter = new AppRecycleAdapter(this, beans);
mAdapter = new HeaderAndFooterRecyclerViewAdapter(adapter);
mRecycleView.setAdapter(mAdapter);
注意setAdapter中写入的不是我们自己写的Adapter而是HeaderAndFooterRecyclerViewAdapter通过传入我们自己定义的aapter后实现通过HeaderAndFooterRecyclerViewAdapter来控制adapter了。
RecyclerViewStateUtils.setFooterViewState(this, mRecycleView, INDEX, LoadingFooter.State.Normal, null);
通过这个类来写入一个FootView,点进去看下这个方法的实现:
public static void setFooterViewState(Activity instance, RecyclerView recyclerView, int pageSize, LoadingFooter.State state, View.OnClickListener errorListener) {
if(instance==null || instance.isFinishing()) {
return;
}
RecyclerView.Adapter outerAdapter = recyclerView.getAdapter();
if (outerAdapter == null || !(outerAdapter instanceof HeaderAndFooterRecyclerViewAdapter)) {
return;
}
HeaderAndFooterRecyclerViewAdapter headerAndFooterAdapter = (HeaderAndFooterRecyclerViewAdapter) outerAdapter;
//只有一页的时候,就别加什么FooterView了
if (headerAndFooterAdapter.getInnerAdapter().getItemCount() < pageSize) {
return;
}
LoadingFooter footerView;
//已经有footerView了
if (headerAndFooterAdapter.getFooterViewsCount() > 0) {
footerView = (LoadingFooter) headerAndFooterAdapter.getFooterView();
footerView.setState(state);
if (state == LoadingFooter.State.NetWorkError) {
footerView.setOnClickListener(errorListener);
}
} else {
footerView = new LoadingFooter(instance);
footerView.setState(state);
if (state == LoadingFooter.State.NetWorkError) {
footerView.setOnClickListener(errorListener);
}
headerAndFooterAdapter.addFooterView(footerView);
}
}
其实最后就是通过addFooterView这个方法给HeaderAndFooterRecyclerViewAdapter写入一个FootView,其实这个FootView就是LoadingFooter控件,通过传入State来控制是显示加载中、加载完成、加载失败等种种效果。
总的来说,通过给HeaderAndFooterRecyclerViewAdapter绑定我们的Apdater后写入FootView,然后通过RecyclerViewStateUtils控制显示底部。
下拉刷新
下拉刷新demo中采用的是SwipeRefreshLayout一个Android自带的下拉刷新效果,网上也有很多优秀的开源库例如:
https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh
就可以实现很多炫酷的下拉刷新效果。
项目地址:https://github.com/Neacy/EffectiveRecycleView
Thanks:
http://www.jianshu.com/p/16712681731e
http://blog.piasy.com/2016/03/26/Insight-Android-RecyclerView-ItemDecoration/
—–The End….