该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索》以及《深入理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相关知识,另外也借鉴了其他的优质博客,在此向各位大神表示感谢,膜拜!!!


前言

上文我们很详细的分析了ListView的使用、优化、及ListView的RecycleBin机制,读者如果对ListView不太清楚,那么请参看我的上篇博文。不过呢,Google Material Design提供的RecyclerView已经逐渐的取代ListView。RecyclerView提供了一种插拔式的体验,高度的解耦,异常的灵活,通过设置它提供的不同LayoutManager,ItemDecoration , ItemAnimator实现令人瞠目的效果。

如果说上面的理由只是大而空泛的话,那我们来看以下场景

  1. 你想控制数据的显示方式,列表显示、网格显示、瀑布流显示等等,之前你需要ListView,GridView和自定义View,而现在你可以通过RecyclerView的布局管理器LayoutManager控制
  2. 你想要控制Item间的间隔(可绘制),想自定义更多样式的分割线,之前你可以设置divider,那么现在你可以使用RecyclerView的ItemDecoration,想怎么画怎么画。
  3. 你想要控制Item增删的动画,ListView呢我们只能自己通过属性动画来操作 Item 的视图。RecyclerView可使用ItemAnimator
  4. 你想要局部刷新某个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]

  1. public class RecyclerViewDemo1Activity extends AppCompatActivity {
  2. @BindView(R.id.recycler_view)
  3. RecyclerView mRecyclerView;
  4. private List<String> mData;
  5. @Override
  6. protected void onCreate(Bundle savedInstanceState) {
  7. super.onCreate(savedInstanceState);
  8. setContentView(R.layout.activity_recycler_demo1_view);
  9. ButterKnife.bind(this);
  10. //LayoutManager必须指定,否则无法显示数据,这里指定为线性布局,
  11. mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
  12. //虚拟数据
  13. mData = createDataList();
  14. //设置Adapter必须指定,否则数据怎么显示
  15. mRecyclerView.setAdapter(new RecyclerViewDemo1Adapter(mData));
  16. }
  17. protected List<String> createDataList() {
  18. mData = new ArrayList<>();
  19. for (int i=0;i<20;i++){
  20. mData.add("这是第"+i+"个View");
  21. }
  22. return mData;
  23. }
  24. }

其对应的布局文件也很简单activity_recycler_demo1_view.xml

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout
  3. xmlns:android="http://schemas.android.com/apk/res/android"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent"
  6. android:orientation="vertical"
  7. >
  8. <android.support.v7.widget.RecyclerView
  9. android:id="@+id/recycler_view"
  10. android:layout_width="match_parent"
  11. android:layout_height="match_parent"
  12. >
  13. </android.support.v7.widget.RecyclerView>
  14. </LinearLayout>

那么我们再来看RecyclerViewDemo1Adapter

  1. /**
  2. * 与ListView的Adapter不同,RecyclerView的Adapter需要继承RecyclerView.Adapter<VH>(VH是ViewHolder的类名)
  3. * 记为RecyclerViewDemo1Adapter。
  4. * 创建ViewHolder:在RecyclerViewDemo1Adapter中创建一个继承RecyclerView.ViewHolder的静态内部类,记为ViewHolder
  5. * (RecyclerView必须使用ViewHolder模式,这里的ViewHolder实现几乎与ListView优化时所使用的ViewHolder一致)
  6. * 在RecyclerViewDemo1Adapter中实现:
  7. * ViewHolder onCreateViewHolder(ViewGroup parent, int viewType): 映射Item Layout Id,创建VH并返回。
  8. *
  9. * void onBindViewHolder(ViewHolder holder, int position): 为holder设置指定数据。
  10. *
  11. * int getItemCount(): 返回Item的个数。
  12. *
  13. * 可以看出,RecyclerView将ListView中getView()的功能拆分成了onCreateViewHolder()和onBindViewHolder()。
  14. */
  15. public class RecyclerViewDemo1Adapter extends RecyclerView.Adapter<RecyclerViewDemo1Adapter.ViewHolder> {
  16. private List<String> mData;
  17. public RecyclerViewDemo1Adapter(List<String> data) {
  18. this.mData = data;
  19. }
  20. @Override
  21. public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  22. View view = LayoutInflater
  23. .from(parent.getContext())
  24. .inflate(R.layout.item_menu_main, parent, false);
  25. return new ViewHolder(view);
  26. }
  27. @Override
  28. public void onBindViewHolder(ViewHolder holder, int position) {
  29. holder.setData(this.mData.get(position));
  30. holder.itemView.setOnClickListener(new View.OnClickListener() {
  31. @Override
  32. public void onClick(View v) {
  33. //item点击事件
  34. }
  35. });
  36. }
  37. @Override
  38. public int getItemCount() {
  39. return this.mData != null ? this.mData.size() : 0;
  40. }
  41. static class ViewHolder extends RecyclerView.ViewHolder{
  42. private TextView mTextView;
  43. public ViewHolder(View itemView) {
  44. super(itemView);
  45. mTextView = (TextView) itemView.findViewById(R.id.tv_title);
  46. }
  47. public void setData(String title) {
  48. this.mTextView.setText(title);
  49. }
  50. }
  51. }

需要注意的是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之间的分割线。

我们来看一下代码

  1. public class RecyclerViewDemo2Activity extends AppCompatActivity {
  2. @BindView(R.id.recycler_view)
  3. RecyclerView mRecyclerView;
  4. private List<String> mData;
  5. @Override
  6. protected void onCreate(Bundle savedInstanceState) {
  7. super.onCreate(savedInstanceState);
  8. setContentView(R.layout.activity_recycler_demo1_view);
  9. ButterKnife.bind(this);
  10. //LayoutManager必须指定,否则无法显示数据,这里指定为线性布局,
  11. mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
  12. //虚拟数据
  13. mData = createDataList();
  14. //设置Adapter必须指定,否则数据怎么显示
  15. mRecyclerView.setAdapter(new RecyclerViewDemo2Adapter(mData));
  16. //设置分割线
  17. mRecyclerView.addItemDecoration(
  18. new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
  19. }
  20. protected List<String> createDataList() {
  21. mData = new ArrayList<>();
  22. for (int i=0;i<20;i++){
  23. mData.add("这是第"+i+"个View");
  24. }
  25. return mData;
  26. }
  27. }

布局文件还跟上面的一致,代码也大致相同,不过我们多了一行

  1. //设置分割线
  2. mRecyclerView.addItemDecoration(
  3. new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));

这里的DividerItemDecoration是Google给了一个参考的实现类,这里我们通过分析这个例子来看如何自定义Item Decoration。

[DividerItemDecoration.java]

  1. public class DividerItemDecoration extends RecyclerView.ItemDecoration {
  2. public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
  3. public static final int VERTICAL = LinearLayout.VERTICAL;
  4. private static final int[] ATTRS = new int[]{ android.R.attr.listDivider };
  5. private Drawable mDivider;
  6. private int mOrientation;
  7. private final Rect mBounds = new Rect();
  8. /**
  9. * 创建一个可使用于LinearLayoutManager的分割线
  10. *
  11. */
  12. public DividerItemDecoration(Context context, int orientation) {
  13. final TypedArray a = context.obtainStyledAttributes(ATTRS);
  14. mDivider = a.getDrawable(0);
  15. a.recycle();
  16. setOrientation(orientation);
  17. }
  18. @Override
  19. public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
  20. if (parent.getLayoutManager() == null) {
  21. return;
  22. }
  23. if (mOrientation == VERTICAL) {
  24. drawVertical(c, parent);
  25. } else {
  26. drawHorizontal(c, parent);
  27. }
  28. }
  29. @SuppressLint("NewApi")
  30. private void drawVertical(Canvas canvas, RecyclerView parent) {
  31. canvas.save();
  32. final int left;
  33. final int right;
  34. if (parent.getClipToPadding()) {
  35. left = parent.getPaddingLeft();
  36. right = parent.getWidth() - parent.getPaddingRight();
  37. canvas.clipRect(left, parent.getPaddingTop(), right,
  38. parent.getHeight() - parent.getPaddingBottom());
  39. } else {
  40. left = 0;
  41. right = parent.getWidth();
  42. }
  43. final int childCount = parent.getChildCount();
  44. for (int i = 0; i < childCount; i++) {
  45. final View child = parent.getChildAt(i);
  46. parent.getDecoratedBoundsWithMargins(child, mBounds);
  47. final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child));
  48. final int top = bottom - mDivider.getIntrinsicHeight();
  49. mDivider.setBounds(left, top, right, bottom);
  50. mDivider.draw(canvas);
  51. }
  52. canvas.restore();
  53. }
  54. @Override
  55. public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
  56. RecyclerView.State state) {
  57. if (mOrientation == VERTICAL) {
  58. outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
  59. } else {
  60. outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
  61. }
  62. }
  63. }

首先看构造函数,构造函数中获得系统属性android:listDivider,该属性是一个Drawable对象。接着设置mOrientation,我们这里传入的是DividerItemDecoration.VERTICAL。

上面我们就说了如何添加分割线,那么作为实例,我们先看DividerItemDecoration的getItemOffsets方法

  1. @Override
  2. public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
  3. RecyclerView.State state) {
  4. if (mOrientation == VERTICAL) {
  5. outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
  6. } else {
  7. outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
  8. }
  9. }

outRect是当前item四周的间距,类似margin属性,现在设置了该item下间距为mDivider.getIntrinsicHeight()。

那么getItemOffsets()是怎么被调用的呢?

RecyclerView继承了ViewGroup,并重写了measureChild(),该方法在onMeasure()中被调用,用来计算每个child的大小,计算每个child大小的时候就需要加上getItemOffsets()设置的外间距:

  1. public void measureChild(View child, int widthUsed, int heightUsed) {
  2. final LayoutParams lp = (LayoutParams) child.getLayoutParams();
  3. final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
  4. widthUsed += insets.left + insets.right;
  5. heightUsed += insets.top + insets.bottom;
  6. ......
  7. }

也就是说getItemOffsets()方法是确定分割线的大小的(这个大小指的是高度,宽度)

那么接着onDraw()以及onDrawOver(),两者的作用是什么呢以及两者之间有什么关系呢?

  1. public class RecyclerView extends ViewGroup {
  2. @Override
  3. public void draw(Canvas c) {
  4. super.draw(c);
  5. final int count = mItemDecorations.size();
  6. for (int i = 0; i < count; i++) {
  7. mItemDecorations.get(i).onDrawOver(c, this, mState);
  8. }
  9. ......
  10. }
  11. @Override
  12. public void onDraw(Canvas c) {
  13. super.onDraw(c);
  14. final int count = mItemDecorations.size();
  15. for (int i = 0; i < count; i++) {
  16. mItemDecorations.get(i).onDraw(c, this, mState);
  17. }
  18. }
  19. }

根据View的绘制流程,首先调用RecyclerView重写的draw()方法,随后super.draw()即调用View的draw(),该方法会先调用onDraw()(这个方法在RecyclerView重写了),再调用dispatchDraw()绘制children。因此:ItemDecoration的onDraw()在绘制Item之前调用,ItemDecoration的onDrawOver()在绘制Item之后调用。

在RecyclerView的onDraw()方法中会得到分割线的数目,并循环调用其onDraw()方法,我们再来看分割线实例DividerItemDecoration的onDraw()方法

  1. @Override
  2. public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
  3. if (parent.getLayoutManager() == null) {
  4. return;
  5. }
  6. if (mOrientation == VERTICAL) {
  7. drawVertical(c, parent);
  8. } else {
  9. drawHorizontal(c, parent);
  10. }
  11. }

这里调用了drawVertical

  1. @SuppressLint("NewApi")
  2. private void drawVertical(Canvas canvas, RecyclerView parent) {
  3. canvas.save();
  4. final int left;
  5. final int right;
  6. if (parent.getClipToPadding()) {
  7. left = parent.getPaddingLeft();
  8. right = parent.getWidth() - parent.getPaddingRight();
  9. canvas.clipRect(left, parent.getPaddingTop(), right,
  10. parent.getHeight() - parent.getPaddingBottom());
  11. } else {
  12. left = 0;
  13. right = parent.getWidth();
  14. }
  15. final int childCount = parent.getChildCount();
  16. for (int i = 0; i < childCount; i++) {
  17. final View child = parent.getChildAt(i);
  18. parent.getDecoratedBoundsWithMargins(child, mBounds);
  19. final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child));
  20. final int top = bottom - mDivider.getIntrinsicHeight();
  21. mDivider.setBounds(left, top, right, bottom);
  22. mDivider.draw(canvas);
  23. }
  24. canvas.restore();
  25. }

drawVertical的逻辑比较简单,重要的代码

  1. //为分割线设置bounds
  2. mDivider.setBounds(left, top, right, bottom);
  3. //画出来
  4. mDivider.draw(canvas);

小结####

在RecyclerView中添加分割线需要的操作已经在上文中比较详细的说明了,这里再总结一下。我们在为RecyclerView添加分割线的时候使用

  1. //设置分割线
  2. mRecyclerView.addItemDecoration(
  3. new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));

其中addItemDecoration方法的参数即为分割线的实例,那么如何创建分割线呢,

创建一个类并继承RecyclerView.ItemDecoration,重写以下两个方法:

  • onDraw()或者onDrawOver: 绘制分割线。
  • getItemOffsets(): 设置分割线的宽、高。

为RecyclerView添加HeaderView以及FooterView###

基本功能设计####

RecyclerView没有提供类似ListView的addHeaderView或者addFooterView方法,所以我们要自己实现。关于实现的方法也有很多种。目前网上能搜到的主流解决办法是在Adapter中重写getItemViewType方法为头部或者底部布局生成特定的item。从而实现头部布局以及底部布局。

本篇的解决办法与上面的并无本质上的不同,只是我们在Adapter的外面再包上一层,以类似装饰者设计模式的方式对Adapter进行无侵入式的包装。

我们希望使用的方式比较简单

  1. //这个是真正的Adapter,在本例中不需要对其改变
  2. mAdapter = new RecyclerViewDemo2Adapter(mData);
  3. //包装的wrapper,对Adapter进行包装。实现添加Header以及Footer等的功能
  4. mHeaderAndFooterWrapper = new HeaderAndFooterWrapper(mAdapter);
  5. TextView t1 = new TextView(this);
  6. t1.setText("Header 1");
  7. TextView t2 = new TextView(this);
  8. t2.setText("Header 2");
  9. mHeaderAndFooterWrapper.addHeaderView(t1);
  10. mHeaderAndFooterWrapper.addHeaderView(t2);
  11. mRecyclerView.setAdapter(mHeaderAndFooterWrapper);
  12. mHeaderAndFooterWrapper.notifyDataSetChanged();

我们下面先对HeaderAndFooterWrapper基本功能

  1. public class HeaderAndFooterWrapper<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder>
  2. {
  3. //以较高的数值作为基数,每一个Header或者Footer对应不同的数值
  4. private static final int BASE_ITEM_TYPE_HEADER = 100000;
  5. private static final int BASE_ITEM_TYPE_FOOTER = 200000;
  6. //存储Header和Footer的集合
  7. private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();
  8. private SparseArrayCompat<View> mFootViews = new SparseArrayCompat<>();
  9. //内部的真正的Adapter
  10. private RecyclerView.Adapter mInnerAdapter;
  11. public HeaderAndFooterWrapper(RecyclerView.Adapter adapter)
  12. {
  13. mInnerAdapter = adapter;
  14. }
  15. private boolean isHeaderViewPos(int position)
  16. {
  17. return position < getHeadersCount();
  18. }
  19. private boolean isFooterViewPos(int position)
  20. {
  21. return position >= getHeadersCount() + getRealItemCount();
  22. }
  23. public void addHeaderView(View view)
  24. {
  25. mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
  26. }
  27. public void addFootView(View view)
  28. {
  29. mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
  30. }
  31. public int getHeadersCount()
  32. {
  33. return mHeaderViews.size();
  34. }
  35. public int getFootersCount()
  36. {
  37. return mFootViews.size();
  38. }
  39. }

我们这里使用SparseArrayCompat作为存储Header和Footer的集合,SparseArrayCompat有什么特点呢?它类似于Map,只不过在某些情况下比Map的性能要好,并且只能存储key为int的情况。

我们这里可以看到HeaderAndFooterWrapper是继承于RecyclerView.Adapter<RecyclerView.ViewHolder>的,这里为什么要这么做呢,当然为了可扩展性,假如RecyclerView需要添加HeaderView或者FooterView,同时呢又需要为其设置空View,此时添加HeaderView或者FooterView可使用HeaderAndFooterWrapper,同时呢我们可以使用EmptyWrapper为RecyclerView设置空View。如下例

  1. //真正进行数据处理以及展示的Adapter
  2. mAdapter = new RecyclerViewDemo2Adapter(mData);
  3. //添加Header以及Footer的wrapper
  4. mHeaderAndFooterWrapper = new HeaderAndFooterWrapper(mAdapter);
  5. //设置空View的wrapper
  6. mEmptyWrapperAdapter = new EmptyWrapper(mHeaderAndFooterWrapper);
  7. mRecyclerView.setAdapter(mEmptyWrapperAdapter);

重写相关方法####

  1. public class HeaderAndFooterWrapper<T> extends
  2. RecyclerView.Adapter<RecyclerView.ViewHolder>
  3. {
  4. private static final int BASE_ITEM_TYPE_HEADER = 100000;
  5. private static final int BASE_ITEM_TYPE_FOOTER = 200000;
  6. //SparseArrayCompat类似于Map,其用法与map相似
  7. private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();
  8. private SparseArrayCompat<View> mFootViews = new SparseArrayCompat<>();
  9. private RecyclerView.Adapter mInnerAdapter;
  10. public HeaderAndFooterWrapper(RecyclerView.Adapter adapter)
  11. {
  12. mInnerAdapter = adapter;
  13. }
  14. /**
  15. * 重写onCreateViewHolder,创建ViewHolder
  16. * @param parent 父容器,这里指的是RecyclerView
  17. * @param viewType view的类型,用int表示,也是SparseArrayCompat的key
  18. * @return
  19. */
  20. @Override
  21. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
  22. {
  23. if (mHeaderViews.get(viewType) != null)
  24. {//如果以viewType为key获取的View为null
  25. //创建ViewHolder并返回
  26. ViewHolder holder = new ViewHolder(parent.getContext(), mHeaderViews.get(viewType));
  27. return holder;
  28. } else if (mFootViews.get(viewType) != null)
  29. {
  30. ViewHolder holder = new ViewHolder(parent.getContext(), mFootViews.get(viewType));
  31. return holder;
  32. }
  33. return mInnerAdapter.onCreateViewHolder(parent, viewType);
  34. }
  35. /**
  36. * 获得对应position的type
  37. * @param position
  38. * @return
  39. */
  40. @Override
  41. public int getItemViewType(int position)
  42. {
  43. if (isHeaderViewPos(position))
  44. {
  45. return mHeaderViews.keyAt(position);
  46. } else if (isFooterViewPos(position))
  47. {
  48. return mFootViews.keyAt(position - getHeadersCount() - getRealItemCount());
  49. }
  50. return mInnerAdapter.getItemViewType(position - getHeadersCount());
  51. }
  52. private int getRealItemCount()
  53. {
  54. return mInnerAdapter.getItemCount();
  55. }
  56. /**
  57. * 绑定数据
  58. * @param holder
  59. * @param position
  60. */
  61. @Override
  62. public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
  63. {
  64. if (isHeaderViewPos(position))
  65. {
  66. return;
  67. }
  68. if (isFooterViewPos(position))
  69. {
  70. return;
  71. }
  72. mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
  73. }
  74. /**
  75. * 得到item数量 (包括头部布局数量和尾部布局数量)
  76. * @return
  77. */
  78. @Override
  79. public int getItemCount()
  80. {
  81. return getHeadersCount() + getFootersCount() + getRealItemCount();
  82. }
  83. private boolean isHeaderViewPos(int position)
  84. {
  85. return position < getHeadersCount();
  86. }
  87. private boolean isFooterViewPos(int position)
  88. {
  89. return position >= getHeadersCount() + getRealItemCount();
  90. }
  91. /**
  92. *以mHeaderViews.size() + BASE_ITEM_TYPE_HEADER为key,头部布局View为Value
  93. *放入mHeaderViews
  94. */
  95. public void addHeaderView(View view)
  96. {
  97. mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
  98. }
  99. public void addFootView(View view)
  100. {
  101. mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
  102. }
  103. public int getHeadersCount()
  104. {
  105. return mHeaderViews.size();
  106. }
  107. public int getFootersCount()
  108. {
  109. return mFootViews.size();
  110. }
  111. class ViewHolder extends RecyclerView.ViewHolder {
  112. private View mConvertView;
  113. private Context mContext;
  114. public ViewHolder(Context context, View itemView) {
  115. super(itemView);
  116. mContext = context;
  117. mConvertView = itemView;
  118. }
  119. }
  120. }

看上面的代码,HeaderAndFooterWrapper继承于RecyclerView.Adapter<RecyclerView.ViewHolder>,需要重写相关方法,

  1. /**
  2. * 重写onCreateViewHolder,创建ViewHolder
  3. * @param parent 父容器,这里指的是RecyclerView
  4. * @param viewType view的类型,用int表示,也是SparseArrayCompat的key
  5. * @return
  6. */
  7. @Override
  8. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
  9. {
  10. if (mHeaderViews.get(viewType) != null)
  11. {//如果以viewType为key获取的View为null
  12. //创建ViewHolder并返回
  13. ViewHolder holder = new ViewHolder(parent.getContext(), mHeaderViews.get(viewType));
  14. return holder;
  15. } else if (mFootViews.get(viewType) != null)
  16. {
  17. ViewHolder holder = new ViewHolder(parent.getContext(), mFootViews.get(viewType));
  18. return holder;
  19. }
  20. return mInnerAdapter.onCreateViewHolder(parent, viewType);
  21. }

我们先看onCreateViewHolder方法,该方法返回ViewHolder,我们在其中为头部以及底部布局单独创建ViewHolder,对于普通的item,我们依然调用内部的mInnerAdapter的onCreateViewHolder方法

创建好ViewHolder后,便进行绑定的工作了

  1. /**
  2. * 绑定数据
  3. * @param holder
  4. * @param position
  5. */
  6. @Override
  7. public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
  8. {
  9. if (isHeaderViewPos(position))
  10. {
  11. return;
  12. }
  13. if (isFooterViewPos(position))
  14. {
  15. return;
  16. }
  17. mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
  18. }

这里我们头部以及底部布局不进行数据的绑定,其他普通的item依然调用内部真正的mInnerAdapter.onBindViewHolder

运行结果如下

适配GridLayoutManager####

上面我们已经初步实现为RecyclerView添加Header以及Footer了,不过上面的我们的布局模式是LinearyLayoutManager,当我们使用GridLayoutManager时,效果就不是我们所想像的那样了

  1. //设置GridLayoutManager
  2. mRecyclerView.setLayoutManager(new GridLayoutManager(this,3));

当我们设置GridLayoutManager时,可以看到头部布局所展示的样子,头部布局还真的被当做一个普通的item布局了。那么我们需要为这个布局做一些特殊处理。我们知道使用GridLayoutManager的SpanSizeLookup设置某个Item所占空间

在我们的HeaderAndFooterWrapper中重写onAttachedToRecyclerView方法(该方法在Adapter与RecyclerView相关联时回调),如下:

  1. @Override
  2. public void onAttachedToRecyclerView(RecyclerView recyclerView)
  3. {
  4. mInnerAdapter.onAttachedToRecyclerView(recyclerView);
  5. RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
  6. if (layoutManager instanceof GridLayoutManager)
  7. {
  8. final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
  9. final GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();
  10. gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup()
  11. {
  12. @Override
  13. public int getSpanSize(int position)
  14. {
  15. int viewType = getItemViewType(position);
  16. if (mHeaderViews.get(viewType) != null)
  17. {
  18. return layoutManager.getSpanCount();
  19. } else if (mFootViews.get(viewType) != null)
  20. {
  21. return layoutManager.getSpanCount();
  22. }
  23. if (spanSizeLookup != null)
  24. return spanSizeLookup.getSpanSize(position);
  25. return 1;
  26. }
  27. });
  28. gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount());
  29. }
  30. }

当发现layoutManager为GridLayoutManager时,通过设置SpanSizeLookup,对其getSpanSize方法,返回值设置为layoutManager.getSpanCount();

适配StaggeredGridLayoutManager####

  1. mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(3,
  2. OrientationHelper.VERTICAL));

当我们设置StaggeredGridLayoutManager时,可以看到如下效果

而针对于StaggeredGridLayoutManager,我们需要使用 StaggeredGridLayoutManager.LayoutParams

在我们的HeaderAndFooterWrapper中重写onViewAttachedToWindow方法(该方法在Adapter与RecyclerView相关联时回调),如下:

  1. @Override
  2. public void onViewAttachedToWindow(RecyclerView.ViewHolder holder)
  3. {
  4. mInnerAdapter.onViewAttachedToWindow(holder);
  5. int position = holder.getLayoutPosition();
  6. if (isHeaderViewPos(position) || isFooterViewPos(position))
  7. {
  8. ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
  9. if (lp != null
  10. && lp instanceof StaggeredGridLayoutManager.LayoutParams)
  11. {
  12. StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
  13. p.setFullSpan(true);
  14. }
  15. }
  16. }

为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]

  1. @Override
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  3. TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
  4. dispatchLayout();
  5. TraceCompat.endSection();
  6. mFirstLayoutComplete = true;
  7. }
  8. void dispatchLayout() {
  9. if (mAdapter == null) {
  10. Log.e(TAG, "No adapter attached; skipping layout");
  11. // leave the state in START
  12. return;
  13. }
  14. if (mLayout == null) {
  15. Log.e(TAG, "No layout manager attached; skipping layout");
  16. // leave the state in START
  17. return;
  18. }
  19. mState.mIsMeasuring = false;
  20. if (mState.mLayoutStep == State.STEP_START) {
  21. //1 没有执行过布局流程的情况
  22. dispatchLayoutStep1();
  23. mLayout.setExactMeasureSpecsFrom(this);
  24. dispatchLayoutStep2();
  25. } else if (mAdapterHelper.hasUpdates()
  26. || mLayout.getWidth() != getWidth() ||
  27. mLayout.getHeight() != getHeight()) {
  28. //2 执行过布局流程,但是之后size又有变化的情况
  29. mLayout.setExactMeasureSpecsFrom(this);
  30. dispatchLayoutStep2();
  31. } else {
  32. //3 执行过布局流程,可以直接使用之前数据的情况
  33. mLayout.setExactMeasureSpecsFrom(this);
  34. }
  35. dispatchLayoutStep3();
  36. }

不过,无论什么情况,最终都是完成dispatchLayoutStep1,dispatchLayoutStep2和dispatchLayoutStep3这三步,这样的情况区分只是为了避免重复计算。

其中第二步的dispatchLayoutStep2是真正的布局!

  1. private void dispatchLayoutStep2() {
  2. ...... // 设置状态
  3. mState.mInPreLayout = false; // 更改此状态,确保不是会执行上一布局操作
  4. // 真正布局就是这一句话,布局的具体策略交给了LayoutManager
  5. mLayout.onLayoutChildren(mRecycler, mState);
  6. ......// 设置和恢复状态
  7. }

由上面的代码可以知道布局的具体操作都交给了具体的LayoutManager,那我们来分析其中的LinearLayoutManager

[LinearLayoutManager.java]

  1. /**
  2. *LinearLayoutManager的onLayoutChildren方法代码也比较多,这里也不进行逐行分析
  3. *只来看关键的几个点
  4. */
  5. @Override
  6. public void onLayoutChildren(RecyclerView.Recycler recycler,
  7. RecyclerView.State state) {
  8. ......
  9. //状态判断以及一些准备操作
  10. onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
  11. /**
  12. *1 感觉这个函数应该跟上一篇我们所分析的ListView的detachAllViewsFromParent();有点像
  13. */
  14. detachAndScrapAttachedViews(recycler);
  15. ......
  16. //2 感觉这个函数跟上一篇我们所分析的ListView的fillUp有点像
  17. fill(recycler, mLayoutState, state, false);
  18. }

上面已经给出了真正布局的代码。我们还是按照上一篇的思路来分析,两次layout

第1次layout

第1个重要函数

[RecyclerView$LayoutManager]

  1. /**
  2. *暂时detach和scrap所有当前附加的子视图。视图将被丢弃到给定的回收器中(即参数recycler)。
  3. *回收器(即Recycler)可能更喜欢重用scrap的视图。
  4. *
  5. * @param recycler 指定的回收器Recycler
  6. */
  7. public void detachAndScrapAttachedViews(Recycler recycler) {
  8. final int childCount = getChildCount();
  9. for (int i = childCount - 1; i >= 0; i--) {
  10. final View v = getChildAt(i);
  11. scrapOrRecycleView(recycler, i, v);
  12. }
  13. }

第1次layout时,RecyclerView并没有Child,所以跳过该函数,不过我们从上面的代码注释也知道了该函数跟缓存Recycler有关

第2个重要函数 ####

[LinearLayoutManager.java]

  1. int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
  2. RecyclerView.State state, boolean stopOnFocusable) {
  3. ......
  4. int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
  5. LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
  6. while ((layoutState.mInfinite || remainingSpace > 0)
  7. && layoutState.hasMore(state)) {//这里循环判断是否还有空间放置item
  8. ......
  9. //真正放置的代码放到了这里
  10. layoutChunk(recycler, state, layoutState, layoutChunkResult);
  11. ......
  12. }
  13. if (DEBUG) {
  14. validateChildOrder();
  15. }
  16. return start - layoutState.mAvailable;
  17. }

跟进layoutChunk

[LinearLayoutManager.java]

  1. void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
  2. LayoutState layoutState, LayoutChunkResult result) {
  3. /**
  4. *获取一个View,这个函数应该是重点了,
  5. */
  6. View view = layoutState.next(recycler);
  7. ......
  8. //添加View
  9. addView(view);
  10. ......
  11. //计算View的大小
  12. measureChildWithMargins(view, 0, 0);
  13. ......
  14. //布局
  15. layoutDecoratedWithMargins(view, left, top, right, bottom);
  16. ......
  17. }

跟进next()

[LinearLayoutManager$LayoutState]

  1. View next(RecyclerView.Recycler recycler) {
  2. if (mScrapList != null) {
  3. return nextViewFromScrapList();
  4. }
  5. final View view = recycler.getViewForPosition(mCurrentPosition);
  6. mCurrentPosition += mItemDirection;
  7. return view;
  8. }

getViewForPosition方法可以说是RecyclerView中缓存策略最重要的方法,该方法是从RecyclerView的回收机制实现类Recycler中获取合适的View,或者新创建一个View

  1. View getViewForPosition(int position, boolean dryRun) {
  2. /**
  3. *从这个函数就能看出RecyclerView是以ViewHolder为缓存单位的些许端倪
  4. */
  5. return tryGetViewHolderForPositionByDeadline
  6. (position, dryRun, FOREVER_NS).itemView;
  7. }

跟进tryGetViewHolderForPositionByDeadline

  1. /**
  2. *试图获得给定位置的ViewHolder,无论是从
  3. *mAttachedScrap、mCachedViews、mViewCacheExtensions、mRecyclerPool、还是直接创建。
  4. *
  5. * @return ViewHolder for requested position
  6. */
  7. @Nullable
  8. ViewHolder tryGetViewHolderForPositionByDeadline(int position,
  9. boolean dryRun, long deadlineNs) {
  10. ......
  11. // 1) 尝试从mAttachedScrap获取
  12. if (holder == null) {
  13. holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
  14. ......
  15. }
  16. if (holder == null) {
  17. ......
  18. final int type = mAdapter.getItemViewType(offsetPosition);
  19. // 2) 尝试从mCachedViews获取
  20. if (mAdapter.hasStableIds()) {
  21. holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
  22. type, dryRun);
  23. if (holder != null) {
  24. // update position
  25. holder.mPosition = offsetPosition;
  26. fromScrapOrHiddenOrCache = true;
  27. }
  28. }
  29. // 3) 尝试从mViewCacheExtensions获取
  30. if (holder == null && mViewCacheExtension != null) {
  31. ......
  32. final View view = mViewCacheExtension
  33. .getViewForPositionAndType(this, position, type);
  34. if (view != null) {
  35. holder = getChildViewHolder(view);
  36. ......
  37. }
  38. }
  39. // 4) 尝试从mRecyclerPool获取
  40. if (holder == null) { // fallback to pool
  41. holder = getRecycledViewPool().getRecycledView(type);
  42. if (holder != null) {
  43. holder.resetInternal();
  44. if (FORCE_INVALIDATE_DISPLAY_LIST) {
  45. invalidateDisplayListInt(holder);
  46. }
  47. }
  48. }
  49. if (holder == null) {
  50. // 5) 直接创建
  51. holder = mAdapter.createViewHolder(RecyclerView.this, type);
  52. }
  53. }
  54. ......
  55. // 6) 判断是否需要bindHolder
  56. if (!holder.isBound()
  57. || holder.needsUpdate()
  58. || holder.isInvalid()) {
  59. final int offsetPosition = mAdapterHelper.findPositionOffset(position);
  60. bound = tryBindViewHolderByDeadline
  61. (holder, offsetPosition, position, deadlineNs);
  62. }
  63. ......
  64. return holder;
  65. }

那么在第1次layout时,,前4步都不能获得ViewHolder,那么进入第5, 直接创建

  1. holder = mAdapter.createViewHolder(RecyclerView.this, type);
  2. public final VH createViewHolder(ViewGroup parent, int viewType) {
  3. TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
  4. //这里终于看到我们的亲人onCreateViewHolder
  5. final VH holder = onCreateViewHolder(parent, viewType);
  6. holder.mItemViewType = viewType;
  7. TraceCompat.endSection();
  8. return holder;
  9. }

这个onCreateViewHolder正是在RecyclerViewDemo1Adapter中我们重写的

  1. @Override
  2. public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  3. Log.d(TAG,"onCreateViewHolder->viewtype"+viewType);
  4. View view = LayoutInflater
  5. .from(parent.getContext())
  6. .inflate(R.layout.item_menu_main, parent, false);
  7. return new ViewHolder(view);
  8. }

初次创建了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的更多相关文章

  1. Android开发之漫漫长途 XIV——RecyclerView

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...

  2. Android开发之漫漫长途 XVI——ListView与RecyclerView项目实战

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...

  3. Android开发之漫漫长途 番外篇——内存泄漏分析与解决

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...

  4. Android开发之漫漫长途 XIV——ListView

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...

  5. Android开发之漫漫长途 Ⅰ——Android系统的创世之初以及Activity的生命周期

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>中的相关知识,再次表示该书 ...

  6. Android开发之漫漫长途 Ⅱ——Activity的显示之Window和View(2)

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...

  7. Android开发之漫漫长途 Ⅱ——Activity的显示之Window和View(1)

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...

  8. Android开发之漫漫长途 Ⅳ——Activity的显示之ViewRootImpl初探

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...

  9. Android开发之漫漫长途 Ⅴ——Activity的显示之ViewRootImpl的PreMeasure、WindowLayout、EndMeasure、Layout、Draw

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...

随机推荐

  1. redis for lack of backlog

    版本: redis-3.2.9 部署: 5台64G内存的物理机,每台机器启动2个redis进程组成5主5备集群,每台机器1个主1个备,并且错开互备. 问题: 发现redis进程占用内存高达40G,而且 ...

  2. 《mysql必知必会》学习_第三章_20180724_欢

    P16: use crashcourse; #选择数据库#使用crashcouse这个数据库,因为我没有crashcourse这个数据库,所以用我的hh数据库代替. P17: show databas ...

  3. Eclipse运行wordcount步骤

    Eclipse运行wordcount步骤 第一步:建立工程,导入代码. 第二步:建立文件写入数据(以空格分开),并上传到hdfs上. 1.创建文件并写入数据: 2.上传hdfs 在hadoop权限下就 ...

  4. wordpress和数据库的连接

    1.首先在数据库里创建wordpress数据库 2.在网页上配置WordPress,安装WordPress 如上配置不对,提交时提示了错误,于是我选择了root用户 123456, 3.提交后,连上了 ...

  5. Android-Kotlin-枚举enum

    案例一 星期: 星期的枚举:enum class 类名 {} package cn.kotlin.kotlin_oop09 /** * 定义星期的枚举类 */ enum class MyEnumera ...

  6. dispatch_async 和dispatch_sync

    dispatch_sync(),同步添加操作.他是等待添加进队列里面的操作完成之后再继续执行. dispatch_queue_t concurrentQueue = dispatch_queue_cr ...

  7. GitHub 教程【转】

    @import url(http://i.cnblogs.com/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/c ...

  8. 关于c#的知识博客

    C#.NET Request相关操作:获取IP.端口其他一些: http://www.cnblogs.com/moss_tan_jun/archive/2011/02/10/1950575.html

  9. react-native项目之样式总结

    react native(以下简称rn)里面涉及到的样式规则都是在js文件中写的,一改pc端的在样式文件中定义规则,所以pc端开发习惯的童鞋转向开发rn项目时,可能会对样式感到有些奇怪:其实react ...

  10. 《Python黑帽子:黑客与渗透测试编程之道》 玩转浏览器

    基于浏览器的中间人攻击: #coding=utf-8 import win32com.client import time import urlparse import urllib data_rec ...