RecyclerView学习与总结

RecyclerView

RecyclerView,顾名思义,这个View代表的就是一个可循环使用的视图集合控件,它定义了ViewHolder类型标准,封装了View缓存判断逻辑,更强大的是它可以通过一个LayoutManager将一个RecyclerView显示为不同的样式。

目的: 在有限的屏幕内展示大量的内容。

RecyclerView的五虎将

  • RecyclerView.LayoutManager :负责Item视图的布局的显示管理
  • RecyclerView.ItemDecoration:给每一项Item视图添加子View,例如可以进行画分割线之类的
  • RecyclerView.ItemAnimator:负责处理数据添加或者删除时候的动画效果
  • RecyclerView.Adapter:为每一项Item创建视图
  • RecyclerView.ViewHolder:承载Item视图的子布局

常用方法

RecyclerView与ListView、GridView类似,都是可以显示同一类型View的集合的控件。首先看看最简单的用法,四步走:

0.介入build.gradle文件中加入

compile 'com.android.support:recyclerview-v7:24.0.0'

1.创建对象

RecyclerView recyclerview = (RecyclerView) findViewById(R.id.recyclerview);

2.设置RV的布局管理器,决定了RV的显示风格

recyclerview.setLayoutManager(new LinearLayoutManager(this, 
	LinearLayoutManager.VERTICAL, false));

RecyclerView将所有的显示规则交给LayoutManager去完成,LayoutManager是一个抽象类,系统已经为我们提供了三个默认的实现类,分别是LinearLayoutManager、GridLayoutManager、StaggerdGridlayoutManager,从名字我们就可以看出,分别是,线性显示,网格显示,瀑布流显示,当然你也可以通过继承这些类来扩展自己的LayoutManager。

3.设置适配器

recyclerview.setAdapter(adapter);

适配器,同ListView一样,用来设置每个item显示内容。通常,我们写ListView适配器,都是首先继承BaseAdapter,实现四个抽象方法(getView, getItem, getCount, getItemId),创建一个静态ViewHolder,getView()方法中判断convertView是否为空,创建还是获取viewHolder对象。

而RecyclerView也是类似的步骤,首先继承RecyclerView.Adapter类,实现三个抽象方法,创建一个静态类的ViewHolder,不过RecyclerView的ViewHolder创建稍微有些限制,类名就是上面继承的时候泛型中声明的类名,并且ViewHOder必须继承自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 VH onCreateViewHolder(ViewGroup parent, int viewType) {
    	return new VH(View.inflate(context, android.R.layout.simple_list_item_2, null));
	}

	@Override
	public void onBindViewHolder(VH holder, int position) {
    	holder.mTextView.setText(dataList.get(position).getNum());
	}

	@Override
	public int getItemCount() {
    	return dataList.size();
	}

	public static class VH extends RecyclerView.ViewHolder {
	    TextView mTextView;
    	public VH(View itemView) {
        	super(itemView);
        	mTextView = (TextView) itemView.findViewById(android.R.id.text1);
    	}
	}
}

其他方法

除了常用的方法,还有不常用的方法。

  • 瀑布流与滚动方向

前面已经介绍过了,RecyclerView实现瀑布流,可以通过一句话设置:RecyclerView.setLayoutManager(new StaggeredGridLayoutManager(2,VERTICAL))就可以了。

其中StaggerGridLayoutManager的第一个参数表示列数,就好像GridView的列数一样,第二个参数表示方向,可以很方便的实现横向滚动或者纵向滚动。

  • 添加删除item的动画

同ListView每次修改了数据后,都要调用notifyDataSetChanged刷新每项item类似,只不过RecyclerView还支持局部刷新notifyItemInserted(index);notifyItemRemoved(position);notifyItemChanged(position);

在添加或删除了数据时,recyclerView还提供了一个默认的动画效果,来改变显示,同时你也可以定制自己的动画效果:模仿DefaultItemAnimator或者直接继承这个类,实现自己的动画效果。并调用RecyclerView.setItemAnimator(new DefaultItemAnimator())设置上自己的动画。

LayoutManager的常用方法

  • findFistVisibleItemPosition();返回当前第一个可见Item的position
  • findFirstCompletelyVisibleItemPosition();返回当前第一个完全可见的Item的position
  • findLastVisibleItemPosition();返回当前最后一个可见Item的position
  • findComplete领了也VisivleItemPosition();返回当前最后一个完全可见Item的Position
  • ScrollBy();滚动到某个位置

LayoutManager工作原理

首先RecyclerView继承关系,可以看到与ListView不同,他是一个ViewGroup,既然是一个View,那么不可少的就是经历onMeasure()、onLayout()、onDraw()这三个方法,实际上RecyclerView就是将onMeasure()、onLayout()交给了LayoutManager去处理,因此如果给RecyclerView设置不同的LayoutManager就可以达到不同的显示效果,因为onMeasure()、onLayout()都不同。

ItemDecoration 工作原理

ItemDecoration是为了显示每个Item之间分隔样式的,它的本质上就是一个Drawable。当RecyclerView执行到onDraw()方法的时候,就会调用它的onDraw(),这时,如果你重写了这个方法,就相当于直接在RecyclerView上画了一个Drawable表现的东西,而最后,在它的内部还有一个叫getItemOffsets()的方法,从字面就可以理解,它是用来偏移每个Item视图的,当我们在每个Item视图之间强行插入一段Drawable,那么如果在照着原本的逻辑去给item视图,就会覆盖掉Decoration了,所以需要getItemOffsets()这个方法,让每个item往后偏移一点,不要覆盖到之前画的上的分个样式。

ItemAnimatior

每一个item在特定情况下都会执行的动画,说是特定情况,其实就是在视图发生改变,我们手动调用notifyxxxx()的时候,通常这个时候我们要传一个下标,那么从这个标记开始一直到结束,所有item都会被执行一次这个动画

Adapter工作原理

首先是适配器,适配器的作用都是类似的,用于提供每一个item,并返回给RecyclerView作为其子布局添加到内部,但是,与ListView不同的是:ListView的适配器是直接返回一个View,将这个View加入到ListView内部;RecyclerView是返回一个ViewHolder并且不是直接将这个holder加入到视图内部,而是加入一个缓存区域,在视图需要的时候去缓存区找到holder在间接找到holder包裹的View

ViewHolder

每一个ViewHolder的内部都是一个View,并且ViewHolder必须继承自RecyclerView.ViewHolder类,这主要是因为RecyclerView内部的缓存结构并不是像ListView那样去缓存一个View,而是直接缓存一个ViewHolder,在ViewHolder的内部又持有一个View,既然是缓存一个ViewHolder,那么当然所有ViewHolder都继承同一个类才能做到。

缓存与复用的原理

RecyclerView的内部维护了一个四级缓存,滑出界面的ViewHolder会暂时放到cache结构中,而从cache结构中移除的ViewHolder,则会放到一个叫做RecyclerViewPool的循环缓存池中。

顺带一说,RecyclerView的性能并不比ListView好多少,它的最大的优势在于其扩展性,但是有一点,在RecyclerView内部的这个第二级缓存池RecyclerViewPool是可以被多个RecyclerView公用的。这一点比起直接缓存View的ListView就要高明了很多,但是正是应为需要被多个RecyclerView公用,所以我们的ViewHolder必须继承自同一个基类。

默认的情况下,cache缓存2个holder,RecyclerViewPool缓存5个holder,对于二级缓存池中holder对象,会根据ViewType进行分类,不同类型的ViewType之间互不影响。缓存这块后面有具体分析。

源码解析

onMeasure

既然是一个自定义View,那么我们先从onMeasure()开始看

之前我们说RecyclerView的Measure和layout都交给了LayoutManager去做

	if (mLayout.mAutoMeasure) {
    	final int widthMode = MeasureSpec.getMode(widthSpec);
    	final int heightMode = MeasureSpec.getMode(heightSpec);
    	final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
            	&& heightMode == MeasureSpec.EXACTLY;
    	mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
    	// 如果RV是 精确模式,则直接使用 mLayout测量宽高即可
    	if(skipMeasure || mAdapter == null) return;
	} else {
    	mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
	}

不论是否启用mAutoMeasure最终都会调用mlayout.onMeasure()方法中,而这个mLayout就是一个LayoutManager对象

我们先挑LinearLayoutManager来看,发现并没有它的onMeasure方法,LinearLayoutManager直接继承自LayoutManager,所以又回到了父类LayoutManager中。

	void defaultOnMeasure(int widthSpec, int heightSpec) {
    	final int width = LayoutManager.chooseSize(widthSpec,
            	getPaddingLeft() + getPaddingRight(),
            	ViewCompat.getMinimumWidth(this));
    	final int height = LayoutManager.chooseSize(heightSpec,
            	getPaddingTop() + getPaddingBottom(),
            	ViewCompat.getMinimumHeight(this));

    	setMeasuredDimension(width, height);
	}

有一句非常奇葩的注释:在这里直接调用LayoutManager静态方法并不完美,因为本身就是在内部类,更好的办法调用一个单独的方法

接着是chooseSize()方法,很简单,直接根据测量值和模式返回了最适大小

	public static int chooseSize(int spec, int desired, int min) {
    	final int mode = View.MeasureSpec.getMode(spec);
    	final int size = View.MeasureSpec.getSize(spec);
    	switch (mode) {
        	case View.MeasureSpec.EXACTLY:
            	return size;
        	case View.MeasureSpec.AT_MOST:
            	return Math.min(size, Math.max(desired, min));
        	case View.MeasureSpec.UNSPECIFIED:
        	default:
            	return Math.max(desired, min);
    	}
	}

紧接着是对子控件Measure,调用了dispatchLayoutStep2(),调用了相同的方法,子控件的Measure在layout过程中讲解

onLayout

然后我们看layout过程,在onLayout()方法中间接调用到这么一个方法:dispatchLayoutStep2(),在它之中又调用到了mLaoutChildern()方法

这个方法在LayoutManager中的实现是空的,那么想必是在子类中实现的,然后找到LinearLayoutManager,根上面Measure过程一样,调用dispatchLayoutStep2()跟进去有这么一个方法:

fill(recycler, mLayoutState, state, false);

recycler,是一个全局的回收复用池,用于对每个itemView回收及其复用提供支持,

	while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore	(state)) {
	    *****
    	layoutChunk(recycler, state, layoutState, layoutChunkResult);
    	layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;

    	if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
        	layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
       	 if (layoutState.mAvailable < 0) {
            	layoutState.mScrollingOffset += layoutState.mAvailable;
        	}
        	recycleByLayoutState(recycler, layoutState);
    	}
	}
	
	viod layoutChunk() {
	    View view = layoutState.next(recycler);
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) { // 使用缓存的view,并添加进RV中
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        measureChildWithMargins(view, 0, 0); // 测量被添加到 RV 中的 item 的宽高
        。。。。。。
        // To calculate correct layout position, we subtract margins.
        // 根据所设置的 Decoration,Margins等属性确定 item 的显示位置
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        。。。。。
    }

fill()作用就是根据当前状态LayoutState决定是应该从缓存池中取itemview填充,还是应该回收当前的itemview,完成子view 的测量布局操作。使用 while 循环判断是否有足够的空间来绘制一个完整的子view

其中layoutChunk()负责从缓存池recycler中取itemview,并调用addView()将取到的itemview添加到RecyclerView中去,并调用itemview自身的layout方法区布局item的位置

同时在这里,还调用MeasureChildWithMargins()来测绘子控件带下以及设置显示位置,这一步我们在下面的draw过程中讲。

而这全部的添加逻辑都放在一个while循环里面,不停的添加itemview到RecyclerView里面,直到塞满所有可见区域为止.

onDraw

	@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);
    	}
	}

在onDraw中除了绘制自己以外,还多调用了一个mItemDecorations的onDraw方法,这个mItemDecorations就是前面提到的分割线的集合

之前在讲RecyclerView的五虎上将的时候就讲过这个ItemDecoration,当时我们还重写了一个方法叫getItemOffsets() 目的是为了不让itemview挡住分割线。

还记得layout时说的那个MeasureChildWithMargins(),就在这里

	public void measureChildWithMargins(View child, int widthUsed, int 	heightUsed) {
    	final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    	widthUsed += insets.left + insets.right;
    	heightUsed += insets.top + insets.bottom;
    	if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
        	child.measure(widthSpec, heightSpec);
    	}
	}

在itemview Measure的时候,会把偏移量也计算出来,也就是说:其实ItemDecoration的宽高是计算在itemview中的,只不过itemview本身绘制区域没有那么大,留出来的地方正好的透明的,于是就透过itemview显示出来,ItemDecoration,那么就很有意思了,如果我故意在ItemDecoration的偏移量中写成0,那么itemview就会挡住ItemDecoration,而在itemview的增加或删除的时候,会短暂的消失,这时候又可以透过itemview看到ItemDecoration的样子,使用这种组合还可以做出意想不到的动画效果。

小结 RV 将 测量和布局的工作放心的委托给了 LayoutManager 来执行。不同的布局风格使用不同的 LayoutManager ,这是一种策略模式。

滚动

前面我们已经完整的走完了RecyclerView的绘制流程,接下来我们再看看它在滚动的时候代码又是怎么调用的,自然要看onTouch()方法的MOVE状态

	case MotionEvent.ACTION_MOVE: {
    	final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
    	final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
    	final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
    	int dx = mLastTouchX - x;
    	int dy = mLastTouchY - y;

    	if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
 			...
    	}

    	if (mScrollState != SCROLL_STATE_DRAGGING) {
			...
        	if (startScroll) {
            	setScrollState(SCROLL_STATE_DRAGGING);
        	}
    	}

    	if (mScrollState == SCROLL_STATE_DRAGGING) {
        	mLastTouchX = x - mScrollOffset[0];
        	mLastTouchY = y - mScrollOffset[1];

        	if (scrollByInternal(
                	canScrollHorizontally ? dx : 0,
                	canScrollVertically ? dy : 0,
                	vtev)) {
            	getParent().requestDisallowInterceptTouchEvent(true);
        	}
    	}
	} break;

看到这个代码的时候,我们就会有疑问MotionEventCompa这个类是干什么的,它是V4包里面提供的一个工具类,用于兼容低版本的触摸屏手势,平时用的时候更多的是用它来处理多点触控的情况,当成MotionEvent就可以了。

dispatchNestedPreScroll(),用于处理嵌套逻辑,例如在ScrollView里面放一个RecyclerView,如果是以前用ListView,还得把高度写死,禁止ListView的复用和滚动逻辑,而RecyclerView则完全不需要更多处理,直接用就是了,而且有一个非常好的地方,如果放到ScrollView里面,ListView的Itemview是不会复用的,如果RecyclerView因为是全局公用一套缓存池,虽说嵌套到ScrollView效率会低很多,但是比起ListView嵌套要好很多,之后将缓存池的时候我们继续讲,

再之后,如果在相应方向上手指move的距离达到最大值,则认为需要滚动,并且设置为滚动状态。

接着走出if块,如果是滚动状态,则调用滚动方法scrollByInternal()执行相应方向的滚动,滚动的距离当然就是手指移动的距离,跟进去,它就是调用了LinearLayoutManager.scrollBy()方法

回收与复用

前面讲layout、滚动的时候,都出现了一个东西,叫Recycler。

	public final class Recycler {
		final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
		private ArrayList<ViewHolder> mChangedScrap = null;

		final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

		private final List<ViewHolder>
        mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

		private RecycledViewPool mRecyclerPool;

		private ViewCacheExtension mViewCacheExtension;

Recycler 的缓存复用机制就是通过Recycler 中的这些数据容器来实现的。它根据访问优先级从上到下可以分为
4级。

  1. mAttachedScrap&mChangedScrap
  2. mCachedViews
  3. ViewCacheExtension
  4. RecycledViewPoll

第一级缓存 mAttachedScrap&mChangedScrap

主要用来缓存屏幕内的ViewHolder。为什么要缓存屏幕内的VH那?

当我们通过下拉刷新来更新列表内容时,不会创建新的VH,只是在原来的VH 基础上进行重新绑定新的数据即可,而这些旧的VH就被保存在这两个集合中。即当我们调用 notifyXXX 方法时,就会向这两个列表进行填充,将旧VH缓存起来。

第二级缓存:mCachedViews

就是上面的一系列mCachedViews,如果仍依赖于RecyclerView(比如已经滑出可视范围,但还没有被移除掉),但已经被标记移除的ItemView集合会被添加到mAttachScrap中,然后如果mAttachedScrap中不在依赖时会被加入到mCachedViews中,mChangedScrap则是存储notifyxxxx方法时需要改变ViewHolder

用来缓存移除至屏幕外的VH,默认情况下缓存个数是2个,可以通过 setViewCachedSize 来改变缓存的容量大小。如果mCachedViews已满,则会根据FIFO 移除旧VH添加新VH。

试想一下,刚被移除屏幕的VH 有可能接下来马上就会再次使用,所以RV不会立即将其设置为无效的VH,而是将他们保存到 cache 中。出于内存消耗的问题,cache 不能将所有移除屏幕外的VH 都缓存起来,所以它的默认容量是2.

第三级缓存 ViewCacheExtension

ViewCacheExtension是一个抽象静态类,用于充当附加的缓存池,当RecyclerView从第一级缓存找不到需要的View时,将会从ViewCacheExtension中找,不过这个缓存是由开发者维护的,如果没有设置它,则不会启用,通常我们也不会去设置它,系统已经预先提供了两级缓存,除非有特殊需求,比如在调用系统的缓存池之前,返回一个特定的视图,才会用到它。

第四级缓存 RecycledViewPool

同样是用来缓存屏幕外的ViewHolder. 当 mCachedViews 中的个数已满,则从mCachedViews 中淘汰的 VH会缓存到RecycledViewPool中。RecycledViewPool 会将 VH 的内部数据全部清理,因此从RecycledViewPool中取出来的 VH 需要重新调用 onBindViewHolder 绑定数据

之前讲了,与ListView直接缓存Itemview不同,从上面代码中我们也能看到,RecyclerView缓存的是ViewHolder,而ViewHolder里面包含了一个View这也就是为什么写Adapter的时候,必须继承一个固定的ViewHolder的原因,我们来看一下RecyclerViewPool

	public static class RecycledViewPool {
 		// 根据 viewType 保存的被废弃的 ViewHolder 集合,以便下次使用
 		private SparseArray<ArrayList<ViewHolder>> mScrap = new SparseArray<ArrayList<ViewHolder>>();
 		/**
   		* 从缓存池移除并返回一个 ViewHolder
   		*/
  		public ViewHolder getRecycledView(int viewType) {
    		final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
    		if (scrapHeap != null && !scrapHeap.isEmpty()) {
      			final int index = scrapHeap.size() - 1;
      			final ViewHolder scrap = scrapHeap.get(index);
      			scrapHeap.remove(index);
      			return scrap;
    		}
      		return null;
    	}
 
 		public void putRecycledView(ViewHolder scrap) {
    		final int viewType = scrap.getItemViewType();
    		final ArrayList scrapHeap = getScrapHeapForType(viewType);
    		if (mMaxScrap.get(viewType) <= scrapHeap.size()) {
      			return;
    		}
    		scrap.resetInternal();
    		scrapHeap.add(scrap);
  		}
 
  		/**
   		* 根据 viewType 获取对应缓存池
   		*/
  		private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {
    		ArrayList<ViewHolder> scrap = mScrap.get(viewType);
      		if (scrap == null) {
        		scrap = new ArrayList<>();
        		mScrap.put(viewType, scrap);
          		if (mMaxScrap.indexOfKey(viewType) < 0) {
            		mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);
          		}	
      		}
    		return scrap;
  		}
	}

从名字看来,他是一个缓存池,实现上,是通过一个默认大小为5的ArrayList实现的,这一点,同ListView的RecyclerBin这个类一样,很奇怪为什么不用LinkedList来做,按理说这种不需要索引读取的缓存池,用链表最合适的.

LinkedList 使用了双向链表,在内存占用上时ArrayList的两倍。虽然,该缓存池的删除添加操作比较频繁,而LinkedList在这方便比较有优势,但是ArrayList的容量比较小,性能上消耗也不是太大。总的来说,这是一种比较折中的设计方案。

然后每一个ArrayList又都是放在一个Map里面,SparseArray这个类我们在讲性能优化的时候已经多次提到了,就是用两个数组,用来代替HashMap

把所有的ArrayList放在一个Map里面,这也是RecyclerView最大的亮点,这样根据itemType来取不同的缓存Holder,每一个Holder都有对应的缓存,而只需要为这些不同RecyclerView设置同一个Pool就可以了。

为什么RecyclerView比ListView好

在适配器中通过onBindViewHolder和onCreateViewHolder两个方法屏蔽了一些固定的逻辑,使得适配器使用更简单,又通过了LayoutManager实现具体的布局,使得RecyclerView具有更强大的定制能力,所以RecyclerView比ListView好。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值