RecyclerView
RecyclerView 与 ListView、GridView 类似,都是可以显示同一种类型 View 的集合的控件。
一.基本使用
RecyclerView的基本使用分四步:
1.build.gradle 文件中加入
compile 'com.android.support:recyclerview-v7:23.0.0'
2.创建RecyclerVIew对象
RecyclerView rv = (RecyclerView) findViewById(R.id.recyclerView);
3.设置布局管理器
LinearLayoutManager manager = new LinearLayoutManager(this,LinearLayout.HORIZONTAL,false);
rv.setLayoutManager(manager);
4.设置适配器
rv.setAdapter(adapter);
与ListView、GridView一样,我们需要为这些控件增加适配器用来设置每个item的显示内容,通常就是自定义一个适配器继承BaseAdapter,实现四个抽象方法,创建一个内部类ViewHolder,在getView() 方法中判断 convertView是否为空,创建还是获取viewholder对象。而RecyclerView也是类似的实现,首先自定义一个适配器类继承RecyclerView.Adapter类,实现三个抽象方法,创建一个内部类ViewHolder,必须继承RecyclerView.ViewHolder,另外,自定义适配器类继承时的泛型必须为此ViewHolder。
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder>{
private Context context;
private List<String> list;
public MyAdapter(Context context,List<String> list){
this.context = context;
this.list = list;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context)
.inflate(R.layout.recycler_view_item,parent,false);
ViewHolder holder = new ViewHolder(view);
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, final int position) {
holder.tv.setText(list.get(position));
}
@Override
public int getItemCount() {
return list.size();
}
class ViewHolder extends RecyclerView.ViewHolder{
ImageView iv;
TextView tv;
Button btn;
public ViewHolder(View itemView) {
super(itemView);
iv = (ImageView) itemView.findViewById(R.id.iv);
tv = (TextView) itemView.findViewById(R.id.tv);
btn = (Button) itemView.findViewById(R.id.btn);
}
}
}
二.布局管理
RecyclerView 将所有的显示规则交给一个叫 LayoutManager 的类去完成了。LayoutManager 是一个抽象类,系统已经为我们提供了三个默认的实现类,分别是 LinearLayoutManager、GridLayoutManager 、 StaggeredGridLayoutManager,从名字我们就能看出来了,分别是,线性显示、网格显示、瀑布流显示。当然你也可以通过继承这些类来扩展实现自己的LayougManager。
* LinearLayoutManager
LinearLayoutManager manager = new LinearLayoutManager(this,LinearLayout.HORIZONTAL,false);
创建一个线性布局管理器对象,第一个参数Context,第二个参数为设置布局方向,水平or竖直,第三个参数表示false表示正常显示,true表示倒序展示,并且初始展示为最后。
- GridLayoutManager
GridLayoutManager manager = new GridLayoutManager(this,3);
创建一个网格布局管理对象,第一个参数为Context,第二个参数为设置布局方向。
- StaggeredGridLayoutManager
StaggeredGridLayoutManager manager = new StaggeredGridLayoutManager(2,StaggeredGridLayoutManager.VERTICAL);
创建一个瀑布流布局管理对象,第一个参数为列数,第二个参数为设置布局方向。
主要实现瀑布流的方式为,在onBindViewHolder方法中给itemView赋于不同的高度:
@Override
public void onBindViewHolder(ViewHolder holder, final int position) {
ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
layoutParams.height = 200 + (position % 2) * 50;
holder.itemView.setLayoutParams(layoutParams);
}
三.添加与删除(动画)
同 ListView 每次修改了数据源后,都要调用 notifyDataSetChanged() 刷新每项 item 类似,只不过RecyclerView 还支持局部刷新 notifyItemInserted(index); notifyItemRemoved(position)、 notifyItemChanged(position)。在添加或删除了数据后,RecyclerView 还提供了一个默认的动画效果,来改变显示。同时,你也可以定制自己的动画效果,模仿 DefaultItemAnimator 或直接继承这个类,实现自己的动画效果,并调用recyclerview.setItemAnimator(new DefaultItemAnimator()); 设置上自己的动画(github上面有很多可参考)。 适配器中添加刷新数据方法如下:
/**
* 增加一项
* @param position
* @param str
*/
public void add(int position,String str){
list.add(position,str);
notifyItemInserted(position);
}
/**
* 删除一项
* @param position
*/
public void remove(int position){
list.remove(position);
notifyItemRemoved(position);
}
四.Click and LongClick
用习惯了 ListView 的 OnItemClickListener,RecyclerView的 OnItemClickListener 呢? 查看API后,只提供了OnItemTouchListener,点击事件需要我们自己定义添加,常见的方式有两种:
1. 通过mRecyclerView.addOnItemTouchListener去监听然后去判断手势
public abstract class RecyclerItemClickListener implements RecyclerView.OnItemTouchListener{
protected abstract void onItemClick(View view, int position);
protected abstract void onLongItemClick(View view,int position);
private GestureDetector mGestureDetector;
public RecyclerItemClickListener(Context context, final RecyclerView recyclerView) {
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapUp(MotionEvent e) {
View childView = recyclerView.findChildViewUnder(e.getX(),e.getY());
if(childView != null){
onItemClick(childView,recyclerView.getChildLayoutPosition(childView));
return true;
}
return false;
}
@Override
public void onLongPress(MotionEvent e) {
View childView = recyclerView.findChildViewUnder(e.getX(), e.getY());
if (childView != null) {
onLongItemClick(childView, recyclerView.getChildAdapterPosition(childView));
}
}
});
}
@Override
public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
//把事件交给GestureDetector处理
if(mGestureDetector.onTouchEvent(e))
return true;
return false;
}
@Override
public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) {
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
}
- 通过adapter中自己去提供回调方法
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder>{
private Context context;
private List<String> list;
public MyAdapter(Context context,List<String> list){
this.context = context;
this.list = list;
}
/**
* 增加一项
* @param position
* @param str
*/
public void add(int position,String str){
list.add(position,str);
notifyItemInserted(position);
}
/**
* 删除一项
* @param position
*/
public void remove(int position){
list.remove(position);
notifyItemRemoved(position);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context)
.inflate(R.layout.recycler_view_item,parent,false);
ViewHolder holder = new ViewHolder(view);
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, final int position) {
if (mOnItemClickLitener != null){
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mOnItemClickLitener.onItemClick(v,position);
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
mOnItemClickLitener.onItemLongClick(v,position);
return false;
}
});
}
}
@Override
public int getItemCount() {
return list.size();
}
class ViewHolder extends RecyclerView.ViewHolder{
ImageView iv;
public ViewHolder(View itemView) {
super(itemView);
iv = (ImageView) itemView.findViewById(R.id.iv);
}
}
public interface OnItemClickLitener {
void onItemClick(View view, int position);
void onItemLongClick(View view , int position);
}
private OnItemClickLitener mOnItemClickLitener;
public void setOnItemClickLitener(OnItemClickLitener mOnItemClickLitener) {
this.mOnItemClickLitener = mOnItemClickLitener;
}
}
五.定制分割线
在ListView中设置 divider 非常简单,只需要在XML文件中设置就可以了,同时还可以设置 divider 高度。
android:divider="@android:color/black"
android:dividerHeight="2dp"
而在RecyclerView里面,想实现这两种需求,稍微复杂一点,需要自己继承RecyclerView.ItemDecoration来实现想要实现的方法。这里封装了一个可以满足基本需求:
public class Divider extends RecyclerView.ItemDecoration{
private Drawable mDivider;
private int leftMargin, rightMargin, topMargin, bottomMargin;
private int width, height;
private int mOrientation;
public Divider(Drawable divider, int orientation) {
setDivider(divider);
setOrientation(orientation);
}
private void setDivider(Drawable divider) {
this.mDivider = divider;
if (mDivider == null) {
mDivider = new ColorDrawable(0xffff0000);
}
width = mDivider.getIntrinsicWidth();
height = mDivider.getIntrinsicHeight();
}
private void setOrientation(int orientation) {
if (orientation != LinearLayoutManager.HORIZONTAL && orientation != LinearLayoutManager.VERTICAL) {
throw new IllegalArgumentException("invalid orientation");
}
mOrientation = orientation;
}
public void setMargin(int left, int top, int right, int bottom) {
this.leftMargin = left;
this.topMargin = top;
this.rightMargin = right;
this.bottomMargin = bottom;
}
public void setHeight(int height) {
this.height = height;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public int getWidth() {
return width;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
if (mOrientation == LinearLayoutManager.HORIZONTAL) {
drawHorizontal(c, parent);
} else {
drawVertical(c, parent);
}
}
public void drawHorizontal(Canvas c, RecyclerView parent) {
final int top = parent.getPaddingTop() + topMargin;
final int bottom = parent.getHeight() - parent.getPaddingBottom() - bottomMargin;
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int left = child.getRight() + params.rightMargin + leftMargin;
final int right = left + width;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
public void drawVertical(Canvas c, RecyclerView parent) {
final int left = parent.getPaddingLeft() + leftMargin;
final int right = parent.getWidth() - parent.getPaddingRight() - rightMargin;
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin + topMargin;
final int bottom = top + height;
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);
if (mOrientation == LinearLayoutManager.HORIZONTAL) {
outRect.set(0, 0, leftMargin + width + rightMargin, 0);
} else {
outRect.set(0, 0, 0, topMargin + height + bottomMargin);
}
}
}
六.缓存与复用原理
RecyclerView 的内部维护了一个二级缓存,滑出界面的 ViewHolder 会暂时放到 cache 结构中,而从 cache 结构中移除的 ViewHolder,则会放到一个叫做 RecycledViewPool 的循环缓存池中。
顺带一说,RecycledView 的性能并不比 ListView 要好多少,它最大的优势在于其扩展性。但是有一点,在 RecycledView 内部的这个第二级缓存池 RecycledViewPool 是可以被多个 RecyclerView 共用的,这一点比起直接缓存 View 的 ListView 就要高明了很多,但也正是因为需要被多个 RecyclerView 公用,所以我们的 ViewHolder 必须继承自同一个基类(即RecyclerView.ViewHolder)。
默认的情况下,cache 缓存 2 个 holder,RecycledViewPool 缓存 5 个 holder。对于二级缓存池中的 holder 对象,会根据 viewType 进行分类,不同类型的 viewType 之间互不影响。
Recycler
一个超大型的缓存器,拥有三级缓存(如果算上创建的那一次,应该是四级了)
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
private ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private int mViewCacheMax = DEFAULT_CACHE_SIZE;
private RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
private static final int DEFAULT_CACHE_SIZE = 2;
- 第一级缓存
就是上面的一系列 mCachedViews。如果仍依赖于 RecyclerView (比如已经滑动出可视范围,但还没有被移除掉),但已经被标记移除的 ItemView 集合会被添加到 mAttachedScrap 中。然后如果 mAttachedScrap 中不再依赖时会被加入到 mCachedViews 中。 mChangedScrap 则是存储 notifXXX 方法时需要改变的 ViewHolder 。 - 第二级缓存
ViewCacheExtension 是一个抽象静态类,用于充当附加的缓存池,当 RecyclerView 从第一级缓存找不到需要的 View 时,将会从 ViewCacheExtension 中找。不过这个缓存是由开发者维护的,如果没有设置它,则不会启用。通常我们也不会去设置他,系统已经预先提供了两级缓存了,除非有特殊需求,比如要在调用系统的缓存池之前,返回一个特定的视图,才会用到他。 - 第三极缓存
最强大的缓存器。之前讲了,与 ListView 直接缓存 ItemView 不同,从上面代码里我们也能看到,RecyclerView 缓存的是 ViewHolder。而 ViewHolder 里面包含了一个 View 这也就是为什么在写 Adapter 的时候 必须继承一个固定的 ViewHolder 的原因。首先来看一下 RecycledViewPool:
public static class RecycledViewPool {
private SparseArray<ArrayList<ViewHolder>> mScrap =
new SparseArray<ArrayList<ViewHolder>>();
private SparseIntArray mMaxScrap = new SparseIntArray();
private int mAttachCount = 0;
private static final int DEFAULT_MAX_SCRAP = 5;
public void clear() {
mScrap.clear();
}
public void setMaxRecycledViews(int viewType, int max) {
mMaxScrap.put(viewType, max);
final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
if (scrapHeap != null) {
while (scrapHeap.size() > max) {
scrapHeap.remove(scrapHeap.size() - 1);
}
}
}
public ViewHolder getRecycledView(int viewType) {
final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
if (scrapHeap != null && !scrapHeap.isEmpty()) {
final int index = scrapHeap.size() - 1;
final ViewHolder scrap = scrapHeap.get(index);
scrapHeap.remove(index);
return scrap;
}
return null;
}
int size() {
int count = 0;
for (int i = 0; i < mScrap.size(); i ++) {
ArrayList<ViewHolder> viewHolders = mScrap.valueAt(i);
if (viewHolders != null) {
count += viewHolders.size();
}
}
return count;
}
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapHeapForType(viewType);
if (mMaxScrap.get(viewType) <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
从名字来看,他是一个缓存池,实现上,是通过一个默认为 5 大小的 ArrayList 实现的。这一点,同 ListView 的 RecyclerBin 这个类一样。然后每一个 ArrayList 又都是放在一个 Map 里面的,SparseArray 这个类我们在讲性能优化的时候已经多次提到了,就是两个数组,用来替代 Map 的。把所有的 ArrayList 放在一个 Map 里面,这也是 RecyclerView 最大的亮点,这样根据 itemType 来取不同的缓存 Holder,每一个 Holder 都有对应的缓存,而只需要为这些不同 RecyclerView 设置同一个 Pool 就可以了。