Android开发之漫漫长途 XV——RecyclerView
该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索》以及《深入理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相关知识,另外也借鉴了其他的优质博客,在此向各位大神表示感谢,膜拜!!!
前言
上文我们很详细的分析了ListView的使用、优化、及ListView的RecycleBin机制,读者如果对ListView不太清楚,那么请参看我的上篇博文。不过呢,Google Material Design提供的RecyclerView已经逐渐的取代ListView。RecyclerView提供了一种插拔式的体验,高度的解耦,异常的灵活,通过设置它提供的不同LayoutManager,ItemDecoration , ItemAnimator实现令人瞠目的效果。
如果说上面的理由只是大而空泛的话,那我们来看以下场景
- 你想控制数据的显示方式,列表显示、网格显示、瀑布流显示等等,之前你需要ListView,GridView和自定义View,而现在你可以通过RecyclerView的布局管理器LayoutManager控制
- 你想要控制Item间的间隔(可绘制),想自定义更多样式的分割线,之前你可以设置divider,那么现在你可以使用RecyclerView的ItemDecoration,想怎么画怎么画。
- 你想要控制Item增删的动画,ListView呢我们只能自己通过属性动画来操作 Item 的视图。RecyclerView可使用ItemAnimator
- 你想要局部刷新某个Item,对于ListView来说,我们知道notifyDataSetChanged 来通知视图更新变化,但是该方法会重绘每个Item,而对于RecyclerView.Adapter 则提供了 notifyItemChanged 用于更新单个 Item View 的刷新,我们可以省去自己写局部更新的工作。
除了上述场景外,RecyclerView强制使用了ViewHolder模式,我们知道ListView使用ViewHolder来进行性能优化,但是这不是必须得,但是在RecyclerView中是必须的,另外RecyclerView还有许多优势,这里就不一一列举了,总体来说现在越来越多的项目使用RecyclerView,许多老旧项目也渐渐使用RecyclerView来替代ListView。
注:当我们想要一个列表显示控件的时候,需要支持动画,或者频繁更新,局部刷新,建议使用RecyclerView,更加强大完善,易扩展;其他情况下ListView在使用上反而更加方便,快捷。
前言我们就讲到这,那么我们来进入正题。
RecyclerView的使用
作为一个“新”控件,RecyclerView的使用有许多需要注意的地方
RecyclerView的简单使用
一样的我们新建一个Demo来演示RecyclerView的使用
[RecyclerViewDemo1Activity.java]
public class RecyclerViewDemo1Activity extends AppCompatActivity {
@BindView(R.id.recycler_view)
RecyclerView mRecyclerView;
private List<String> mData;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recycler_demo1_view);
ButterKnife.bind(this);
//LayoutManager必须指定,否则无法显示数据,这里指定为线性布局,
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
//虚拟数据
mData = createDataList();
//设置Adapter必须指定,否则数据怎么显示
mRecyclerView.setAdapter(new RecyclerViewDemo1Adapter(mData));
}
protected List<String> createDataList() {
mData = new ArrayList<>();
for (int i=0;i<20;i++){
mData.add("这是第"+i+"个View");
}
return mData;
}
}
其对应的布局文件也很简单activity_recycler_demo1_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
</android.support.v7.widget.RecyclerView>
</LinearLayout>
那么我们再来看RecyclerViewDemo1Adapter
/**
* 与ListView的Adapter不同,RecyclerView的Adapter需要继承RecyclerView.Adapter<VH>(VH是ViewHolder的类名)
* 记为RecyclerViewDemo1Adapter。
* 创建ViewHolder:在RecyclerViewDemo1Adapter中创建一个继承RecyclerView.ViewHolder的静态内部类,记为ViewHolder
* (RecyclerView必须使用ViewHolder模式,这里的ViewHolder实现几乎与ListView优化时所使用的ViewHolder一致)
* 在RecyclerViewDemo1Adapter中实现:
* ViewHolder onCreateViewHolder(ViewGroup parent, int viewType): 映射Item Layout Id,创建VH并返回。
*
* void onBindViewHolder(ViewHolder holder, int position): 为holder设置指定数据。
*
* int getItemCount(): 返回Item的个数。
*
* 可以看出,RecyclerView将ListView中getView()的功能拆分成了onCreateViewHolder()和onBindViewHolder()。
*/
public class RecyclerViewDemo1Adapter extends RecyclerView.Adapter<RecyclerViewDemo1Adapter.ViewHolder> {
private List<String> mData;
public RecyclerViewDemo1Adapter(List<String> data) {
this.mData = data;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater
.from(parent.getContext())
.inflate(R.layout.item_menu_main, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.setData(this.mData.get(position));
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//item点击事件
}
});
}
@Override
public int getItemCount() {
return this.mData != null ? this.mData.size() : 0;
}
static class ViewHolder extends RecyclerView.ViewHolder{
private TextView mTextView;
public ViewHolder(View itemView) {
super(itemView);
mTextView = (TextView) itemView.findViewById(R.id.tv_title);
}
public void setData(String title) {
this.mTextView.setText(title);
}
}
}
需要注意的是RecyclerView没有提供如ListView的setOnItemClickListener或者setOnItemLongClickListener之类的Item点击事件,我们必须自己去实现该部分功能,实现的方法有很多种,也比较容易,本例中采用在Adapter中BindViewHolder绑定数据的时候为item设置了点击事件。
小结###
RecyclerView的四大组成分别是:
- Adapter:为Item提供数据。必须提供,关于Adapter我们上面的代码注释已经说的很明白了
- Layout Manager:Item的布局。必须提供,我们需要为RecyclerView指定一个布局管理器
- Item Animator:添加、删除Item动画。可选提供,默认是DefaultItemAnimator
- Item Decoration:Item之间的Divider。可选提供,默认是空
所以上面代码的运行结果看起来像是是一个没有分割线的ListView
RecyclerView的进阶使用
上面的基本使用我们是会了,而且点击Item也有反应了,不过巨丑无比啊有木有。起码的分割线都没有,真无语
为RecyclerView添加分割线###
那么如何创建分割线呢,
创建一个类并继承RecyclerView.ItemDecoration,重写以下两个方法:
- onDraw()或者onDrawOver: 绘制分割线。
- getItemOffsets(): 设置分割线的宽、高。
然后使用RecyclerView通过addItemDecoration()方法添加item之间的分割线。
我们来看一下代码
public class RecyclerViewDemo2Activity extends AppCompatActivity {
@BindView(R.id.recycler_view)
RecyclerView mRecyclerView;
private List<String> mData;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recycler_demo1_view);
ButterKnife.bind(this);
//LayoutManager必须指定,否则无法显示数据,这里指定为线性布局,
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
//虚拟数据
mData = createDataList();
//设置Adapter必须指定,否则数据怎么显示
mRecyclerView.setAdapter(new RecyclerViewDemo2Adapter(mData));
//设置分割线
mRecyclerView.addItemDecoration(
new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
}
protected List<String> createDataList() {
mData = new ArrayList<>();
for (int i=0;i<20;i++){
mData.add("这是第"+i+"个View");
}
return mData;
}
}
布局文件还跟上面的一致,代码也大致相同,不过我们多了一行
//设置分割线
mRecyclerView.addItemDecoration(
new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
这里的DividerItemDecoration是Google给了一个参考的实现类,这里我们通过分析这个例子来看如何自定义Item Decoration。
[DividerItemDecoration.java]
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
public static final int VERTICAL = LinearLayout.VERTICAL;
private static final int[] ATTRS = new int[]{ android.R.attr.listDivider };
private Drawable mDivider;
private int mOrientation;
private final Rect mBounds = new Rect();
/**
* 创建一个可使用于LinearLayoutManager的分割线
*
*/
public DividerItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(orientation);
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() == null) {
return;
}
if (mOrientation == VERTICAL) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
@SuppressLint("NewApi")
private void drawVertical(Canvas canvas, RecyclerView parent) {
canvas.save();
final int left;
final int right;
if (parent.getClipToPadding()) {
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
canvas.clipRect(left, parent.getPaddingTop(), right,
parent.getHeight() - parent.getPaddingBottom());
} else {
left = 0;
right = parent.getWidth();
}
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child));
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mOrientation == VERTICAL) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
}
首先看构造函数,构造函数中获得系统属性android:listDivider,该属性是一个Drawable对象。接着设置mOrientation,我们这里传入的是DividerItemDecoration.VERTICAL。
上面我们就说了如何添加分割线,那么作为实例,我们先看DividerItemDecoration的getItemOffsets方法
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mOrientation == VERTICAL) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
outRect是当前item四周的间距,类似margin属性,现在设置了该item下间距为mDivider.getIntrinsicHeight()。
那么getItemOffsets()是怎么被调用的呢?
RecyclerView继承了ViewGroup,并重写了measureChild(),该方法在onMeasure()中被调用,用来计算每个child的大小,计算每个child大小的时候就需要加上getItemOffsets()设置的外间距:
public void measureChild(View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
......
}
也就是说getItemOffsets()方法是确定分割线的大小的(这个大小指的是高度,宽度)
那么接着onDraw()以及onDrawOver(),两者的作用是什么呢以及两者之间有什么关系呢?
public class RecyclerView extends ViewGroup {
@Override
public void draw(Canvas c) {
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
......
}
@Override
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之后调用。
在RecyclerView的onDraw()方法中会得到分割线的数目,并循环调用其onDraw()方法,我们再来看分割线实例DividerItemDecoration的onDraw()方法
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() == null) {
return;
}
if (mOrientation == VERTICAL) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
这里调用了drawVertical
@SuppressLint("NewApi")
private void drawVertical(Canvas canvas, RecyclerView parent) {
canvas.save();
final int left;
final int right;
if (parent.getClipToPadding()) {
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
canvas.clipRect(left, parent.getPaddingTop(), right,
parent.getHeight() - parent.getPaddingBottom());
} else {
left = 0;
right = parent.getWidth();
}
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child));
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}
drawVertical的逻辑比较简单,重要的代码
//为分割线设置bounds
mDivider.setBounds(left, top, right, bottom);
//画出来
mDivider.draw(canvas);
小结####
在RecyclerView中添加分割线需要的操作已经在上文中比较详细的说明了,这里再总结一下。我们在为RecyclerView添加分割线的时候使用
//设置分割线
mRecyclerView.addItemDecoration(
new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
其中addItemDecoration方法的参数即为分割线的实例,那么如何创建分割线呢,
创建一个类并继承RecyclerView.ItemDecoration,重写以下两个方法:
- onDraw()或者onDrawOver: 绘制分割线。
- getItemOffsets(): 设置分割线的宽、高。
为RecyclerView添加HeaderView以及FooterView###
基本功能设计####
RecyclerView没有提供类似ListView的addHeaderView或者addFooterView方法,所以我们要自己实现。关于实现的方法也有很多种。目前网上能搜到的主流解决办法是在Adapter中重写getItemViewType方法为头部或者底部布局生成特定的item。从而实现头部布局以及底部布局。
本篇的解决办法与上面的并无本质上的不同,只是我们在Adapter的外面再包上一层,以类似装饰者设计模式的方式对Adapter进行无侵入式的包装。
我们希望使用的方式比较简单
//这个是真正的Adapter,在本例中不需要对其改变
mAdapter = new RecyclerViewDemo2Adapter(mData);
//包装的wrapper,对Adapter进行包装。实现添加Header以及Footer等的功能
mHeaderAndFooterWrapper = new HeaderAndFooterWrapper(mAdapter);
TextView t1 = new TextView(this);
t1.setText("Header 1");
TextView t2 = new TextView(this);
t2.setText("Header 2");
mHeaderAndFooterWrapper.addHeaderView(t1);
mHeaderAndFooterWrapper.addHeaderView(t2);
mRecyclerView.setAdapter(mHeaderAndFooterWrapper);
mHeaderAndFooterWrapper.notifyDataSetChanged();
我们下面先对HeaderAndFooterWrapper基本功能
public class HeaderAndFooterWrapper<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder>
{
//以较高的数值作为基数,每一个Header或者Footer对应不同的数值
private static final int BASE_ITEM_TYPE_HEADER = 100000;
private static final int BASE_ITEM_TYPE_FOOTER = 200000;
//存储Header和Footer的集合
private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();
private SparseArrayCompat<View> mFootViews = new SparseArrayCompat<>();
//内部的真正的Adapter
private RecyclerView.Adapter mInnerAdapter;
public HeaderAndFooterWrapper(RecyclerView.Adapter adapter)
{
mInnerAdapter = adapter;
}
private boolean isHeaderViewPos(int position)
{
return position < getHeadersCount();
}
private boolean isFooterViewPos(int position)
{
return position >= getHeadersCount() + getRealItemCount();
}
public void addHeaderView(View view)
{
mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
}
public void addFootView(View view)
{
mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
}
public int getHeadersCount()
{
return mHeaderViews.size();
}
public int getFootersCount()
{
return mFootViews.size();
}
}
我们这里使用SparseArrayCompat作为存储Header和Footer的集合,SparseArrayCompat有什么特点呢?它类似于Map,只不过在某些情况下比Map的性能要好,并且只能存储key为int的情况。
我们这里可以看到HeaderAndFooterWrapper是继承于RecyclerView.Adapter<RecyclerView.ViewHolder>的,这里为什么要这么做呢,当然为了可扩展性,假如RecyclerView需要添加HeaderView或者FooterView,同时呢又需要为其设置空View,此时添加HeaderView或者FooterView可使用HeaderAndFooterWrapper,同时呢我们可以使用EmptyWrapper为RecyclerView设置空View。如下例
//真正进行数据处理以及展示的Adapter
mAdapter = new RecyclerViewDemo2Adapter(mData);
//添加Header以及Footer的wrapper
mHeaderAndFooterWrapper = new HeaderAndFooterWrapper(mAdapter);
//设置空View的wrapper
mEmptyWrapperAdapter = new EmptyWrapper(mHeaderAndFooterWrapper);
mRecyclerView.setAdapter(mEmptyWrapperAdapter);
重写相关方法####
public class HeaderAndFooterWrapper<T> extends
RecyclerView.Adapter<RecyclerView.ViewHolder>
{
private static final int BASE_ITEM_TYPE_HEADER = 100000;
private static final int BASE_ITEM_TYPE_FOOTER = 200000;
//SparseArrayCompat类似于Map,其用法与map相似
private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();
private SparseArrayCompat<View> mFootViews = new SparseArrayCompat<>();
private RecyclerView.Adapter mInnerAdapter;
public HeaderAndFooterWrapper(RecyclerView.Adapter adapter)
{
mInnerAdapter = adapter;
}
/**
* 重写onCreateViewHolder,创建ViewHolder
* @param parent 父容器,这里指的是RecyclerView
* @param viewType view的类型,用int表示,也是SparseArrayCompat的key
* @return
*/
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
if (mHeaderViews.get(viewType) != null)
{//如果以viewType为key获取的View为null
//创建ViewHolder并返回
ViewHolder holder = new ViewHolder(parent.getContext(), mHeaderViews.get(viewType));
return holder;
} else if (mFootViews.get(viewType) != null)
{
ViewHolder holder = new ViewHolder(parent.getContext(), mFootViews.get(viewType));
return holder;
}
return mInnerAdapter.onCreateViewHolder(parent, viewType);
}
/**
* 获得对应position的type
* @param position
* @return
*/
@Override
public int getItemViewType(int position)
{
if (isHeaderViewPos(position))
{
return mHeaderViews.keyAt(position);
} else if (isFooterViewPos(position))
{
return mFootViews.keyAt(position - getHeadersCount() - getRealItemCount());
}
return mInnerAdapter.getItemViewType(position - getHeadersCount());
}
private int getRealItemCount()
{
return mInnerAdapter.getItemCount();
}
/**
* 绑定数据
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
if (isHeaderViewPos(position))
{
return;
}
if (isFooterViewPos(position))
{
return;
}
mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
}
/**
* 得到item数量 (包括头部布局数量和尾部布局数量)
* @return
*/
@Override
public int getItemCount()
{
return getHeadersCount() + getFootersCount() + getRealItemCount();
}
private boolean isHeaderViewPos(int position)
{
return position < getHeadersCount();
}
private boolean isFooterViewPos(int position)
{
return position >= getHeadersCount() + getRealItemCount();
}
/**
*以mHeaderViews.size() + BASE_ITEM_TYPE_HEADER为key,头部布局View为Value
*放入mHeaderViews
*/
public void addHeaderView(View view)
{
mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
}
public void addFootView(View view)
{
mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
}
public int getHeadersCount()
{
return mHeaderViews.size();
}
public int getFootersCount()
{
return mFootViews.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
private View mConvertView;
private Context mContext;
public ViewHolder(Context context, View itemView) {
super(itemView);
mContext = context;
mConvertView = itemView;
}
}
}
看上面的代码,HeaderAndFooterWrapper继承于RecyclerView.Adapter<RecyclerView.ViewHolder>,需要重写相关方法,
/**
* 重写onCreateViewHolder,创建ViewHolder
* @param parent 父容器,这里指的是RecyclerView
* @param viewType view的类型,用int表示,也是SparseArrayCompat的key
* @return
*/
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
if (mHeaderViews.get(viewType) != null)
{//如果以viewType为key获取的View为null
//创建ViewHolder并返回
ViewHolder holder = new ViewHolder(parent.getContext(), mHeaderViews.get(viewType));
return holder;
} else if (mFootViews.get(viewType) != null)
{
ViewHolder holder = new ViewHolder(parent.getContext(), mFootViews.get(viewType));
return holder;
}
return mInnerAdapter.onCreateViewHolder(parent, viewType);
}
我们先看onCreateViewHolder方法,该方法返回ViewHolder,我们在其中为头部以及底部布局单独创建ViewHolder,对于普通的item,我们依然调用内部的mInnerAdapter的onCreateViewHolder方法
创建好ViewHolder后,便进行绑定的工作了
/**
* 绑定数据
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
if (isHeaderViewPos(position))
{
return;
}
if (isFooterViewPos(position))
{
return;
}
mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
}
这里我们头部以及底部布局不进行数据的绑定,其他普通的item依然调用内部真正的mInnerAdapter.onBindViewHolder
运行结果如下
适配GridLayoutManager####
上面我们已经初步实现为RecyclerView添加Header以及Footer了,不过上面的我们的布局模式是LinearyLayoutManager,当我们使用GridLayoutManager时,效果就不是我们所想像的那样了
//设置GridLayoutManager
mRecyclerView.setLayoutManager(new GridLayoutManager(this,3));
当我们设置GridLayoutManager时,可以看到头部布局所展示的样子,头部布局还真的被当做一个普通的item布局了。那么我们需要为这个布局做一些特殊处理。我们知道使用GridLayoutManager的SpanSizeLookup设置某个Item所占空间
在我们的HeaderAndFooterWrapper中重写onAttachedToRecyclerView方法(该方法在Adapter与RecyclerView相关联时回调),如下:
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView)
{
mInnerAdapter.onAttachedToRecyclerView(recyclerView);
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof GridLayoutManager)
{
final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
final GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup()
{
@Override
public int getSpanSize(int position)
{
int viewType = getItemViewType(position);
if (mHeaderViews.get(viewType) != null)
{
return layoutManager.getSpanCount();
} else if (mFootViews.get(viewType) != null)
{
return layoutManager.getSpanCount();
}
if (spanSizeLookup != null)
return spanSizeLookup.getSpanSize(position);
return 1;
}
});
gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount());
}
}
当发现layoutManager为GridLayoutManager时,通过设置SpanSizeLookup,对其getSpanSize方法,返回值设置为layoutManager.getSpanCount();
适配StaggeredGridLayoutManager####
mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(3,
OrientationHelper.VERTICAL));
当我们设置StaggeredGridLayoutManager时,可以看到如下效果
而针对于StaggeredGridLayoutManager,我们需要使用 StaggeredGridLayoutManager.LayoutParams
在我们的HeaderAndFooterWrapper中重写onViewAttachedToWindow方法(该方法在Adapter与RecyclerView相关联时回调),如下:
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder)
{
mInnerAdapter.onViewAttachedToWindow(holder);
int position = holder.getLayoutPosition();
if (isHeaderViewPos(position) || isFooterViewPos(position))
{
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp != null
&& lp instanceof StaggeredGridLayoutManager.LayoutParams)
{
StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
p.setFullSpan(true);
}
}
}
为RecyclerView设置EmptyView###
上面已经详细给出了为RecyclerView添加Header以及Footer的例子,关于EmptyView的实现方法与上面基本类似,读者可自行实现,当然在本篇末会给出完整的源码地址。
RecyclerView的缓存机制
RecyclerView和ListView的回收机制非常相似,但是ListView是以View作为单位进行回收,RecyclerView是以ViewHolder作为单位进行回收。相比于ListView,RecyclerView的回收机制更为完善
Recycler是RecyclerView回收机制的实现类,他实现了四级缓存:
- mAttachedScrap: 缓存在屏幕上的ViewHolder。
- mCachedViews: 缓存屏幕外的ViewHolder,默认为2个。ListView对于屏幕外的缓存都会调用getView()。
- mViewCacheExtensions: 需要用户定制,默认不实现。
- mRecyclerPool: 缓存池,多个RecyclerView共用。
要想理解RecyclerView的回收机制,我们就必须从其数据展示谈起,我们都知道RecyclerView使用LayoutManager管理其数据布局的显示。
注:以下源码来自support-v7 25.4.0
RecyclerView$LayoutManager
LayoutManager是RecyclerView下的一个抽象类,Google提供了LinearLayoutManager,GridLayoutManager以及StaggeredGridLayoutManager基本上能满足大部分开发者的需求。这三个类的代码都非常长,这要分析下来可了不得。本篇文章只分析LinearLayoutManager的一部分内容
与分析ListView时类似,RecyclerView作为一个ViewGroup,肯定也跑不了那几大过程,我们依然还是只分析其layout过程
[RecyclerView.java]
@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() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
return;
}
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
//1 没有执行过布局流程的情况
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates()
|| mLayout.getWidth() != getWidth() ||
mLayout.getHeight() != getHeight()) {
//2 执行过布局流程,但是之后size又有变化的情况
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
//3 执行过布局流程,可以直接使用之前数据的情况
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
不过,无论什么情况,最终都是完成dispatchLayoutStep1,dispatchLayoutStep2和dispatchLayoutStep3这三步,这样的情况区分只是为了避免重复计算。
其中第二步的dispatchLayoutStep2是真正的布局!
private void dispatchLayoutStep2() {
...... // 设置状态
mState.mInPreLayout = false; // 更改此状态,确保不是会执行上一布局操作
// 真正布局就是这一句话,布局的具体策略交给了LayoutManager
mLayout.onLayoutChildren(mRecycler, mState);
......// 设置和恢复状态
}
由上面的代码可以知道布局的具体操作都交给了具体的LayoutManager,那我们来分析其中的LinearLayoutManager
[LinearLayoutManager.java]
/**
*LinearLayoutManager的onLayoutChildren方法代码也比较多,这里也不进行逐行分析
*只来看关键的几个点
*/
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler,
RecyclerView.State state) {
......
//状态判断以及一些准备操作
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
/**
*1 感觉这个函数应该跟上一篇我们所分析的ListView的detachAllViewsFromParent();有点像
*/
detachAndScrapAttachedViews(recycler);
......
//2 感觉这个函数跟上一篇我们所分析的ListView的fillUp有点像
fill(recycler, mLayoutState, state, false);
}
上面已经给出了真正布局的代码。我们还是按照上一篇的思路来分析,两次layout
第1次layout
第1个重要函数
[RecyclerView$LayoutManager]
/**
*暂时detach和scrap所有当前附加的子视图。视图将被丢弃到给定的回收器中(即参数recycler)。
*回收器(即Recycler)可能更喜欢重用scrap的视图。
*
* @param recycler 指定的回收器Recycler
*/
public void detachAndScrapAttachedViews(Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
第1次layout时,RecyclerView并没有Child,所以跳过该函数,不过我们从上面的代码注释也知道了该函数跟缓存Recycler有关
第2个重要函数 ####
[LinearLayoutManager.java]
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
......
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0)
&& layoutState.hasMore(state)) {//这里循环判断是否还有空间放置item
......
//真正放置的代码放到了这里
layoutChunk(recycler, state, layoutState, layoutChunkResult);
......
}
if (DEBUG) {
validateChildOrder();
}
return start - layoutState.mAvailable;
}
跟进layoutChunk
[LinearLayoutManager.java]
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
/**
*获取一个View,这个函数应该是重点了,
*/
View view = layoutState.next(recycler);
......
//添加View
addView(view);
......
//计算View的大小
measureChildWithMargins(view, 0, 0);
......
//布局
layoutDecoratedWithMargins(view, left, top, right, bottom);
......
}
跟进next()
[LinearLayoutManager$LayoutState]
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
getViewForPosition方法可以说是RecyclerView中缓存策略最重要的方法,该方法是从RecyclerView的回收机制实现类Recycler中获取合适的View,或者新创建一个View
View getViewForPosition(int position, boolean dryRun) {
/**
*从这个函数就能看出RecyclerView是以ViewHolder为缓存单位的些许端倪
*/
return tryGetViewHolderForPositionByDeadline
(position, dryRun, FOREVER_NS).itemView;
}
跟进tryGetViewHolderForPositionByDeadline
/**
*试图获得给定位置的ViewHolder,无论是从
*mAttachedScrap、mCachedViews、mViewCacheExtensions、mRecyclerPool、还是直接创建。
*
* @return ViewHolder for requested position
*/
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
......
// 1) 尝试从mAttachedScrap获取
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
......
}
if (holder == null) {
......
final int type = mAdapter.getItemViewType(offsetPosition);
// 2) 尝试从mCachedViews获取
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
// 3) 尝试从mViewCacheExtensions获取
if (holder == null && mViewCacheExtension != null) {
......
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
......
}
}
// 4) 尝试从mRecyclerPool获取
if (holder == null) { // fallback to pool
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
// 5) 直接创建
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
}
......
// 6) 判断是否需要bindHolder
if (!holder.isBound()
|| holder.needsUpdate()
|| holder.isInvalid()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline
(holder, offsetPosition, position, deadlineNs);
}
......
return holder;
}
那么在第1次layout时,,前4步都不能获得ViewHolder,那么进入第5, 直接创建
holder = mAdapter.createViewHolder(RecyclerView.this, type);
public final VH createViewHolder(ViewGroup parent, int viewType) {
TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
//这里终于看到我们的亲人onCreateViewHolder
final VH holder = onCreateViewHolder(parent, viewType);
holder.mItemViewType = viewType;
TraceCompat.endSection();
return holder;
}
这个onCreateViewHolder正是在RecyclerViewDemo1Adapter中我们重写的
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Log.d(TAG,"onCreateViewHolder->viewtype"+viewType);
View view = LayoutInflater
.from(parent.getContext())
.inflate(R.layout.item_menu_main, parent, false);
return new ViewHolder(view);
}
初次创建了ViewHolder之后,便进入6,导致我们重写的onBindViewHolder回调,数据与View绑定了
第2次layout
从上一篇ListView中我们就知道了再简单的View也至少需要两次Layout,在ListView中通过把屏幕的子View detach并加入mActivieViews,以避免重复添加item并可通过attach提高性能,那么在RecyclerView中,它的做法与ListView十分类似,RecyclerView也是通过detach子View,并把子View对应的ViewHolder加入其1级缓存mAttachedScrap。这部分我们就不详细分析了,读者可参照上一篇的步骤进行分析。
RecyclerView与ListView 缓存机制对比分析
ListView和RecyclerView最大的区别在于数据源改变时的缓存的处理逻辑,ListView是”一锅端”,将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。
小结
在一些场景下,如界面初始化,滑动等,ListView和RecyclerView都能很好地工作,两者并没有很大的差异,但是在需要支持动画,或者频繁更新,局部刷新,建议使用RecyclerView,更加强大完善,易扩展
本篇总结
本篇呢,我们分析了RecyclerView的使用方法以及RecyclerView部分源码。目的是为了更好的掌握RecyclerView。
这里呢再上图总结一下RecyclerView的layout流程
下篇预告
下篇呢,也是一篇干货,上面两篇文章,我们的数据都是虚拟的,静态的,而实际开发中数据通常都是从服务器动态获得的,这也产生了一系列问题,如列表的下拉刷新以及上拉加载、ListVIew异步获取图片显示错位等等问题
参考博文
http://blog.csdn.net/lmj623565791/article/details/51854533
源码地址:源码传送门
此致,敬礼
Android开发之漫漫长途 XV——RecyclerView的更多相关文章
- Android开发之漫漫长途 XIV——RecyclerView
该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...
- Android开发之漫漫长途 XVI——ListView与RecyclerView项目实战
该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...
- Android开发之漫漫长途 番外篇——内存泄漏分析与解决
该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...
- Android开发之漫漫长途 XIV——ListView
该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...
- Android开发之漫漫长途 Ⅰ——Android系统的创世之初以及Activity的生命周期
该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>中的相关知识,再次表示该书 ...
- Android开发之漫漫长途 Ⅱ——Activity的显示之Window和View(2)
该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...
- Android开发之漫漫长途 Ⅱ——Activity的显示之Window和View(1)
该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...
- Android开发之漫漫长途 Ⅳ——Activity的显示之ViewRootImpl初探
该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...
- Android开发之漫漫长途 Ⅴ——Activity的显示之ViewRootImpl的PreMeasure、WindowLayout、EndMeasure、Layout、Draw
该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...
随机推荐
- poj 2299 Ultra-QuickSort(归并排序,树状数组,线段树)
Description In this problem, you have to analyze a particular sorting algorithm. The algorithm proce ...
- session(会话)研究(一)基础
一.Session对象的生成 session对象生成的过程,可以通过一个直观图进行观察. 也就是说,客户第一次请求访问时,Cookie中是没有SessionID的.在第一次访问之后,由服务器生成一个S ...
- 1.7.6方法stop()与java.lang.threadDeath异常
调用stop方法时会抛出java.lang.ThreadDeath异常,但一般情况下这个异常不需要显示的捕捉 package com.cky.thread; /** * Created by edis ...
- 20155326刘美岑2016-2017-2《Java程序设计》第一周学习总结
20155326刘美岑 2006-2007-2 <Java程序设计>第1周学习总结 教材学习内容总结 在老师的要求下我看了这门课的考察方式.这门课和以前上的课有些差别,成绩是由平时一点一点 ...
- 学习protobuf 感想
前俩篇博文是从大牛的博客抄过来的, 写的都很好. 这里还写简单写下自己的感想: 1. 和json比, protobuff编码后的体积小很多, 这是肯定的. 都源自于protobuff内部的一系列特殊的 ...
- input.php
<?php /** * */ class Input { function get($index = NULL, $xss_clean = FALSE) { if($index == NULL ...
- 用命令行上传本地代码到GitHub
有两种方式上传,ssh和https,ssh老是报错=.=我用的是https 先下载git https://git-scm.com/downloads 在代码的文件夹的同级目录中邮件打开git ba ...
- DXP中插入LOGO图片方法(1)
DXP中插入LOGO图片方法 1.QQ截图后,打开“开始”-->"附件"——>"画图工具",如图: 2.另存为BMP文件格式(设置图片大小.黑白色即 ...
- LDO-AMS1117
AMS1117 1.是AMS艾默森公司简称.与AMS1117对应的IC有LM1117.AMS1117的最大输入电压为15V,而LM1117的极限是20V. 2.其输出电压有固定式(1.5V,1.8V, ...
- WPF TreeView IsExpanded 绑定不上的问题
最近项目上需要通过MVVM来控制TreeView,其中需要需要控制通过搜索来定位某个节点,正常逻辑下,首先通过需要在树上面找到该节点,然后选中该节点,并将该节点的父节点展开,这个时候需要通过MVVM来 ...