1.简介
RecyclerView的官方资料介绍是:A flexible view for providing a limited window into a large data set,大体意思就是RecyclerView是一个用于显示大量数据的弹性视图控件。在RecyclerView出现之前,我们往往使用ListView显示大量的数据,对于ListView,其官方介绍是:A view that shows items in a vertically scrolling list,即垂直显示一组数据,注意这里加入了垂直两个字,这也正是RecyclerView和ListView的一个非常直观的区别。使用RecylclerView能够很容易的实现水平、垂直、瀑布流等显示样式,而ListView只能进行垂直显示。究其原因在于,ListView把布局方式硬编码进ListView类中,而RecyclerView则把布局方式和自身解耦。
2.特性
1.封装了对viewholder的回收复用。
2.可以同时实现线性布局,网格布局,瀑布流布局,完美替代ListView+GrideView,更容易组合设计出自己需要的滑动布局。
3.自带了ItemAnimation,可以设置加载和移除时的动画,方便做出各种动态浏览的效果。
2.基本设计原理
RecyclerView本质是用来把View和Datas关联起来,将Data展示到View上。
RecyclerView的职责就是将Datas中的数据以一定的规则展示在它的上面,但说破天RecyclerView只是一个ViewGroup,它只认识View,不清楚Data数据的具体结构,所以两个陌生人之间想构建通话,我们很容易想到适配器模式,因此,RecyclerView需要一个Adapter来与Datas进行交流:
如上如所示,RecyclerView表示只会和ViewHolder进行接触,而Adapter的工作就是将Data转换为RecyclerView认识的ViewHolder,因此RecyclerView就间接地接触了Datas。
然而仍然不是特别完美,尽管Adapter已经将Datas转换为RecyclerView所熟知的View,但RecyclerView并不想自己管理些子View,因此,它雇佣了一个叫做LayoutManager的Manager来帮其完成布局,现在,图示变成下面这样:
到了这里,有负责翻译数据的Adapter,有负责布局的LayoutManager,有负责管理View的Recycler,一切都很完美,但RecyclerView之所优雅,还在于当子View变动的时候姿态要优雅(动画),所以用需要了一个舞者ItemAnimator,因此,动画也进入了这个图示:
3.基本使用
1.创建对象
RecyclerView recyclerview = (RecyclerView) findViewById(R.id.recyclerview);
2.设置显示规则
recyclerview.setLayoutManager(new LinearLayoutManager(this));
RecyclerView 将所有的显示规则交给一个叫 LayoutManager 的类去完成了。
LayoutManager是一个抽象类,系统已经为我们提供了三个默认的实现类,分别是 LinearLayoutManager 、 GridLayoutManager 、 StaggeredGridLayoutManager ,从名字我们就能看出来了,分别是,线性显示、网格显示、瀑布流显示。当然你也可以通过继承这些类来扩展实现自己的 LayougManager 。
3.设置适配器
recyclerview.setAdapter(adapter);
适配器,同 ListView 一样,用来设置每个item显示内容的。
通常,我们写 ListView 适配器,都是首先继承 BaseAdapter ,实现四个抽象方法,创建一个静态 ViewHolder , getView() 方法中判断 convertView 是否为空,创建还是获取 viewholder 对象。
而 RecyclerView 也是类似的步骤,首先继承 RecyclerView.Adapter<VH> 类,实现三个抽象方法,创建一个静态的 ViewHolder ,并且 ViewHolder 必须继承自 RecyclerView.ViewHolder 类。
public class DemoAdapter extends RecyclerView.Adapter<DemoAdapter.VH> {
private List<Data> dataList;
private Context context;
public DemoAdapter(Context context, ArrayList<Data> datas) {
this.dataList = datas;
this.context = context;
}
@Override
public int getItemViewType(int position) {
return list.get(position).getType();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new VH(View.inflate(context, android.R.layout.simple_list_item_2, null));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.mTextView.setText(dataList.get(position).getNum());
}
@Override
public int getItemCount() {
return dataList.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView mTextView;
public ViewHolder(View itemView) {
super(itemView);
mTextView = (TextView) itemView.findViewById(android.R.id.text1);
}
}
}
public class DemoAdapter extends BaseAdapter {
private List<Student> stuList;
private LayoutInflater inflater;
public DemoAdapter(List<Student> stuList,Context context) {
this.stuList = stuList;
this.inflater=LayoutInflater.from(context);
}
@Override
public int getCount() {
return stuList==null?0:stuList.size();
}
@Override
public Student getItem(int position) {
return stuList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
//加载布局为一个视图
ViewHolder holder;
if(convertView == null){
convertView = mInflater.inflate(R.layout.list_view, null);
holder = new ViewHolder();
holder.tv = (TextView)convertView.findViewById(R.id.text);
holder.iv = (ImageView)convertView.findViewById(R.id.img);
convertView.setTag(holder);
}else{
holder = (ViewHolder) convertView.getTag();
}
holder.tv.setText(DATA[position]);
holder.iv.setImageBitmap(icon);
return convertView;
}
static class ViewHolder{
private TextView tv;
private ImageView iv;
}
ps:与Listview的Adapter对比:
- .在Recycleview中封装了ViewHolder,即内部即支持复用,不用再手动判断ContentView及ViewHolder。
- 增加ViewType概念,即不同的ViewType对应不同的View,实现不同View的Item复用
- 将View的获取与数据填充解耦。
4.添加删除 item 的动画
同 ListView每次修改了数据源后,都要调用 notifyDataSetChanged()
刷新每项 item 类似,只不过RecyclerView 还支持局部刷新 notifyItemInserted(index);
notifyItemRemoved(position)
、notifyItemChanged(position)
。
在添加或删除了数据后,RecyclerView还提供了一个默认的动画效果,来改变显示。同时,你也可以定制自己的动画效果:模仿 DefaultItemAnimator 或直接继承这个类,实现自己的动画效果,并调用recyclerview.setItemAnimator(new DefaultItemAnimator());
设置上自己的动画。
5.Item Decoration
RecyclerView通过addItemDecoration()
方法添加item之间的分割线。但是RecycleView并并没有提供实现好的Divider,因此任何分割线样式都需要自己实现。
方法是:创建一个类并继承RecyclerView.ItemDecoration,重写以下两个方法:
- onDraw(): 绘制分割线。
- getItemOffsets(): 设置分割线的宽、高。
6.点击事件
RecyCleView默认不支持任何点击或者长按事件,好处是可以自己上实现任意的点击事件或者任意的回调,缺点是什么都要自己写。
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = View.inflate(context, android.R.layout.simple_list_item_2, null)
view.setOnclickListener(this);
return new VH(view);
}
@Override
public void onClick(View v) {
if (mOnItemClickListener != null) {
//设置该item被点击
list.get((int)v.getTag()).setClicked(true);
//注意这里使用getTag方法获取数据
mOnItemClickListener.onItemClick(v,list.get((int)v.getTag()));
notifyDataSetChanged();
}
}
public interface OnRecyclerViewItemClickListener {
void onItemClick(View view , NewsInfo data);
}
5.缓存分析
源码十分复杂,所以主要结合缓存机制来讲他的一些源代码。
不管是ListView还是RecycleView,缓存机制大致可以描述为下图:View本身拥有一个缓存池,对于滑出屏幕的itemView,我们会将他放入缓存池中,在展示新的itemView时,会从缓存池中拿出可以服用的itemView。
4.1 ListView回收机制
ListView为了保证Item View的复用,实现了一套回收机制,该回收机制的实现类是RecycleBin,他实现了两级缓存:
-
View[] mActiveViews
: 缓存屏幕上的View,在该缓存里的View不需要调用getView()
。 -
ArrayList<View>[] mScrapViews;
: 每个Item Type对应一个列表作为回收站,缓存由于滚动而消失的View,此处的View如果被复用,会以参数的形式传给getView()
。
接下来我们通过源码分析ListView是如何与RecycleBin交互的。其实ListView和RecyclerView的layout过程大同小异,ListView的布局函数是 layoutChildren()
,实现如下:
if (dataChanged) {
//如果数据源改变,会将所有itemView回收至ScrapView,否则放至ActiveViews
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
// Clear out old views
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
if (newSel != null) {
sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
} else {
sel = fillFromMiddle(childrenTop, childrenBottom);
}
break;
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
break;
case LAYOUT_FORCE_BOTTOM:
sel = fillUp(mItemCount - 1, childrenBottom);
adjustViewsUpOrDown();
break;
case LAYOUT_FORCE_TOP:
mFirstPosition = 0;
sel = fillFromTop(childrenTop);
adjustViewsUpOrDown();
break;
case LAYOUT_SPECIFIC:
final int selectedPosition = reconcileSelectedPosition();
sel = fillSpecific(selectedPosition, mSpecificTop);
其中 fillXxx()
实现了对Item View进行填充,这些方法内部都调用了 makeAndAddView()
,实现如下:
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
// Try to use an existing view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
其中, getActiveView()
是从mActiveViews中获取合适的View,如果获取到了,则直接返回,而不调用 obtainView()
,这也印证了如果从mActiveViews获取到了可复用的View,则不需要调用adapter中的 getView()
。
obtainView()
是从mScrapViews中获取合适的View,然后以参数形式传给了 getView()
,实现如下:
View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) {
child = mAdapter.getView(position, scrapView, this);
if (child != scrapView) {
mRecycler.addScrapView(scrapView);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
dispatchFinishTemporaryDetach(child);
}
} else {
child = mAdapter.getView(position, null, this);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}
return child;
}
4.2 RecycleView 回收机制
RecyclerView和ListView的回收机制非常相似,但是ListView是以View作为单位进行回收,RecyclerView是以ViewHolder作为单位进行回收。Recycler是RecyclerView回收机制的实现类,他实现了四级缓存:
- mAttachedScrap: 缓存在屏幕上的ViewHolder。
- mCachedViews: 缓存屏幕外的ViewHolder,默认为2个。ListView对于屏幕外的缓存都会调用
getView()
。 - mViewCacheExtensions: 需要用户定制,默认不实现。
- mRecyclerPool: 缓存池,多个RecyclerView共用。
RecycleView 的itemView回收同理也是主要在onLayout()中,主要代码如下:
@Override
protected void onLayout(boolean changed, int l, int t,
int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
void dispatchLayout() {
......
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
......
dispatchLayoutStep1();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() || mLayout.getHeight() != getHeight()) {
......
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
private void dispatchLayoutStep1(){
...
if (mState.mRunSimpleAnimations) {
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
mViewInfoStore.addToPreLayout(holder, animationInfo);
...
}
}
...
mState.mLayoutStep = State.STEP_LAYOUT;
}
step的第一步目的就是在记录View的状态,首先遍历当前所有的View依次进行处理,mItemAnimator会根据每个View的信息封装成一个ItemHolderInfo,这个ItemHolderInfo中主要包含的就是当前View的位置状态等。
private void dispatchLayoutStep2() {
...
mLayout.onLayoutChildren(mRecycler, mState);
...
mState.mLayoutStep = State.STEP_ANIMATIONS;
}
layout的第二步主要就是真正的去布局View了,前面也说过,RecyclerView的布局是由LayoutManager负责的,所以第二步的主要工作也都在LayoutManager中,由于每种布局的方式不一样,这里我们以常见的LinearLayoutManager为例。
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION ||
mPendingSavedState != null) {
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
}
...
if (mAnchorInfo.mLayoutFromEnd) {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL :
LayoutState.ITEM_DIRECTION_HEAD;
} else {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
LayoutState.ITEM_DIRECTION_TAIL;
}
...
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
...
if (mAnchorInfo.mLayoutFromEnd) {
...
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
...
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
...
fill(recycler, mLayoutState, state, false);
...
}
...
}
无论从上到下还是从下到上布局,都调用的是fill方法,我们进入fill方法来查看:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
}
return start - layoutState.mAvailable;
}
首先比较重要的函数是recycleByLayoutState,这个函数它会根据当前信息对不需要的View进行回收。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
...
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
...
}
...
layoutDecoratedWithMargins(view, left, top, right, bottom);
...
}
View next(RecyclerView.Recycler recycler) {
...
final View view = recycler.getViewForPosition(mCurrentPosition);
return view;
}
最终的实现:
View getViewForPosition(int position, boolean dryRun) {
boolean fromScrap = false;
ViewHolder holder = null;
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrap = holder != null;
}
if (holder == null) {
holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
...
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
final int type = mAdapter.getItemViewType(offsetPosition);
if (mAdapter.hasStableIds()) {
holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
}
if (holder == null && mViewCacheExtension != null) {
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
...
}
if (holder == null) { // fallback to recycler
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
}
//生成LayoutParams的代码 ...
return holder.itemView;
}
}
查找的逻辑:
- 搜索mChangedScrap,如果找到则返回相应holder。
- 搜索mAttachedScrap与mCachedViews,如果找到且holder有效则返回相应holder。
- 如果设置了mViewCacheExtension,对其调用getViewForPositionAndType方法进行获取,若该方法返回结果则生成对应的holder。
- 搜索mRecyclerPool,如果找到到则返回相应holder
- 如果上述过程都没有找到对应的holder,则执行我们熟悉的Adapter.createViewHolder(),创建新的holder实例
5.使用情景
ListView | RecyCleView | |
---|---|---|
使用场景 | 业务简单,布局简单,数据简单 | 1.列表需要动画支持2.频繁的更新3.局部更新4.多布局5.数据庞大 |
6.待整理问题
- header和footer的合适解决方案,可以动态更换
- 瀑布流