Android 控件 RecyclerView(转载)
概述
•RecyclerView是什么
从Android 5.0开始,谷歌公司推出了一个用于大量数据展示的新控件RecylerView,可以用来代替传统的ListView,更加强大和灵活。
RecyclerView的官方定义如下:
A flexible view for providing a limited window into a large data set.
从定义可以看出,flexible(可扩展性)是RecyclerView的特点。
RecyclerView是support-v7包中的新组件,是一个强大的滑动组件;
与经典的ListView相比,同样拥有item回收复用的功能,这一点从它的名字Recyclerview即回收view也可以看出。
•RecyclerView的优点
RecyclerView并不会完全替代ListView(这点从ListView没有被标记为@Deprecated可以看出),两者的使用场景不一样。
但是RecyclerView的出现会让很多开源项目被废弃,例如横向滚动的ListView, 横向滚动的GridView, 瀑布流控件,因为RecyclerView能够实现所有这些功能。
比如:有一个需求是屏幕竖着的时候的显示形式是ListView,屏幕横着的时候的显示形式是2列的GridView;
此时如果用RecyclerView,则通过设置LayoutManager一行代码实现替换。
RecylerView相对于ListView的优点罗列如下:
- RecyclerView封装了viewholder的回收复用,也就是说RecyclerView标准化了ViewHolder,编写Adapter面向的是ViewHolder而不再是View了,复用的逻辑被封装了,写起来更加简单。
直接省去了listview中convertView.setTag(holder)和convertView.getTag()这些繁琐的步骤。- 提供了一种插拔式的体验,高度的解耦,异常的灵活,针对一个Item的显示RecyclerView专门抽取出了相应的类,来控制Item的显示,使其的扩展性非常强。
- 设置布局管理器以控制Item的布局方式,横向、竖向以及瀑布流方式
例如:你想控制横向或者纵向滑动列表效果可以通过LinearLayoutManager这个类来进行控制(与GridView效果对应的是GridLayoutManager,与瀑布流对应的还StaggeredGridLayoutManager等)。也就是说RecyclerView不再拘泥于ListView的线性展示方式,它也可以实现GridView的效果等多种效果。- 可设置Item的间隔样式(可绘制)
通过继承RecyclerView的ItemDecoration这个类,然后针对自己的业务需求去书写代码。- 可以控制Item增删的动画,可以通过ItemAnimator这个类进行控制,当然针对增删的动画,RecyclerView有其自己默认的实现。
但是关于Item的点击和长按事件,需要用户自己去实现。
•基本使用
recyclerView = (RecyclerView) findViewById(R.id.recyclerView); LinearLayoutManager layoutManager = new LinearLayoutManager(this ); //设置布局管理器 recyclerView.setLayoutManager(layoutManager); //设置为垂直布局,这也是默认的 layoutManager.setOrientation(OrientationHelper. VERTICAL); //设置Adapter recyclerView.setAdapter(recycleAdapter); //设置分隔线 recyclerView.addItemDecoration( new DividerGridItemDecoration(this )); //设置增加或删除条目的动画 recyclerView.setItemAnimator( new DefaultItemAnimator());在使用RecyclerView时候,必须指定一个适配器Adapter和一个布局管理器LayoutManager。
适配器继承
RecyclerView.Adapter
类,具体实现类似ListView的适配器,取决于数据信息以及展示的UI。布局管理器用于确定RecyclerView中Item的展示方式以及决定何时复用已经不可见的Item,避免重复创建以及执行高成本的
findViewById()
方法。可以看见RecyclerView相比ListView会多出许多操作,这也是RecyclerView灵活的地方,它将许多动能暴露出来,用户可以选择性的自定义属性以满足需求。
基本使用
•引用
在build.gradle文件中引入该类。
compile 'com.android.support:recyclerview-v7:23.4.0'
•布局
Activity布局文件activity_rv.xml ... Item的布局文件item_1.xml ...
•创建适配器
标准实现步骤如下:
① 创建Adapter:创建一个继承RecyclerView.Adapter<VH>
的Adapter类(VH是ViewHolder的类名)
② 创建ViewHolder:在Adapter中创建一个继承RecyclerView.ViewHolder
的静态内部类,记为VH。ViewHolder的实现和ListView的ViewHolder实现几乎一样。
③ 在Adapter中实现3个方法:
- onCreateViewHolder()
- 这个方法主要生成为每个Item inflater出一个View,但是该方法返回的是一个ViewHolder
- 该方法把View直接封装在ViewHolder中,然后我们面向的是ViewHolder这个实例
- 当然这个ViewHolder需要我们自己去编写
- 需要注意的是在
onCreateViewHolder()
中,映射Layout必须为:- View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, parent, false);
- 而不能是:
- View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, null);
- onBindViewHolder()
- 这个方法主要用于适配渲染数据到View中
- 方法提供给你了一viewHolder而不是原来的convertView
- getItemCount()
- 这个方法就类似于BaseAdapter的getCount方法了,即总共有多少个条目。
可以看出,RecyclerView将ListView中
getView()
的功能拆分成了onCreateViewHolder()
和onBindViewHolder()
。基本的Adapter实现如下:
// ① 创建Adapter public class NormalAdapter extends RecyclerView.Adapter<NormalAdapter.VH>{ //② 创建ViewHolder public static class VH extends RecyclerView.ViewHolder{ public final TextView title; public VH(View v) { super(v); title = (TextView) v.findViewById(R.id.title); } } private List<String> mDatas; public NormalAdapter(List<String> data) { this.mDatas = data; } //③ 在Adapter中实现3个方法 @Override public void onBindViewHolder(VH holder, int position) { holder.title.setText(mDatas.get(position)); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //item 点击事件 } }); } @Override public int getItemCount() { return mDatas.size(); } @Override public VH onCreateViewHolder(ViewGroup parent, int viewType) { //LayoutInflater.from指定写法 View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, parent, false); return new VH(v); } }
•设置RecyclerView
创建完Adapter,接着对RecyclerView进行设置,一般来说,需要为RecyclerView进行四大设置,也就是后文说的四大组成:
- Layout Manager(必选)
- Adapter(必选)
- Item Decoration(可选,默认为空)
- Item Animator(可选,默认为DefaultItemAnimator)
如果要实现ListView的效果,只需要设置Adapter和Layout Manager,如下:
List<String> data = initData(); RecyclerView rv = (RecyclerView) findViewById(R.id.rv); rv.setLayoutManager(new LinearLayoutManager(this)); rv.setAdapter(new NormalAdapter(data));
•四大组成
RecyclerView的四大组成是:
- Layout Manager:Item的布局。
- Adapter:为Item提供数据。
- Item Decoration:Item之间的Divider。
- Item Animator:添加、删除Item动画。
•Layout Manager布局管理器
在最开始就提到,RecyclerView 能够支持各种各样的布局效果,这是 ListView 所不具有的功能,那么这个功能如何实现的呢?
其核心关键在于 RecyclerView.LayoutManager 类中。
从前面的基础使用可以看到,RecyclerView 在使用过程中要比 ListView 多一个 setLayoutManager 步骤;
这个 LayoutManager 就是用于控制我们 RecyclerView 最终的展示效果的。
LayoutManager负责RecyclerView的布局,其中包含了Item View的获取与回收。
RecyclerView提供了三种布局管理器:
- LinerLayoutManager 以垂直或者水平列表方式展示Item
- GridLayoutManager 以网格方式展示Item
- StaggeredGridLayoutManager 以瀑布流方式展示Item
如果你想用 RecyclerView 来实现自己自定义效果;
则应该去继承实现自己的 LayoutManager,并重写相应的方法,而不应该想着去改写 RecyclerView。
LayoutManager 常见 API
关于 LayoutManager 的使用有下面一些常见的 API(有些在 LayoutManager 实现的子类中)
canScrollHorizontally();//能否横向滚动 canScrollVertically();//能否纵向滚动 scrollToPosition(int position);//滚动到指定位置 setOrientation(int orientation);//设置滚动的方向 getOrientation();//获取滚动方向 findViewByPosition(int position);//获取指定位置的Item View findFirstCompletelyVisibleItemPosition();//获取第一个完全可见的Item位置 findFirstVisibleItemPosition();//获取第一个可见Item的位置 findLastCompletelyVisibleItemPosition();//获取最后一个完全可见的Item位置 findLastVisibleItemPosition();//获取最后一个可见Item的位置上面仅仅是列出一些常用的 API 而已,更多的 API 可以查看官方文档;
通常你想用 RecyclerView 实现某种效果;
例如指定滚动到某个 Item 位置,但是你在 RecyclerView 中又找不到可以调用的 API 时,就可以跑到 LayoutManager 的文档去看看,基本都在那里。
另外还有一点关于瀑布流布局效果 StaggeredGridLayoutManager 想说的:
- 看到网上有些文章写的示例代码
- 在设置了 StaggeredGridLayoutManager 后仍要去 Adapter 中动态设置 View 的高度,才能实现瀑布流
- 这种做法是完全错误的
- 之所以 StaggeredGridLayoutManager 的瀑布流效果出不来
- 基本是 item 布局的 xml 问题以及数据问题导致。
- 如果要在 Adapter 中设置 View 的高度,则完全违背了 LayoutManager 的设计理念了
LinearLayoutManager源码分析
这里我们简单分析LinearLayoutManager的实现。
对于LinearLayoutManager来说,比较重要的几个方法有:
onLayoutChildren()
: 对RecyclerView进行布局的入口方法。fill()
: 负责填充RecyclerView。scrollVerticallyBy()
:根据手指的移动滑动一定距离,并调用fill()
填充。canScrollVertically()
或canScrollHorizontally()
: 判断是否支持纵向滑动或横向滑动。
onLayoutChildren()
的核心实现如下:public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { detachAndScrapAttachedViews(recycler); //将原来所有的Item View全部放到Recycler的Scrap Heap或Recycle Pool fill(recycler, mLayoutState, state, false); //填充现在所有的Item View }RecyclerView的回收机制有个重要的概念,即将回收站分为Scrap Heap和Recycle Pool;
其中Scrap Heap的元素可以被直接复用,而不需要调用
onBindViewHolder()
。
detachAndScrapAttachedViews()
会根据情况,将原来的Item View放入Scrap Heap或Recycle Pool,从而在复用时提升效率。
fill()
是对剩余空间不断地调用layoutChunk()
,直到填充完为止。
layoutChunk()
的核心实现如下:public void layoutChunk() { View view = layoutState.next(recycler); //调用了getViewForPosition() addView(view); //加入View measureChildWithMargins(view, 0, 0); //计算View的大小 layoutDecoratedWithMargins(view, left, top, right, bottom); //布局View }其中
next()
调用了getViewForPosition(currentPosition)
,该方法是从RecyclerView的回收机制实现类Recycler中获取合适的View;在后文的回收机制中会介绍该方法的具体实现。
如果要自定义LayoutManager,可以参考:
•Adapter适配器
Adapter的使用方式前面已经介绍了,功能就是为RecyclerView提供数据,这里主要介绍万能适配器的实现。
其实万能适配器的概念在ListView就已经存在了,即base-adapter-helper。
这里我们只针对RecyclerView,聊聊万能适配器出现的原因。
为了创建一个RecyclerView的Adapter,每次我们都需要去做重复劳动;
包括重写
onCreateViewHolder()
,getItemCount()
、创建ViewHolder,并且实现过程大同小异,因此万能适配器出现了。万能适配器
这里讲解下万能适配器的实现思路。
我们通过
public abstract class QuickAdapter<T> extends RecyclerView.Adapter<QuickAdapter.VH>
定义万能适配器QuickAdapter类;T是列表数据中每个元素的类型,QuickAdapter.VH是QuickAdapter的ViewHolder实现类,称为万能ViewHolder。
首先介绍QuickAdapter.VH的实现:
static class VH extends RecyclerView.ViewHolder{ private SparseArray<View> mViews; private View mConvertView; private VH(View v){ super(v); mConvertView = v; mViews = new SparseArray<>(); } public static VH get(ViewGroup parent, int layoutId){ View convertView = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); return new VH(convertView); } public <T extends View> T getView(int id){ View v = mViews.get(id); if(v == null){ v = mConvertView.findViewById(id); mViews.put(id, v); } return (T)v; } public void setText(int id, String value){ TextView view = getView(id); view.setText(value); } }其中的关键点在于通过
SparseArray<View>
存储item view的控件;
getView(int id)
的功能就是通过id获得对应的View:
- 首先在mViews中查询是否存在
- 如果没有,那么
findViewById()
并放入mViews中,避免下次再执行findViewById()
)。QuickAdapter的实现如下:
public abstract class QuickAdapter<T> extends RecyclerView.Adapter<QuickAdapter.VH>{ private List<T> mDatas; public QuickAdapter(List<T> datas){ this.mDatas = datas; } public abstract int getLayoutId(int viewType); @Override public VH onCreateViewHolder(ViewGroup parent, int viewType) { return VH.get(parent,getLayoutId(viewType)); } @Override public void onBindViewHolder(VH holder, int position) { convert(holder, mDatas.get(position), position); } @Override public int getItemCount() { return mDatas.size(); } public abstract void convert(VH holder, T data, int position); static class VH extends RecyclerView.ViewHolder{ private SparseArray<View> mViews; private View mConvertView; private VH(View v){ super(v); mConvertView = v; mViews = new SparseArray<>(); } public static VH get(ViewGroup parent, int layoutId){ View convertView = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); return new VH(convertView); } public <T extends View> T getView(int id){ View v = mViews.get(id); if(v == null){ v = mConvertView.findViewById(id); mViews.put(id, v); } return (T)v; } public void setText(int id, String value){ TextView view = getView(id); view.setText(value); } } }其中:
getLayoutId(int viewType)
是根据viewType返回布局ID。convert()
做具体的bind操作。就这样,万能适配器实现完成了。
通过万能适配器能通过以下方式快捷地创建一个Adapter:
mAdapter = new QuickAdapter<String>(data) { @Override public int getLayoutId(int viewType) { return R.layout.item; } @Override public void convert(VH holder, String data, int position) { holder.setText(R.id.text, data); //holder.itemView.setOnClickListener(); 此处还可以添加点击事件 } };是不是很方便。当然复杂情况也可以轻松解决。
mAdapter = new QuickAdapter<Model>(data) { @Override public int getLayoutId(int viewType) { switch(viewType){ case TYPE_1: return R.layout.item_1; case TYPE_2: return R.layout.item_2; } } @Override public int getItemViewType(int position) { if(position % 2 == 0){ return TYPE_1; } else{ return TYPE_2; } } @Override public void convert(VH holder, Model data, int position) { int type = getItemViewType(position); switch(type){ case TYPE_1: holder.setText(R.id.text, data.text); break; case TYPE_2: holder.setImage(R.id.image, data.image); break; } } };
•Item Decoration间隔样式
RecyclerView通过
addItemDecoration()
方法添加item之间的分割线。Android并没有提供实现好的Divider,因此任何分割线样式都需要自己实现。
自定义间隔样式需要继承
RecyclerView.ItemDecoration
类;该类是个抽象类,官方目前并没有提供默认的实现类,主要有三个方法:
- onDraw(Canvas c, RecyclerView parent, State state)
- 在Item绘制之前被调用,该方法主要用于绘制间隔样式
- onDrawOver(Canvas c, RecyclerView parent, State state)
- 在Item绘制之前被调用,该方法主要用于绘制间隔样式
- getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
- 设置item的偏移量,偏移的部分用于填充间隔样式
- 即设置分割线的宽、高
- 在RecyclerView的
onMesure()
中会调用该方法。
onDraw()
和onDrawOver()
这两个方法都是用于绘制间隔样式,我们只需要复写其中一个方法即可。Google在sample中给了一个参考的实现类:DividerItemDecoration,这里我们通过分析这个例子来看如何自定义Item Decoration。
自定义的间隔样式的实现步骤
- ①通过读取系统主题中的 Android.R.attr.listDivider作为Item间的分割线,并且支持横向和纵向。
- 该分割线是系统默认的,你可以在theme.xml中找到该属性(android:listDivider)的使用情况。
- 如果要设置,则需要在value/styles.xml中设置:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="android:listDivider">@drawable/item_divider</item> </style>
- ② 获取到listDivider以后,该属性的值是个Drawable,在getItemOffsets中,outRect去设置了绘制的范围。
- ③ onDraw中实现了真正的绘制。
① 获取listDivider
首先看构造函数,构造函数中获得系统属性
android:listDivider
,该属性是一个Drawable对象。private static final int[] ATTRS = new int[]{android.R.attr.listDivider}; private Drawable mDivider; public DividerItemDecoration(Context context, int orientation) { final TypedArray a = context.obtainStyledAttributes(ATTRS); mDivider = a.getDrawable(0); a.recycle(); setOrientation(orientation); }② getItemOffsets
接着来看
getItemOffsets()
的实现:public void getItemOffsets(Rect outRect, int position, RecyclerView parent) { if (mOrientation == VERTICAL_LIST) { outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); } else { outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); } }这里只看
mOrientation == VERTICAL_LIST
的情况,outRect是当前item四周的间距,类似margin属性,现在设置了该item下间距为mDivider.getIntrinsicHeight()
那么
getItemOffsets()
是怎么被调用的呢?RecyclerView继承了ViewGroup,并重写了
measureChild()
;该方法在
onMeasure()
中被调用,用来计算每个child的大小;计算每个child大小的时候就需要加上
getItemOffsets()
设置的外间距:public void measureChild(View child, int widthUsed, int heightUsed){ final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);//调用getItemOffsets()获得Rect对象 widthUsed += insets.left + insets.right; heightUsed += insets.top + insets.bottom; //... }③ onDraw
这里我们只考虑
mOrientation == VERTICAL_LIST
的情况;DividerItemDecoration的
onDraw()
实际上调用了drawVertical()
:public void drawVertical(Canvas c, RecyclerView parent) { final int left = parent.getPaddingLeft(); final int right = parent.getWidth() - parent.getPaddingRight(); final int childCount = parent.getChildCount(); // 画每个item的分割线 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 + Math.round(ViewCompat.getTranslationY(child)); final int bottom = top + mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom);/*规定好左上角和右下角*/ mDivider.draw(c); } }那么
onDraw()
是怎么被调用的呢?还有ItemDecoration还有一个方法
onDrawOver()
,该方法也可以被重写,那么onDraw()
和onDrawOver()
之间有什么关系呢?我们来看下面的代码:
class RecyclerView extends ViewGroup{ public void draw(Canvas c) { super.draw(c); //调用View的draw(),该方法会先调用onDraw(),再调用dispatchDraw()绘制children final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDrawOver(c, this, mState); } ... } public void onDraw(Canvas c) { super.onDraw(c); final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDraw(c, this, mState); } } }根据View的绘制流程,首先调用RecyclerView重写的
draw()
方法;随后
super.draw()
即调用View的draw()
,该方法会先调用onDraw()
(这个方法在RecyclerView重写了);再调用
dispatchDraw()
绘制children。因此:ItemDecoration的
onDraw()
在绘制Item之前调用,ItemDecoration的onDrawOver()
在绘制Item之后调用。当然,如果只需要实现Item之间相隔一定距离,那么只需要为Item的布局设置margin即可,没必要自己实现ItemDecoration这么麻烦。
•Item Animator动画
RecyclerView能够通过
mRecyclerView.setItemAnimator(ItemAnimator animator)
设置添加、删除、移动、改变的动画效果。RecyclerView提供了默认的ItemAnimator实现类:DefaultItemAnimator。如果没有特殊的需求,默认使用这个动画即可。
// 设置Item添加和移除的动画 mRecyclerView.setItemAnimator(new DefaultItemAnimator());下面就添加一下删除和添加Item的动作,在Adapter里面添加方法:
public void addNewItem() { if(mData == null) { mData = new ArrayList<>(); } mData.add(0, "new Item"); ////更新数据集不是用adapter.notifyDataSetChanged()而是notifyItemInserted(position)与notifyItemRemoved(position) 否则没有动画效果。 notifyItemInserted(0); } public void deleteItem() { if(mData == null || mData.isEmpty()) { return; } mData.remove(0); notifyItemRemoved(0); }添加事件的处理:
public void onClick(View v) { int id = v.getId(); if(id == R.id.rv_add_item_btn) { mAdapter.addNewItem(); // 由于Adapter内部是直接在首个Item位置做增加操作,增加完毕后列表移动到首个Item位置 mLayoutManager.scrollToPosition(0); } else if(id == R.id.rv_del_item_btn){ mAdapter.deleteItem(); // 由于Adapter内部是直接在首个Item位置做删除操作,删除完毕后列表移动到首个Item位置 mLayoutManager.scrollToPosition(0); } }准备工作完毕后,来看一下运行的效果。
•运行效果
转载
本篇文章转载自:Android 控件 RecyclerView