com.android.support:recyclerview-v7:23.4.0
从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也可以看出。
RecylerView相对于ListView的优点罗列如下:
但是关于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
的Adapter类(VH是ViewHolder的类名)
② 创建ViewHolder:在Adapter中创建一个继承RecyclerView.ViewHolder
的静态内部类,记为VH。ViewHolder的实现和ListView的ViewHolder实现几乎一样。
③ 在Adapter中实现3个方法:
需要注意的是在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);
可以看出,RecyclerView将ListView中getView()
的功能拆分成了onCreateViewHolder()
和onBindViewHolder()
。
基本的Adapter实现如下:
// ① 创建Adapter
public class NormalAdapter extends RecyclerView.Adapter{
//② 创建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 mDatas;
public NormalAdapter(List 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);
}
}
创建完Adapter,接着对RecyclerView进行设置,一般来说,需要为RecyclerView进行四大设置,也就是后文说的四大组成:
如果要实现ListView的效果,只需要设置Adapter和Layout Manager,如下:
List data = initData();
RecyclerView rv = (RecyclerView) findViewById(R.id.rv);
rv.setLayoutManager(new LinearLayoutManager(this));
rv.setAdapter(new NormalAdapter(data));
RecyclerView的四大组成是:
在最开始就提到,RecyclerView 能够支持各种各样的布局效果,这是 ListView 所不具有的功能,那么这个功能如何实现的呢?其核心关键在于 RecyclerView.LayoutManager 类中。从前面的基础使用可以看到,RecyclerView 在使用过程中要比 ListView 多一个 setLayoutManager 步骤,这个 LayoutManager 就是用于控制我们 RecyclerView 最终的展示效果的。
LayoutManager负责RecyclerView的布局,其中包含了Item View的获取与回收。
RecyclerView提供了三种布局管理器:
如果你想用 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的使用方式前面已经介绍了,功能就是为RecyclerView提供数据,这里主要介绍万能适配器的实现。其实万能适配器的概念在ListView就已经存在了,即base-adapter-helper。
这里我们只针对RecyclerView,聊聊万能适配器出现的原因。为了创建一个RecyclerView的Adapter,每次我们都需要去做重复劳动,包括重写onCreateViewHolder()
,getItemCount()
、创建ViewHolder,并且实现过程大同小异,因此万能适配器出现了。
万能适配器
这里讲解下万能适配器的实现思路。
我们通过public abstract class QuickAdapter
定义万能适配器QuickAdapter类,T是列表数据中每个元素的类型,QuickAdapter.VH是QuickAdapter的ViewHolder实现类,称为万能ViewHolder。
首先介绍QuickAdapter.VH的实现:
static class VH extends RecyclerView.ViewHolder{
private SparseArray 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 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
存储item view的控件,getView(int id)
的功能就是通过id获得对应的View(首先在mViews中查询是否存在,如果没有,那么findViewById()
并放入mViews中,避免下次再执行findViewById()
)。
QuickAdapter的实现如下:
public abstract class QuickAdapter extends RecyclerView.Adapter{
private List mDatas;
public QuickAdapter(List 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 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 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(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(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;
}
}
};
RecyclerView通过addItemDecoration()
方法添加item之间的分割线。Android并没有提供实现好的Divider,因此任何分割线样式都需要自己实现。
自定义间隔样式需要继承RecyclerView.ItemDecoration
类,该类是个抽象类,官方目前并没有提供默认的实现类,主要有三个方法。
onMesure()
中会调用该方法。onDraw()
和onDrawOver()
这两个方法都是用于绘制间隔样式,我们只需要复写其中一个方法即可。
Google在sample中给了一个参考的实现类:DividerItemDecoration,这里我们通过分析这个例子来看如何自定义Item Decoration。
自定义的间隔样式的实现步骤
① 获取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这么麻烦。
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);
}
}
准备工作完毕后,来看一下运行的效果。
DefaultItemAnimator源码分析
这里我们通过分析DefaultItemAnimator的源码来介绍如何自定义Item Animator。
DefaultItemAnimator继承自SimpleItemAnimator,SimpleItemAnimator继承自ItemAnimator。
首先我们介绍ItemAnimator类的几个重要方法:
notifyItemChanged()
和notifyDataSetChanged()
的情况下布局发生改变时被调用。notifyItemChanged()
或notifyDataSetChanged()
时被调用。animateXxx()
返回true。上面的方法比较难懂,不过没关系,因为Android提供了SimpleItemAnimator类(继承自ItemAnimator),该类提供了一系列更易懂的API,在自定义Item Animator时只需要继承SimpleItemAnimator即可:
notifyItemChanged()
或notifyDataSetChanged()
时被调用。对于以上四个方法,注意两点:
runPendingAnimations()
中)需要调用dispatchXxxStarting(holder)
,执行完后需要调用dispatchXxxFinished(holder)
。runPendingAnimations()
中一并执行。DefaultItemAnimator类是RecyclerView提供的默认动画类。我们通过阅读该类源码学习如何自定义Item Animator。我们先看DefaultItemAnimator的成员变量:
private ArrayList mPendingAdditions = new ArrayList<>();//存放下一帧要执行的一系列add动画
ArrayList> mAdditionsList = new ArrayList<>();//存放正在执行的一批add动画
ArrayList mAddAnimations = new ArrayList<>(); //存放当前正在执行的add动画
private ArrayList mPendingRemovals = new ArrayList<>();
ArrayList mRemoveAnimations = new ArrayList<>();
private ArrayList mPendingMoves = new ArrayList<>();
ArrayList> mMovesList = new ArrayList<>();
ArrayList mMoveAnimations = new ArrayList<>();
private ArrayList mPendingChanges = new ArrayList<>();
ArrayList> mChangesList = new ArrayList<>();
ArrayList mChangeAnimations = new ArrayList<>();
DefaultItemAnimator实现了SimpleItemAnimator的animateAdd()
方法,该方法只是将该item添加到mPendingAdditions中,等到runPendingAnimations()
中执行。
public boolean animateAdd(final ViewHolder holder) {
resetAnimation(holder); //重置清空所有动画
ViewCompat.setAlpha(holder.itemView, 0); //将要做动画的View先变成透明
mPendingAdditions.add(holder);
return true;
}
接着看runPendingAnimations()
的实现,该方法是执行remove,move,change,add动画,执行顺序为:remove动画最先执行,随后move和change并行执行,最后是add动画。为了简化,我们将remove,move,change动画执行过程省略,只看执行add动画的过程,如下:
public void runPendingAnimations() {
//1、判断是否有动画要执行,即各个动画的成员变量里是否有值。
//2、执行remove动画
//3、执行move动画
//4、执行change动画,与move动画并行执行
//5、执行add动画
if (additionsPending) {
final ArrayList additions = new ArrayList<>();
additions.addAll(mPendingAdditions);
mAdditionsList.add(additions);
mPendingAdditions.clear();
Runnable adder = new Runnable() {
@Override
public void run() {
for (ViewHolder holder : additions) {
animateAddImpl(holder); //***** 执行动画的方法 *****
}
additions.clear();
mAdditionsList.remove(additions);
}
};
if (removalsPending || movesPending || changesPending) {
long removeDuration = removalsPending ? getRemoveDuration() : 0;
long moveDuration = movesPending ? getMoveDuration() : 0;
long changeDuration = changesPending ? getChangeDuration() : 0;
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
View view = additions.get(0).itemView;
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); //等remove,move,change动画全部做完后,开始执行add动画
}
}
}
为了防止在执行add动画时外面有新的add动画添加到mPendingAdditions中,从而导致执行add动画错乱,这里将mPendingAdditions的内容移动到局部变量additions中,然后遍历additions执行动画。
在runPendingAnimations()
中,animateAddImpl()
是执行add动画的具体方法,其实就是将itemView的透明度从0变到1(在animateAdd()
中已经将view的透明度变为0),实现如下:
void animateAddImpl(final ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mAddAnimations.add(holder);
animation.alpha(1).setDuration(getAddDuration()).
setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchAddStarting(holder); //在开始add动画前调用
}
@Override
public void onAnimationCancel(View view) {
ViewCompat.setAlpha(view, 1);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchAddFinished(holder); //在结束add动画后调用
mAddAnimations.remove(holder);
if (!isRunning()) {
dispatchAnimationsFinished(); //结束所有动画后调用
}
}
}).start();
}
开源动画recyclerview-animators
从DefaultItemAnimator类的实现来看,发现自定义Item Animator好麻烦,需要继承SimpleItemAnimator类,然后实现一堆方法。
别急,recyclerview-animators解救你,原因如下:
public class DefaultItemAnimator extends BaseItemAnimator {
public DefaultItemAnimator() {
}
public DefaultItemAnimator(Interpolator interpolator) {
mInterpolator = interpolator;
}
@Override protected void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
ViewCompat.animate(holder.itemView)
.alpha(0)
.setDuration(getRemoveDuration())
.setListener(new DefaultRemoveVpaListener(holder))
.setStartDelay(getRemoveDelay(holder))
.start();
}
@Override protected void preAnimateAddImpl(RecyclerView.ViewHolder holder) {
ViewCompat.setAlpha(holder.itemView, 0); //透明度先变为0
}
@Override protected void animateAddImpl(final RecyclerView.ViewHolder holder) {
ViewCompat.animate(holder.itemView)
.alpha(1)
.setDuration(getAddDuration())
.setListener(new DefaultAddVpaListener(holder))
.setStartDelay(getAddDelay(holder))
.start();
}
}
是不是比继承SimpleItemAnimator方便多了。
局部刷新闪屏问题解决
对于RecyclerView的Item Animator,有一个常见的坑就是“闪屏问题”。
这个问题的描述是:当Item视图中有图片和文字,当更新文字并调用notifyItemChanged()
时,文字改变的同时图片会闪一下。这个问题的原因是当调用notifyItemChanged()
时,会调用DefaultItemAnimator的animateChangeImpl()
执行change动画,该动画会使得Item的透明度从0变为1,从而造成闪屏。
解决办法很简单,在rv.setAdapter()
之前调用((SimpleItemAnimator)rv.getItemAnimator()).setSupportsChangeAnimations(false)
禁用change动画。
RecyclerView并没有像ListView一样暴露出Item点击事件或者长按事件处理的api,也就是说使用RecyclerView时候,需要我们自己来实现Item的点击和长按等事件的处理。
实现方法有很多:
我们选择第二种方法,更加直观和简单。
看一下Adapter的完整代码。
public class MyAdapter extends RecyclerView.Adapter{
// 展示数据
private ArrayList mData;
// 事件回调监听
private MyAdapter.OnItemClickListener onItemClickListener;
public MyAdapter(ArrayList data) {
this.mData = data;
}
public void updateData(ArrayList data) {
this.mData = data;
notifyDataSetChanged();
}
// 添加新的Item
public void addNewItem() {
if(mData == null) {
mData = new ArrayList<>();
}
mData.add(0, "new Item");
notifyItemInserted(0);
}
// 删除Item
public void deleteItem() {
if(mData == null || mData.isEmpty()) {
return;
}
mData.remove(0);
notifyItemRemoved(0);
}
// ① 定义点击回调接口
public interface OnItemClickListener {
void onItemClick(View view, int position);
void onItemLongClick(View view, int position);
}
// ② 定义一个设置点击监听器的方法
public void setOnItemClickListener(MyAdapter.OnItemClickListener listener) {
this.onItemClickListener = listener;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 实例化展示的view
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_rv_item, parent, false);
// 实例化viewholder
ViewHolder viewHolder = new ViewHolder(v);
return viewHolder;
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
// 绑定数据
holder.mTv.setText(mData.get(position));
//③ 对RecyclerView的每一个itemView设置点击事件
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
if(onItemClickListener != null) {
int pos = holder.getLayoutPosition();
onItemClickListener.onItemClick(holder.itemView, pos);
}
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if(onItemClickListener != null) {
int pos = holder.getLayoutPosition();
onItemClickListener.onItemLongClick(holder.itemView, pos);
}
//表示此事件已经消费,不会触发单击事件
return true;
}
});
}
@Override
public int getItemCount() {
return mData == null ? 0 : mData.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView mTv;
public ViewHolder(View itemView) {
super(itemView);
mTv = (TextView) itemView.findViewById(R.id.item_tv);
}
}
}
设置Adapter的事件监听。
mAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
Toast.makeText(MDRvActivity.this,"click " + position + " item", Toast.LENGTH_SHORT).show();
}
@Override
public void onItemLongClick(View view, int position) {
Toast.makeText(MDRvActivity.this,"long click " + position + " item", Toast.LENGTH_SHORT).show();
}
});
最后的实现效果。
RecyclerView展示的样式由布局管理器LayoutManager来控制。
网格样式的管理器是GridLayoutManager
,看一下它最常用的两个构造函数以及参数含义。
// 竖直方向的网格样式,每行四个Item
mLayoutManager = new GridLayoutManager(this, 4, OrientationHelper.VERTICAL, false);
mRecyclerView.setLayoutManager(mLayoutManager);
运行效果
网格样式已经显示出来了,和之前遇见的问题一样,没有间隔线,非常丑,间隔线必须加,而且要使用自定义,不使用系统自带的。
新建文件md_divider.xml,是一个灰色的矩形。
在styles.xml中的自定义的应用主题里替换掉listdivider属性。
然后继承RecyclerView.ItemDecoration
类,在构造函数里获取自定义的间隔线,复写绘制间隔线的方法。
public class MDGridRvDividerDecoration extends RecyclerView.ItemDecoration {
private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
// 用于绘制间隔样式
private Drawable mDivider;
public MDGridRvDividerDecoration(Context context) {
// 获取默认主题的属性
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
// 绘制间隔,每一个item,绘制右边和下方间隔样式
int childCount = parent.getChildCount();
int spanCount = ((GridLayoutManager)parent.getLayoutManager()).getSpanCount();
int orientation = ((GridLayoutManager)parent.getLayoutManager()).getOrientation();
boolean isDrawHorizontalDivider = true;
boolean isDrawVerticalDivider = true;
int extra = childCount % spanCount;
extra = extra == 0 ? spanCount : extra;
for(int i = 0; i < childCount; i++) {
isDrawVerticalDivider = true;
isDrawHorizontalDivider = true;
// 如果是竖直方向,最右边一列不绘制竖直方向的间隔
if(orientation == OrientationHelper.VERTICAL && (i + 1) % spanCount == 0) {
isDrawVerticalDivider = false;
}
// 如果是竖直方向,最后一行不绘制水平方向间隔
if(orientation == OrientationHelper.VERTICAL && i >= childCount - extra) {
isDrawHorizontalDivider = false;
}
// 如果是水平方向,最下面一行不绘制水平方向的间隔
if(orientation == OrientationHelper.HORIZONTAL && (i + 1) % spanCount == 0) {
isDrawHorizontalDivider = false;
}
// 如果是水平方向,最后一列不绘制竖直方向间隔
if(orientation == OrientationHelper.HORIZONTAL && i >= childCount - extra) {
isDrawVerticalDivider = false;
}
if(isDrawHorizontalDivider) {
drawHorizontalDivider(c, parent, i);
}
if(isDrawVerticalDivider) {
drawVerticalDivider(c, parent, i);
}
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int spanCount = ((GridLayoutManager) parent.getLayoutManager()).getSpanCount();
int orientation = ((GridLayoutManager)parent.getLayoutManager()).getOrientation();
int position = parent.getChildLayoutPosition(view);
if(orientation == OrientationHelper.VERTICAL && (position + 1) % spanCount == 0) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
return;
}
if(orientation == OrientationHelper.HORIZONTAL && (position + 1) % spanCount == 0) {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
return;
}
outRect.set(0, 0, mDivider.getIntrinsicWidth(), mDivider.getIntrinsicHeight());
}
/* 绘制竖直间隔线
* @param canvas
* @param parent 父布局,RecyclerView
* @param position item在父布局中所在的位置 */
private void drawVerticalDivider(Canvas canvas, RecyclerView parent, int position) {
final View child = parent.getChildAt(position);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getTop() - params.topMargin;
final int bottom = child.getBottom() + params.bottomMargin + mDivider.getIntrinsicHeight();
final int left = child.getRight() + params.rightMargin;
final int right = left + mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
/* 绘制水平间隔线
* @param canvas
* @param parent 父布局,RecyclerView
* @param position item在父布局中所在的位置 */
private void drawHorizontalDivider(Canvas canvas, RecyclerView parent, int position) {
final View child = parent.getChildAt(position);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDivider.getIntrinsicHeight();
final int left = child.getLeft() - params.leftMargin;
final int right = child.getRight() + params.rightMargin + mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
}
设置RecyclerView的间隔线。
mRecyclerView.addItemDecoration(new MDGridRvDividerDecoration(this));
运行效果如下图。
关于网格样式的RecyclerView使用大体和列表样式相同,主要在于间隔线的实现上有些不同,来看一下如果真正的使用自定义的间隔线需要做些什么。
RecyclerView.ItemDecoration
类,并实现其中的绘制间隔线方法RecyclerView的瀑布流布局管理器是StaggeredGridLayoutManager
,它最常用的构造函数就一个,StaggeredGridLayoutManager(int spanCount, int orientation),spanCount代表每行或每列的Item个数,orientation代表列表的方向,竖直或者水平。
看在代码中的使用。
// 初始化布局管理器
mLayoutManager = new StaggeredGridLayoutManager(2, OrientationHelper.VERTICAL);
// 设置布局管理器
mRecyclerView.setLayoutManager(mLayoutManager);
// 设置adapter
mRecyclerView.setAdapter(mAdapter);
// 设置间隔样式
mRecyclerView.addItemDecoration(new MDStaggeredRvDividerDecotation(this));
要实现瀑布流效果(仅讨论竖直方向的瀑布流样式),每一个Item的高度要有所差别,如果所有的item的高度相同,就和网格样式是一样的展示效果。示例中就实现两中不同高度的Item,一个高度为80dp,一个高度为100dp。
view_rv_staggered_item.xml布局:
view_rv_staggered_item_two.xml布局:
Item不同的布局是在Adapter里面绑定的,看一下Adapter的实现。
public class MDStaggeredRvAdapter extends RecyclerView.Adapter {
// 展示数据
private ArrayList mData;
public MDStaggeredRvAdapter(ArrayList data) {
this.mData = data;
}
public void updateData(ArrayList data) {
this.mData = data;
notifyDataSetChanged();
}
@Override
public int getItemViewType(int position) {
// 瀑布流样式外部设置spanCount为2,在这列设置两个不同的item type,以区分不同的布局
return position % 2;
}
@Override
public MDStaggeredRvAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 实例化展示的view
View v;
if(viewType == 1) {
v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_rv_staggered_item, parent, false);
} else {
v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_rv_staggered_item_two, parent, false);
}
// 实例化viewholder
ViewHolder viewHolder = new ViewHolder(v);
return viewHolder;
}
@Override
public void onBindViewHolder(MDStaggeredRvAdapter.ViewHolder holder, int position) {
// 绑定数据
holder.mTv.setText(mData.get(position));
}
@Override
public int getItemCount() {
return mData == null ? 0 : mData.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView mTv;
public ViewHolder(View itemView) {
super(itemView);
mTv = (TextView) itemView.findViewById(R.id.item_tv);
}
}
}
接下来是设置瀑布流样式的间隔线样式的,上面代码中使用的是MDStaggeredRvDividerDecotation
类,其实是直接拷贝的网格样式的间隔线绘制类。看一下运行效果。
很奇怪,间隔线并没有按照我们想象中的方式绘制,仔细看瀑布流中Item的分布,发现瀑布流样式的Item分布和网格样式的Item分布有些不同。对比一下两者Item的分布,如下图。
网格样式的Item分布规律很明显,竖直方向的网格,Item是从左向右从上到下依次按顺序排列分布。
瀑布流样式的Item分布也是从上到下,从左到右的顺序排列,但是有一个高度的优先级,如果某一列中有一个高度最低的位置为空,最优先在此处添加Item。看第三张图的3 item
,因为该位置最低,优先在此处添加Item。
分析出了瀑布流样式的Item的分布规律,就会发现,按照以往列表样式或者网格样式去设置间隔线是有问题的,因为不知道Item具体的位置,上下左右间隔线是否需要绘制不确定,参考第二张图,其实第三张图的间隔线也有问题,向上滑动就会展示出来。
目前能考虑到的瀑布流添加间隔线的思路:
设置间隔有两个方法:
第一种设置间隔的方法会导致相邻的Item间距是间隔的两倍,第二种设置间隔的方法会导致Item某一个方向上的与父布局边缘无间隔,但是另一个方向与父布局边缘有间隔,例如左上相邻两边设置了间隔,最左边一列的Item左边与父布局边缘有间隔,但是最右边一列Item右边与父布局无间隔,第一行和最后一行的Item也会出现这种情况。
要解决上面的问题,父布局RecyclerView也需要根据相应的情况设置padding让整个布局的间隔都一致。下面的例子是选择在Item布局中设置间隔,因为可以自己在布局文件中控制颜色比较方便,选择右下两边设置间隔。
首先修改两个Item的布局文件。
view_rv_staggered_item.xml修改背景色和外层间距背景色。
同样修改view_rv_staggered_item_two.xml。
最后修改RecyclerView的一些属性。
运行一下,看看最后的效果。
差不多完美的解决了间隔线的问题,有细心的同学可能发现,在RecyclerView滑动的时候上面一直有一条灰色的间隔线,这个可以通过取消xml布局文件中RecyclerView的paddingTop属性去掉顶部灰色的间隔线。
RecyclerView默认没有提供类似addHeaderView()
和addFooterView()
的API,因此这里介绍如何优雅地实现这两个接口。
如果你已经实现了一个Adapter,现在想为这个Adapter添加addHeaderView()
和addFooterView()
接口,则需要在Adapter中添加几个Item Type,然后修改getItemViewType()
,onCreateViewHolder()
,onBindViewHolder()
,getItemCount()
等方法,并添加switch语句进行判断。那么如何在不破坏原有Adapter实现的情况下完成呢?
这里引入装饰器(Decorator)设计模式,该设计模式通过组合的方式,在不破话原有类代码的情况下,对原有类的功能进行扩展。
具体实现思路其实很简单,创建一个继承RecyclerView.Adapter
的类,并重写常见的方法,然后通过引入ITEM TYPE的方式实现:
public class NormalAdapterWrapper extends RecyclerView.Adapter{
enum ITEM_TYPE{
HEADER,
FOOTER,
NORMAL
}
private NormalAdapter mAdapter;
private View mHeaderView;
private View mFooterView;
public NormalAdapterWrapper(NormalAdapter adapter){
mAdapter = adapter;
}
@Override
public int getItemViewType(int position) {
if(position == 0){
return ITEM_TYPE.HEADER.ordinal();
} else if(position == mAdapter.getItemCount() + 1){
return ITEM_TYPE.FOOTER.ordinal();
} else{
return ITEM_TYPE.NORMAL.ordinal();
}
}
@Override
public int getItemCount() {
return mAdapter.getItemCount() + 2;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(position == 0){
return;
} else if(position == mAdapter.getItemCount() + 1){
return;
} else{
mAdapter.onBindViewHolder(((NormalAdapter.VH)holder), position - 1);
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(viewType == ITEM_TYPE.HEADER.ordinal()){
return new RecyclerView.ViewHolder(mHeaderView) {};
} else if(viewType == ITEM_TYPE.FOOTER.ordinal()){
return new RecyclerView.ViewHolder(mFooterView) {};
} else{
return mAdapter.onCreateViewHolder(parent,viewType);
}
}
public void addHeaderView(View view){
this.mHeaderView = view;
}
public void addFooterView(View view){
this.mFooterView = view;
}
}
这恰恰满足了我们的需求。我们只需要通过以下方式为原有的Adapter(这里命名为NormalAdapter)添加addHeaderView()
和addFooterView()
接口:
NormalAdapter adapter = new NormalAdapter(data);
NormalAdapterWrapper newAdapter = new NormalAdapterWrapper(adapter);
View headerView = LayoutInflater.from(this).inflate(R.layout.item_header, mRecyclerView, false);
View footerView = LayoutInflater.from(this).inflate(R.layout.item_footer, mRecyclerView, false);
newAdapter.addFooterView(footerView);
newAdapter.addHeaderView(headerView);
mRecyclerView.setAdapter(newAdapter);
是不是看起来特别优雅。
ListView提供了setEmptyView()
设置Adapter数据为空时的View视图。RecyclerView虽然没提供直接的API,但是也可以很简单地实现。
getRootView().addView(emptyView)
将空数据时显示的View添加到当前View的层次结构中。具体实现如下:
public class EmptyRecyclerView extends RecyclerView{
private View mEmptyView;
private AdapterDataObserver mObserver = new AdapterDataObserver() {
@Override
public void onChanged() {
Adapter adapter = getAdapter();
if(adapter.getItemCount() == 0){
mEmptyView.setVisibility(VISIBLE);
EmptyRecyclerView.this.setVisibility(GONE);
} else{
mEmptyView.setVisibility(GONE);
EmptyRecyclerView.this.setVisibility(VISIBLE);
}
}
public void onItemRangeChanged(int positionStart, int itemCount) {onChanged();}
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {onChanged();}
public void onItemRangeRemoved(int positionStart, int itemCount) {onChanged();}
public void onItemRangeInserted(int positionStart, int itemCount) {onChanged();}
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {onChanged();}
};
public EmptyRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public void setEmptyView(View view){
this.mEmptyView = view;
((ViewGroup)this.getRootView()).addView(mEmptyView); //加入主界面布局
}
public void setAdapter(RecyclerView.Adapter adapter){
super.setAdapter(adapter);
adapter.registerAdapterDataObserver(mObserver);
mObserver.onChanged();
}
}
布局
Activity中使用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_5);
mRv = (EmptyRecyclerView) findViewById(R.id.rv);
mRv.setLayoutManager(new LinearLayoutManager(this));
mData = new ArrayList<>();
mAdapter = new NormalAdapter(mData);
//View view = LayoutInflater.from(this).inflate(R.layout.empty, null);
View view = findViewById(R.id.text_empty);
mRv.setEmptyView(view);
mRv.setAdapter(mAdapter);
}
Android提供了ItemTouchHelper类,使得RecyclerView能够轻易地实现滑动和拖拽,此处我们要实现上下拖拽和侧滑删除。
① 创建ItemTouchHelper.Callback类
首先创建一个继承自ItemTouchHelper.Callback
的类,并重写以下方法:
getMovementFlags()
: 设置支持的拖拽和滑动的方向,此处我们支持的拖拽方向为上下,滑动方向为从左到右和从右到左,内部通过makeMovementFlags()
设置。onMove()
: 拖拽时回调。onSwiped()
: 滑动时回调。onSelectedChanged()
: 状态变化时回调,一共有三个状态,分别是ACTION_STATE_IDLE(空闲状态),ACTION_STATE_SWIPE(滑动状态),ACTION_STATE_DRAG(拖拽状态)。此方法中可以做一些状态变化时的处理,比如拖拽的时候修改背景色。clearView()**
: 用户交互结束**时回调。此方法可以做一些状态的清空,比如拖拽结束后还原背景色。isLongPressDragEnabled()
: 是否支持长按拖拽,默认为true。如果不想支持长按拖拽,则重写并返回false。具体实现如下:
public class SimpleItemTouchCallback extends ItemTouchHelper.Callback {
private NormalAdapter mAdapter;
private List mData;
public SimpleItemTouchCallback(NormalAdapter adapter, List data){
mAdapter = adapter;
mData = data;
}
//设置支持的拖拽、滑动的方向
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN; //s上下拖拽
int swipeFlag = ItemTouchHelper.START | ItemTouchHelper.END; //左->右和右->左滑动
return makeMovementFlags(dragFlag,swipeFlag);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
Collections.swap(mData, from, to);
mAdapter.notifyItemMoved(from, to);
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int pos = viewHolder.getAdapterPosition();
mData.remove(pos);
mAdapter.notifyItemRemoved(pos);
}
//状态改变时回调
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
if(actionState != ItemTouchHelper.ACTION_STATE_IDLE){
NormalAdapter.VH holder = (NormalAdapter.VH)viewHolder;
holder.itemView.setBackgroundColor(0xffbcbcbc); //设置拖拽和侧滑时的背景色
}
}
//拖拽或滑动完成之后调用,用来清除一些状态
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
NormalAdapter.VH holder = (NormalAdapter.VH)viewHolder;
holder.itemView.setBackgroundColor(0xffeeeeee); //背景色还原
}
}
② 设置ItemTouchHelper给RecyclerView
然后通过以下代码为RecyclerView设置该滑动、拖拽功能:
ItemTouchHelper helper = new ItemTouchHelper(new SimpleItemTouchCallback(adapter, data));
helper.attachToRecyclerView(recyclerview);
触摸拖拽
前面拖拽的触发方式只有长按,如果想支持触摸Item中的某个View实现拖拽,则核心方法为helper.startDrag(holder)
。首先定义接口:
interface OnStartDragListener{
void startDrag(RecyclerView.ViewHolder holder);
}
然后让Activity实现该接口:
public MainActivity extends Activity implements OnStartDragListener{
...
public void startDrag(RecyclerView.ViewHolder holder) {
mHelper.startDrag(holder);
}
}
如果要对ViewHolder的text对象支持触摸拖拽,则在Adapter中的onBindViewHolder()
中添加:
holder.text.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_DOWN){
mListener.startDrag(holder);
}
return false;
}
});
其中mListener是在创建Adapter时将实现OnStartDragListener接口的Activity对象作为参数传进来。
完整代码如下:
public class NormalAdapter extends RecyclerView.Adapter{
private List mDatas;
private OnStartDragListener mListener;
public NormalAdapter(List data, OnStartDragListener listener) {
this.mDatas = data;
mListener = listener;
}
@Override
public void onBindViewHolder(final VH holder, int position) {
ObjectModel model = mDatas.get(position);
holder.title.setText(model.title);
holder.number.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_DOWN){
mListener.startDrag(holder);
}
return false;
}
});
}
@Override
public int getItemCount() {
return mDatas.size();
}
@Override
public VH onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_3, parent, false);
return new VH(v);
}
public static class VH extends RecyclerView.ViewHolder{
public final TextView title;
public final ImageView number;
public VH(View v) {
super(v);
title = (TextView) v.findViewById(R.id.title);
number = (ImageView) v.findViewById(R.id.icon);
}
}
}
public class Activity3 extends AppCompatActivity implements OnStartDragListener{
private RecyclerView mRv;
private NormalAdapter mAdapter;
private ItemTouchHelper mHelper;
private List mData;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_3);
mRv = (RecyclerView) findViewById(R.id.rv);
mRv.setLayoutManager(new LinearLayoutManager(this));
mAdapter = new NormalAdapter(mData = initData(), this);
mRv.setAdapter(mAdapter);
mHelper = new ItemTouchHelper(new SimpleItemTouchCallback(mAdapter, mData));
mHelper.attachToRecyclerView(mRv);
}
public ArrayList initData(){
ArrayList models = new ArrayList<>();
String[] titles = getResources().getStringArray(R.array.title_array);
for(int i=0;i
Android 5.0推出了嵌套滑动机制,在之前,一旦子View处理了触摸事件,父View就没有机会再处理这次的触摸事件,而嵌套滑动机制解决了这个问题,能够实现如下效果:
为了支持嵌套滑动,子View必须实现NestedScrollingChild接口,父View必须实现NestedScrollingParent接口。
而RecyclerView实现了NestedScrollingChild接口,而CoordinatorLayout实现了NestedScrollingParent接口,上图是实现CoordinatorLayout嵌套RecyclerView的效果。
为了实现上图的效果,需要用到的组件有:
实现中需要注意的点有:
app:layout_collapseMode
设置为pin,表示折叠之后固定在顶端,而为ImageView的app:layout_collapseMode
设置为parallax,表示视差模式,即渐变的效果。app:layout_behavior="@string/appbar_scrolling_view_behavior"
。app:layout_scrollFlags="scroll|exitUntilCollapsed"
,其中scroll表示滚动出屏幕,exitUntilCollapsed表示退出后折叠。具体实现参见Demo6。
布局:
适配器:
public abstract class QuickAdapter extends RecyclerView.Adapter{
private List mDatas;
public QuickAdapter(List 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 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 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);
}
}
}
Activity:
public class Activity6 extends AppCompatActivity {
private RecyclerView mRv;
private QuickAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_6);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mRv = (RecyclerView) findViewById(R.id.rv);
mRv.setLayoutManager(new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL));
mAdapter = new QuickAdapter(initData()) {
@Override
public int getLayoutId(int viewType) {
return R.layout.item_6;
}
@Override
public void convert(VH holder, Integer data, int position) {
ImageView imageView = holder.getView(R.id.image);
Picasso.with(Activity6.this).load(data).into(imageView);
//holder.itemView.setOnClickListener(); 此处添加点击事件
}
@Override
public int getItemViewType(int position) {
return super.getItemViewType(position);
}
};
mAdapter.setHasStableIds(true);
((SimpleItemAnimator)mRv.getItemAnimator()).setSupportsChangeAnimations(false);
mRv.setAdapter(mAdapter);
}
public List initData(){
Integer[] images = {R.drawable.s1, R.drawable.s2, R.drawable.s3, R.drawable.s4, R.drawable.s5,
R.drawable.s6, R.drawable.s7, R.drawable.s8, R.drawable.s9, R.drawable.s10
};
ArrayList list = new ArrayList<>();
for(int i=0;i<2;i++){
for(Integer image:images){
list.add(image);
}
}
return list;
}
}
ListView相比RecyclerView,有一些优点:
addHeaderView()
, addFooterView()
添加头视图和尾视图。setOnItemClickListener()
和setOnItemLongClickListener()
设置点击事件和长按事件。这些功能在RecyclerView中都没有直接的接口,要自己实现(虽然实现起来很简单),因此如果只是实现简单的显示功能,ListView无疑更简单。
RecyclerView相比ListView,有一些明显的优点:
if(convertView == null)
的实现,而且回收机制更加完善。RecyclerView是一个插件式的实现,对各个功能进行解耦,从而扩展性比较好。
我们都知道ListView通过adapter.notifyDataSetChanged()
实现ListView的更新,这种更新方法的缺点是全局更新,即对每个Item View都进行重绘。但事实上很多时候,我们只是更新了其中一个Item的数据,其他Item其实可以不需要重绘。
这里给出ListView实现局部更新的方法:
public void updateItemView(ListView listview, int position, Data data){
int firstPos = listview.getFirstVisiblePosition();
int lastPos = listview.getLastVisiblePosition();
if(position >= firstPos && position <= lastPos){ //可见才更新,不可见则在getView()时更新
//listview.getChildAt(i)获得的是当前可见的第i个item的view
View view = listview.getChildAt(position - firstPos);
VH vh = (VH)view.getTag();
vh.text.setText(data.text);
}
}
可以看出,我们通过ListView的getChildAt()
来获得需要更新的View,然后通过getTag()
获得ViewHolder,从而实现更新。
RecyclerView提供了notifyItemInserted()
,notifyItemRemoved()
,notifyItemChanged()
等API更新单个或某个范围的Item视图。
ListView与RecyclerView缓存机制原理大致相似,如下图所示:
image
过程中,离屏的ItemView即被回收至缓存,入屏的ItemView则会优先从缓存中获取,只是ListView与RecyclerView的实现细节有差异.(这只是缓存使用的其中一个场景,还有如刷新等)
1. 层级不同:
RecyclerView比ListView多两级缓存,支持多个离ItemView缓存,支持开发者自定义缓存处理逻辑,支持所有RecyclerView共用同一个RecyclerViewPool(缓存池)。
具体来说:
ListView(两级缓存):
image
RecyclerView(四级缓存):
image
ListView和RecyclerView缓存机制基本一致:
1). mActiveViews和mAttachedScrap功能相似,意义在于快速重用屏幕上可见的列表项ItemView,而不需要重新createView和bindView;
2). mScrapView和mCachedViews + mReyclerViewPool功能相似,意义在于缓存离开屏幕的ItemView,目的是让即将进入屏幕的ItemView重用.
3). RecyclerView的优势在于
客观来说,RecyclerView在特定场景下对ListView的缓存机制做了补强和完善。
2. 缓存不同:
1). RecyclerView缓存RecyclerView.ViewHolder,抽象可理解为:
View + ViewHolder(避免每次createView时调用findViewById) + flag(标识状态);
2). ListView缓存View。
缓存不同,二者在缓存的使用上也略有差别,具体来说:
ListView获取缓存的流程:
image
RecyclerView获取缓存的流程:
image
1). RecyclerView中mCacheViews(屏幕外)获取缓存时,是通过匹配pos获取目标位置的缓存,这样做的好处是,当数据源数据不变的情况下,无须重新bindView:
而同样是离屏缓存,ListView从mScrapViews根据pos获取相应的缓存,但是并没有直接使用,而是重新getView(即必定会重新bindView),相关代码如下:
//AbsListView源码:line2345
//通过匹配pos从mScrapView中获取缓存
final View scrapView = mRecycler.getScrapView(position);
//无论是否成功都直接调用getView,导致必定会调用createView
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
mRecycler.addScrapView(scrapView, position);
} else {
...
}
}
2). ListView中通过pos获取的是view,即pos–>view;
RecyclerView中通过pos获取的是viewholder,即pos –> (view,viewHolder,flag);
从流程图中可以看出,标志flag的作用是判断view是否需要重新bindView,这也是RecyclerView实现局部刷新的一个核心.
由上文可知,RecyclerView的缓存机制确实更加完善,但还不算质的变化,RecyclerView更大的亮点在于提供了局部刷新的接口,通过局部刷新,就能避免调用许多无用的bindView.
(RecyclerView和ListView添加,移除Item效果对比)
结合RecyclerView的缓存机制,看看局部刷新是如何实现的:
以RecyclerView中notifyItemRemoved(1)为例,最终会调用requestLayout(),使整个RecyclerView重新绘制,过程为:
onMeasure()–>onLayout()–>onDraw()
其中,onLayout()为重点,分为三步:
其中,layoutChildren()流程图:
image
image
当调用notifyItemRemoved时,会对屏幕内ItemView做预处理,修改ItemView相应的pos以及flag(流程图中红色部分):
image
当调用fill()中RecyclerView.getViewForPosition(pos)时,RecyclerView通过对pos和flag的预处理,使得bindview只调用一次.
需要指出,ListView和RecyclerView最大的区别在于数据源改变时的缓存的处理逻辑,ListView是”一锅端”,将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。
ListView回收机制
ListView为了保证Item View的复用,实现了一套回收机制,该回收机制的实现类是RecycleBin,他实现了两级缓存:
View[] mActiveViews
: 缓存屏幕上的View,在该缓存里的View不需要调用getView()
。ArrayList[] mScrapViews;
: 每个Item Type对应一个列表作为回收站,缓存由于滚动而消失的View,此处的View如果被复用,会以参数的形式传给getView()
。接下来我们通过源码分析ListView是如何与RecycleBin交互的。其实ListView和RecyclerView的layout过程大同小异,ListView的布局函数是layoutChildren()
,实现如下:
void layoutChildren(){
//1. 如果数据被改变了,则将所有Item View回收至scrapView
//(而RecyclerView会根据情况放入Scrap Heap或RecyclePool);否则回收至mActiveViews
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
//2. 填充
switch(){
case LAYOUT_XXX:
fillXxx();
break;
case LAYOUT_XXX:
fillXxx();
break;
}
//3. 回收多余的activeView
mRecycler.scrapActiveViews();
}
其中fillXxx()
实现了对Item View进行填充,该方法内部调用了makeAndAddView()
,实现如下:
View makeAndAddView(){
if (!mDataChanged) {
child = mRecycler.getActiveView(position);
if (child != null) {
return child;
}
}
child = obtainView(position, mIsScrap);
return child;
}
其中,getActiveView()
是从mActiveViews中获取合适的View,如果获取到了,则直接返回,而不调用obtainView()
,这也印证了如果从mActiveViews获取到了可复用的View,则不需要调用getView()
。
obtainView()
是从mScrapViews中获取合适的View,然后以参数形式传给了getView()
,实现如下:
View obtainView(int position){
final View scrapView = mRecycler.getScrapView(position); //从RecycleBin中获取复用的View
final View child = mAdapter.getView(position, scrapView, this);
}
接下去我们介绍getScrapView(position)
的实现,该方法通过position得到Item Type,然后根据Item Type从mScrapViews获取可复用的View,如果获取不到,则返回null,具体实现如下:
class RecycleBin{
private View[] mActiveViews; //存储屏幕上的View
private ArrayList[] mScrapViews; //每个item type对应一个ArrayList
private int mViewTypeCount; //item type的个数
private ArrayList mCurrentScrap; //mScrapViews[0]
View getScrapView(int position) {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap < 0) {
return null;
}
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else if (whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}
private View retrieveFromScrap(ArrayList scrapViews, int position){
int size = scrapViews.size();
if(size > 0){
return scrapView.remove(scrapViews.size() - 1); //从回收列表中取出最后一个元素复用
} else{
return null;
}
}
}
RecyclerView回收机制
RecyclerView和ListView的回收机制非常相似,但是ListView是以View作为单位进行回收,RecyclerView是以ViewHolder作为单位进行回收。
Recycler是RecyclerView回收机制的实现类,他实现了四级缓存:
getView()
。在上文Layout Manager中已经介绍了RecyclerView的layout过程,但是一笔带过了getViewForPosition()
,因此此处介绍该方法的实现。
View getViewForPosition(int position, boolean dryRun){
if(holder == null){
//从mAttachedScrap,mCachedViews获取ViewHolder
holder = getScrapViewForPosition(position,INVALID,dryRun); //此处获得的View不需要bind
}
final int type = mAdapter.getItemViewType(offsetPosition);
if (mAdapter.hasStableIds()) { //默认为false
holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
}
if(holder == null && mViewCacheExtension != null){
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type); //从
if(view != null){
holder = getChildViewHolder(view);
}
}
if(holder == null){
holder = getRecycledViewPool().getRecycledView(type);
}
if(holder == null){ //没有缓存,则创建
holder = mAdapter.createViewHolder(RecyclerView.this, type); //调用onCreateViewHolder()
}
if(!holder.isBound() || holder.needsUpdate() || holder.isInvalid()){
mAdapter.bindViewHolder(holder, offsetPosition);
}
return holder.itemView;
}
从上述实现可以看出,依次从mAttachedScrap, mCachedViews, mViewCacheExtension, mRecyclerPool寻找可复用的ViewHolder,如果是从mAttachedScrap或mCachedViews中获取的ViewHolder,则不会调用onBindViewHolder()
,mAttachedScrap和mCachedViews也就是我们所说的Scrap Heap;而如果从mViewCacheExtension或mRecyclerPool中获取的ViewHolder,则会调用onBindViewHolder()
。
RecyclerView局部刷新的实现原理也是基于RecyclerView的回收机制,即能直接复用的ViewHolder就不调用onBindViewHolder()
。
在一些场景下,如界面初始化,滑动等,ListView和RecyclerView都能很好地工作,两者并没有很大的差异:
数据源频繁更新的场景,如弹幕:http://www.jianshu.com/p/2232a63442d6等RecyclerView的优势会非常明显;