RecyclerView详解一,使用及缓存机制
Posted 挽弦慕笙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RecyclerView详解一,使用及缓存机制相关的知识,希望对你有一定的参考价值。
本文大致会先讲解RecyclerView的基础知识及使用,最后会深入讲解一点原理。当然,本人知识水平有限哈,太深入的东西我现在还没接触到,还请大家包容,阿里嘎多~
一、RecyclerView的历史与发展
既然讲到了RV,那不得不先知道它怎么来的。
RecyclerView是Android 5.0提出的新的UI控件,与其一起诞生的还有著名的Material Design以及CardView等新特性。最初位于support.v7包中,这里既然提到了v7,那我就简单介绍一点v4,v7包以及androidx的历史发展。support-v4是Android 3.0推出的库,为了加入Fragment以及向下兼容老系统,即最低兼容到Android 1.6。support-v7向下兼容到Android 2.1,这两个库中包含有RecyclerView、ViewPager等常用控件。随着时间的推移,现在的Android系统已经发展到13了,显然这两个库就有些跟不上时代了,于是从Android 9.0开始,Google推出了androidx,以后推出的所有新特性都会加入到androidx中,并且androidx包下面的API都是随着扩展库发布的,这些API基本不会依赖于操作系统的具体版本,所有命名中它就不再包含版本号了。
所以,现在我们使用的RecyclerView都是包含在androidx包中。我们最开始学Android的时候肯定都接触过ListView,ListView的功能也很强大,在RecyclerView没出现之前,开发者们使用的都是ListView来展示大量的数据。但是ListView的性能比较差,之后我会对比一下二者的缓存策略,扩展性也不是很好,所以具有更加强大功能的RecyclerView诞生了。它包含有横向纵向排列的LinearLayoutManager、网格排列的GridLayoutManager和瀑布流排列的StaggeredGridLayoutManager。下面我先来带大家简单了解一下RecyclerView的使用。
二、RecyclerView的使用
这部分我不会讲很多,毕竟学会使用它也不是很难,具体的大家可以去参考一下《第一行代码》,本文的重点还是放在更深一点的缓存策略,回收复用,LayoutManager 以及 ItemTouchHelper等分析上。
(1)创建数据列表
这里演示的就不搞那么复杂了
class Data(val string: String)
(2)创建Adapter
class BasicAdapter(private val dataList: List<Data>) : RecyclerView.Adapter<BasicAdapter.ViewHolder>()
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view)
val dataString: TextView = view.findViewById(R.id.tv_str)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.title_item, parent, false)
return ViewHolder(view)
override fun onBindViewHolder(holder: ViewHolder, position: Int)
val data = dataList[position]
holder.dataString.text = data.string
override fun getItemCount(): Int = dataList.size
(3)Activity创建RecyclerView对象
// 这是单向布局
val layoutManager = LinearLayoutManager(this)
vb.recyclerView.layoutManager = layoutManager
val adapter = BasicAdapter(dataList)
vb.recyclerView.adapter = adapter
// 网格布局
// 将第一行代码替换为
val layoutManager = LinearLayoutManager(this, 2) // 表示分两列排布
// 瀑布流布局
val layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
演示结果就不给大家展示了,比较简单,一笔带过。接下来的内容就涉及到更加深入的部分了。
三、RecyclerView的缓存复用机制
RecyclerView的性能之所以强大,就是得益于它的缓存机制。我们通常认为RV具有四级缓存机制,而官方表示只有三级,这里我还是以四级缓存来讲述。下面是RV的四级缓存结构图。
层级 | 缓存变量 | 缓存名 | 用途 |
---|---|---|---|
1 | mChangeScrap与 mAttachedScrap | 可见缓存 | 用于布局过程中屏幕可见表项的回收和复用 |
2 | mCachedViews | 缓存列表 | 用于移出屏幕表项的回收和复用,不会清空数据 |
3 | mViewCacheExtension | 自定义缓存 | 自定义一个缓存,我们一般用不到 |
4 | RecycledViewPool | 缓存池 | 用于移出屏幕表项的回收和复用,会将ViewHolder的数据重置 |
在正式介绍四级缓存之前,我们还需要了解一下RV的Item的几个状态。
方法 | FLAG | 含义 | 具体场景 |
---|---|---|---|
isInvalid() | FLAG_INVALID | ViewHolder的数据是无效的 | 1. 调用了setAdapter() 2. 调用了notifyDataSetChanged()等方法 |
isRemoved() | FLAG_REMOVED | ViewHolder的数据已经被移除 | 调用了notifyItemRemoved() |
isUpdated() | FLAG_UPDATE | ViewHolder的数据需要重新绑定 | 1. isInvalid的几种情况 2. 调用了onBindViewHolder() 3. 调用了notifyItemChanged() |
isBound() | FLAG_BOUND | 数据已经绑定了某个Item上,数据是有效状态 | 调用了onBindViewHolder() |
1. 一级缓存
(1)一级缓存原理
Scrap是RV中最轻量的缓存,包括mChangeScrap和mAttachedScrap,只是作为临时缓存的存在。主要用于缓存出现在屏幕内的item,当我们通过notifyItemRemoved(),notifyItemChanged()通知item发生变化的时候,通过mAttachedScrap缓存没有发生变化的ViewHolder,其他的则由mChangedScrap缓存,添加itemView的时候快速从里面取出,完成局部刷新。通过源码理解一下。
void scrapView(View view)
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder))
if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds())
throw new IllegalArgumentException("Called scrap view with an invalid view."
+ " Invalid views cannot be reused from scrap, they should rebound from"
+ " recycler pool." + exceptionLabel());
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
else
if (mChangedScrap == null)
mChangedScrap = new ArrayList<ViewHolder>();
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
可以看到,对于页面中显示的Item,当调用 LayoutManager 类的 onLayoutChildren() 方法对views进行布局,这时会将RecyclerView上的items全部暂存到一个 ArrayList 集合,这里的数据是没有做修改的,所以不用重新绑定 Adapter。而如果其他情况比如调用了 notifyItemChanged() 和 notifyItemRangeChanged() 来通知数据发生了更新,数据或位置发生改变,那么该ViewHolder会被缓存到mChangedScrap中,这里存储的是发生了变化的ViewHolder,所以要重新走Adapter的绑定方法。可能我文字表述的不是很清楚,下面我放一张图来助大家理解。
图中的itemB删除掉,然后itemC,itemD依次移动上来,这里itemA和itemB前后参数没有发生变化(虽然itemB被移除了,但移除的时候它还是有效的,会被打上REMOVED标签,表示它是要删除的),所以他们两个存储到mAttachedscrap(),而itemC和itemD的位置发生了改变,所以他俩要存到mChangedScrap()中去。总结来说,删除itemB时,ABCD都会进入Scrap缓存,删除后,会从Scrap中将ACD取出,A的位置和数据都没有发生变化,CD的位置发生了变化但数据还是原封不动。
(2)一级缓存复用
复用的源码在ViewHolder tryGetViewHolderForPositionByDeadline(*)方法中。我对源码的解释在代码的注释里。
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
// 这里是从mChangedScrap()取ViewHolder,isPreLayout()判断是否为预布局,是一个特殊情况。
// 那什么是预布局呢?稍后我讲LayoutManager的时候会具体说一下
if (mState.isPreLayout())
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
// OK,现在我们点进getChangedScrapViewForPosition()方法中看一下是怎么取得ViewHolder
// find by position
// 这是按照position来取
for (int i = 0; i < changedScrapSize; i++)
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position)
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); // 添加标签表示从Scrap取
return holder;
// find by id
// 这是通过定义的id来取
if (mAdapter.hasStableIds())
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount())
final long id = mAdapter.getItemId(offsetPosition);
for (int i = 0; i < changedScrapSize; i++)
// 从mChangedScrap中取holder
final ViewHolder holder = mChangedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getItemId() == id)
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
从这里开始就是缓存复用真正的第一步,上面的是预加载的特殊情况。
// 1) Find by position from scrap/hidden list/cache
// 这里就是从mAttachedScrap()中取ViewHolder了
if (holder == null)
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null)
// 这里还要检验ViewHolder的有效性
if (!validateViewHolderForOffsetPosition(holder))
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun)
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap())
removeDetachedView(holder.itemView, false);
holder.unScrap();
else if (holder.wasReturnedFromScrap())
holder.clearReturnedFromScrapFlag();
// 如果不满足有效性则直接回收该ViewHolder
recycleViewHolderInternal(holder);
holder = null;
else
fromScrapOrHiddenOrCache = true;
// 这是从mAttachedScrap()取VH的核心源码,和上面的差不多。
// 只是多了几个判断条件,该holder须是有效的并且未被移除。
final int scrapCount = mAttachedScrap.size();
// Try first for an exact, non-invalid match from scrap.
for (int i = 0; i < scrapCount; i++)
final ViewHolder holder = mAttachedScrap.get(i);
// 第二个条件为索引判断,表示只能复用到指定位置
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved()))
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
从源码中可以看到,ViewHolder的复用是有顺序的,首先会判断是否预布局,如果是就从一级缓存中的mChangedScrap()中获取。如果没获取到就去mAttachScrap()和二级缓存中找。而一级缓存之所以说轻量,首先是因为它只针对当前页面显示的这些item,其次是因为它用完就会清空缓存,不占空间,效率也快。所以通知数据更新我们推荐使用notifyItemChanged(),实现局部刷新,用的是一级缓存来实现复用。而如果我们调用notifyDataChanged()来通知更新,会使数据全部进行刷新,不会走Scrap,性能低下。
3. 二级缓存
(1)二级缓存原理
CacheView用于RecyclerView列表位置产生变动时,通常称为离屏缓存,对刚刚移出屏幕的view进行回收。它的默认容量是2(可以修改),同样我们用一张图来帮助理解一下。
(2)二级缓存复用
接着上面的一级缓存复用讲。
这里还是getScrapOrHiddenOrCachedHolderForPosition()方法里的,如果刚才从mAttachScrap里没取到ViewHolder,那么就会走二级缓存,从mCachedViews里找。
// Search in our first-level recycled view cache. 官方说这里是第一级,但在我们日常使用中还是称他为第二级缓存
// 这是根据position来取
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++)
final ViewHolder holder = mCachedViews.get(i);
// invalid view holders may be in cache if adapter has stable ids as they can be
// retrieved via getScrapOrCachedViewForId
// 这里要对索引进行判断,只有当位置对得上才能拿来复用,
// 这也就意味着从mCatchedViews中取出的ViewHolder只能复用到指定的位置。
if (!holder.isInvalid() && holder.getLayoutPosition() == position
&& !holder.isAttachedToTransitionOverlay())
// 如果不在容量范围内,就把ViewHolder丢出去,丢到缓存池中。
if (!dryRun)
mCachedViews.remove(i);
if (DEBUG)
Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
+ ") found match in cache: " + holder);
return holder;
接下来就是源码里的第三步,这里是根据id来取,上面的是根据position来取,相差不大。
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds())
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
if (holder != null)
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
// 点进getScrapOrCachedViewForId()
// Search the first-level cache
final int cacheSize = mCachedViews.size();
for (int i = cacheSize - 1; i >= 0; i--)
final ViewHolder holder = mCachedViews.get(i);
......
CachedView的缓存主要是应对来回滑动的情况,这时候CachedView才会真正的起作用,其缓存的ViewHolder不需要重新赋值,就可以直接拿来用了。而且我们还可以修改它的容量,通过下面这个方法来修改。
public void setItemViewCacheSize(int size)
mRecycler.setViewCacheSize(size);
总结一下,mAttachedScrapp和mCachedViews都是需要进行索引判断,也就是说从这两个缓存中取出的ViewHolder只能复用到指定的位置。mCachedViews只能缓存屏幕外它容量大小的ViewHolder,超出容量的部分会被移除,丢到缓存池中,一会我再来具体讲解缓存池。
5. 三级缓存
三级缓存ViewCacheExtension是我们自定义的缓存,一般来说官方给的一、二、四级缓存就够用了,我们不会用到它,所以我也就一笔带过了。
if (holder == null && mViewCacheExtension != null)
// We are NOT sending the offsetPosition because LayoutManager does not know it.
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
......
从源码中分析,如果我们自定义了一个缓存并且前面的一二级缓存没有找到ViewHolder,系统就会从我们自定义的这个缓存里去找ViewHolder。
6. 四级缓存
好的,本文的第一个重点来了!在这里我会详细地分析RV的缓存池机制。
先来看看这一级的复用机制。
(1)缓存池结构
if (holder == null) // fallback to pool
if (DEBUG)
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null)
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST)
invalidateDisplayListInt(holder);
可以看到,这一部分和之前的一二级缓存复用机制有很大区别,没有那么多的限制条件了,不用判断索引是不是指定位置。但是它需要根据itemType来区分不同类型的ViewHolder。但在了解缓存池的复用机制之前,我们得先知道RecycledViewPool的基本结构。
public static class RecycledViewPool
//同类ViewHolder缓存个数上限为5
private static final int DEFAULT_MAX_SCRAP = 5;
// Tracks both pooled holders, as well as create/bind timing metadata for the given type.
// 回收池中存放单个类型ViewHolder的容器
static class ScrapData
//同类ViewHolder存储在ArrayList中
ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
//回收池中存放所有类型ViewHolder的容器
SparseArray<ScrapData> mScrap = new SparseArray<>();
...
//ViewHolder入池按viewType分类入池,一个类型的ViewType存放在一个ScrapData中
public void putRecycledView(ViewHolder scrap)
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
//如果超限了,则放弃入池
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size())
return;
if (DEBUG && scrapHeap.contains(scrap))
throw new IllegalArgumentException("this scrap item already exists");
scrap.resetInternal();
//回收时,ViewHolder从列表尾部插入
scrapHeap.add(scrap);
//从回收池中获取ViewHolder对象
public ViewHolder getRecycledView(int viewType)
// 获取到viewType
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty())
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
//复用时,从列表尾部获取ViewHolder(优先复用刚入池的ViewHoler)
return scrapHeap.remove(scrapHeap.size() - 1);
return null;
我们可以根据上述分析得到如下结论:RecycledViewPool中的ViewHolder存储在SparseArray中,并且按viewType分类存储,同一类型的ViewHolder存放在一个ArrayList中。虽然没有了对索引的判断,但是从mRecyclerPool中取出的ViewHolder只能复用于相同viewType的表项。 说来惭愧,就目前以我的水平来看,我大部分都只用到一种viewType。
那么现在我们再来分析缓存池的复用过程。
(2)缓存池复用
holder = getRecycledViewPool().getRecycledView(type);
// 这里是getRecyclerViewPool(),主要作用就是new了一个RecyclerViewPool对象出来。
// 然后再根据type来从缓存池中获取对应类型的ViewHolder。
RecycledViewPool getRecycledViewPool()
if (mRecyclerPool == null)
mRecyclerPool = new RecycledViewPool();
return mRecyclerPool;
这就是缓存池复用的运作机制,相信大家都已经对这部分的内容有所了解了,那么什么时候数据会被放到缓存池中呢?
(3)表项放入缓存池的几种情况
item移出屏幕
如果大家有印象的话,上面交代了一部分,超出mCachedViews的部分会被丢到这里来。这种情况就是当你滑动屏幕,item移出到屏幕之外后,超出屏幕两个之外的item被缓存池回收,为什么是两个之外呢?因为这两个是缓存在mCachedViews中的,因为它的复用效率更快,优先级更高。超出两个之外的按照先入先出的原则,被mCachedViews移出缓存。从这里可以看出,当你在滑动屏幕的过程中mCachedViews是不断进行 “输入输出” 的。
一级缓存的ViewHolder无效
在讲一级二级缓存复用机制的时候,我说过从mAttachedScrap,mCachedViews中取ViewHolder时,还需要检验有效性,具体是怎么检验的呢,我们从源码入手。
boolean validateViewHolderForOffsetPosition(ViewHolder holder)
// if it is a removed holder, nothing to verify since we cannot ask adapter anymore
// if it is not removed, verify the type and id.
// item是否被移除
if (holder.isRemovedRecyclerView使用详解
在上一篇(RecyclerView使用详解(二))文章中介绍了RecyclerView的多Item布局实现,接下来要来讲讲RecyclerView的Cursor实现,相较于之前的实现,Cursor有更多的使用场景,也更加的常用,特别是配合LoaderManager和CursorLoader进行数据的缓存及加载显示,基于此我们来重点看看RecyclerView的CursorAdapter具体要怎么实现。
一、CursorAdapter实现(配合LoaderManager和CursorLoader)
如果之前你用过ListView实现过此功能(CursorAdapter),那么你一定对下面这两个方法并不陌生
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return null;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
}
其中newView方法是用来创建Item布局的,bindView 方法是用来绑定当前View数据的,就相当于之前的getView方法拆成了两个方法实现。
如果你用RecyclerView,你会发现CursorAdapter这个类没有了,既然没有了,那我们就自己仿照着ListView的CursorAdapter类来实现,具体的代码没什么大的出入,无非就是注册两个观察者去监听数据库数据的变化,但是有两个地方需要注意一下,一个就是hasStableIds() 这个方法RecyclerView.Adapter中不能复写父类的方法,需要在初始化的时候调用setHasStableIds(true); 来完成相同功能,第二个就是notifyDataSetInvalidated() 这个方法没有,统一修改成notifyDataSetChanged() 方法即可。
void init(Context context, Cursor c, int flags) {
boolean cursorPresent = c != null;
mCursor = c;
mDataValid = cursorPresent;
mContext = context;
mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
mChangeObserver = new ChangeObserver();
mDataSetObserver = new MyDataSetObserver();
} else {
mChangeObserver = null;
mDataSetObserver = null;
}
if (cursorPresent) {
if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
}
setHasStableIds(true);//这个地方要注意一下,需要将表关联ID设置为true
}
//************//
public Cursor swapCursor(Cursor newCursor) {
if (newCursor == mCursor) {
return null;
}
Cursor oldCursor = mCursor;
if (oldCursor != null) {
if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
}
mCursor = newCursor;
if (newCursor != null) {
if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
mDataValid = true;
// notify the observers about the new cursor
notifyDataSetChanged();
} else {
mRowIDColumn = -1;
mDataValid = false;
// notify the observers about the lack of a data set
//There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
notifyDataSetChanged();//注意此处
}
return oldCursor;
}
//************//
private class MyDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
mDataValid = true;
notifyDataSetChanged();
}
@Override
public void onInvalidated() {
mDataValid = false;
//There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
notifyDataSetChanged();//注意此处
}
}
怎么样,是不是很简单,没错,就是这么简单,这里是完整的BaseAbstractRecycleCursorAdapter代码,用法和ListView的CursorAdapter用法一致,具体的可以看看我的Recyclerview LoaderManager Provider
二、Item的动画实现
RecyclerView本身就已经实现了ITEM的动画,只需要调用以下几个函数来增删Item即可出现默认动画。
notifyItemChanged(int)
notifyItemInserted(int)
notifyItemRemoved(int)
notifyItemRangeChanged(int, int)
notifyItemRangeInserted(int, int)
notifyItemRangeRemoved(int, int)
怎么样,是不是很轻松,如果你不满足系统默认动画,那么你可以自定义实现RecyclerView.ItemAnimator的接口方法,实现代码可以参考DefaultItemAnimator.当然,如果你不想自己实现,那么也没关系,这里有人已经写了开源库,你可以去看看recyclerview-animators,这里给出默认动画实现方式代码AnimFragment
三、嵌套RecycleView
一般是不推荐使用嵌套RecycleView的,和ListView是类似的,遇到这种需要嵌套的View一般都是想别的办法来规避,比如动态AddView,或者通过RecycleView的MultipleItemAdapter来实现,通过设置不同的ItemType布局不同的View,但是有时候会闲麻烦,想直接就用嵌套的方式来做,那么和ListView实现方式不同的是,ListView的实现一般都是继承ListView然后复写onMeasure方法,如下所示:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
但是RecycleView的实现方式不再是继承RecycleView来做,而是通过修改LayoutManager的方式,即通过继承LinearLayoutManager GridLayoutManager StaggeredGridLayoutManager来修改子控件的测量,下面给出主要代码:
private int[] mMeasuredDimension = new int[2];
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
int widthSpec, int heightSpec) {
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
Log.i(TAG, "onMeasure called. \nwidthMode " + widthMode
+ " \nheightMode " + heightSpec
+ " \nwidthSize " + widthSize
+ " \nheightSize " + heightSize
+ " \ngetItemCount() " + getItemCount());
int width = 0;
int height = 0;
for (int i = 0; i < getItemCount(); i++) {
measureScrapChild(recycler, i,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
mMeasuredDimension);
if (getOrientation() == HORIZONTAL) {
width = width + mMeasuredDimension[0];
if (i == 0) {
height = mMeasuredDimension[1];
}
} else {
height = height + mMeasuredDimension[1];
if (i == 0) {
width = mMeasuredDimension[0];
}
}
}
switch (widthMode) {
case View.MeasureSpec.EXACTLY:
width = widthSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
}
switch (heightMode) {
case View.MeasureSpec.EXACTLY:
height = heightSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
}
setMeasuredDimension(width, height);
}
private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
int heightSpec, int[] measuredDimension) {
try {
View view = recycler.getViewForPosition(0);//fix 动态添加时报IndexOutOfBoundsException
if (view != null) {
RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
getPaddingLeft() + getPaddingRight(), p.width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
getPaddingTop() + getPaddingBottom(), p.height);
view.measure(childWidthSpec, childHeightSpec);
measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin;
recycler.recycleView(view);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
}
}
private int[] mMeasuredDimension = new int[2];
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
int width = 0;
int height = 0;
int count = getItemCount();
int span = getSpanCount();
for (int i = 0; i < count; i++) {
measureScrapChild(recycler, i,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
mMeasuredDimension);
if (getOrientation() == HORIZONTAL) {
if (i % span == 0) {
width = width + mMeasuredDimension[0];
}
if (i == 0) {
height = mMeasuredDimension[1];
}
} else {
if (i % span == 0) {
height = height + mMeasuredDimension[1];
}
if (i == 0) {
width = mMeasuredDimension[0];
}
}
}
switch (widthMode) {
case View.MeasureSpec.EXACTLY:
width = widthSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
}
switch (heightMode) {
case View.MeasureSpec.EXACTLY:
height = heightSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
}
setMeasuredDimension(width, height);
}
private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
int heightSpec, int[] measuredDimension) {
if (position < getItemCount()) {
try {
View view = recycler.getViewForPosition(0);//fix 动态添加时报IndexOutOfBoundsException
if (view != null) {
RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
getPaddingLeft() + getPaddingRight(), p.width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
getPaddingTop() + getPaddingBottom(), p.height);
view.measure(childWidthSpec, childHeightSpec);
measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin;
recycler.recycleView(view);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
四、效果图如下:
Item默认动画效果
嵌套ScrollView效果
以上是关于RecyclerView详解一,使用及缓存机制的主要内容,如果未能解决你的问题,请参考以下文章