真香警告!RecyclerView的新组件来了~
共 10532字,需浏览 22分钟
·
2021-11-01 18:33
BATcoder技术群,让一部分人先进大厂
大家好,我是刘望舒,腾讯TVP,著有三本业内知名畅销书,连续四年蝉联电子工业出版社年度优秀作者,百度百科收录的资深技术专家。
前华为架构师,现大厂技术总监。
想要加入 BATcoder技术群,公号回复BAT
即可。
前言
ConcatAdapter
是RecyclerView是在1.2.0版本上推出的新组件。今天主要介绍ConcatAdapter
的基本使用和实现原理,包括:
- 使用特点
- 基础用法
- 实现原理
- 核心处理逻辑:ConcatAdapter层、ConcatAdapterController层、Helper层
使用特点
RecyclerView
在使用Adapter加载数据的时候,可能会区分多种ViewType,一般的处理方式都是通过重新Adapter
的getItemViewType
方法返回不同的ViewType,然后在onCreateViewHolder
方法里面通过不同的ViewType来定义不同的布局。
从一个具体的场景来说,RecyclerView通常会被分为三个部分,分别是:Header部分,Content部分,Footer部分,这其中三个部分的布局均不相同,所以就需要通过不同的ViewType来实现目的。
不过,一般项目里面会通过这种场景的逻辑均是通用的,所以我们最好是能将上面的逻辑定义成通用的从而方便使用。ConcatAdapter
可以将几个Adapter组合成为一个Adapter,每个子Adapter里面的ViewType是相同,子Adapter之间的ViewType可以是不同的,这样便能将不同的逻辑拆分,后续在复用起来就会更加的方便。
基础使用
按照RecyclerView常见的Header、Content和Footer三个部分的场景,实现一个小小的Demo,具体效果如下图:
- 先定义三个Adapter,用来加载不同部分的布局:HeaderAdapter、ContentAdapter 和FooterAdapter
- 使用ConcatAdapter将三个Adapter组合起来。组合步骤主要分为如下2步:
❝a. 定义ConcatAdapter的Config。主要是配置ViewType是否相互隔离,以及stableId的策略。b. 使用ConcatAdapter将子Adapter组合起来。
❞
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 1. 定义Config
val config = ConcatAdapter.Config.Builder()
.setIsolateViewTypes(true)
.setStableIdMode(ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS)
.build()
val adapter = ConcatAdapter(config)
// 2. 使用ConcatAdapter将三个Adapter组合起来。
adapter.addAdapter(HeaderAdapter(generateList("Header", 2)).apply { setHasStableIds(true)})
adapter.addAdapter(ContentAdapter(generateList("Content", 2)).apply { setHasStableIds(true)})
adapter.addAdapter(FooterAdapter(generateList("Footer", 2)).apply { setHasStableIds(true) })
val recyclerView = findViewById(R.id.recyclerView)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
}
private fun generateList(title: String, count: Int) = ArrayList().apply {
for (index in 0 until count) {
add("$title position = $index")
}
}
}
ConcatAdapter
的基本使用过程就是如上所说的内容,但是其中还有很多隐藏细节我并没有,比如说在定义Config的时候,setIsolateViewTypes
和setStableIdMode
这两个方法的作用是什么,以及ConcatAdapter究竟是怎么将子Adapter串联起来的。这些问题的答案,我都会在后面的内容详细介绍。
实现原理
1. 执行流程
2. 组成
ConcatAdappter
的实现主要分为3层:
- ConcatAdapter层:
ConcatAdappter
实现了RecyclerView#Adapter
的很多方法,它主要是面对于RecyclerView。 - ConcatAdapterController层:
ConcatAdapterController
可以认为是ConcatAdapter
的代理类,它接管了ConcatAdapter
的很多方法,包括核心的onCreateViewHolder
、onBindViewHolder
和getItemCount
等方法,所以各种处理逻辑并不是在ConcatAdapter
中,而是在ConcatAdapterController
里面进行的。 - Helper层:在这里,
ConcatAdapterController
里面用到的Helper类都应该属于Helper层,其中极具代表性的是l两个类:ViewTypeStorage
主要是用于处理ViewType相关的逻辑;StableIdStorage
主要是用于处理stableId
相关的逻辑。在这一层的类,主要的作用帮助ConcatAdapterController
处理相关逻辑。
下面,将分别介绍这三层核心处理逻辑。
第一层:ConcatAdapter
其内部的实现跟我们平时使用的Adapter没有多大的差别,不同的地方就在于ConcatAdapter
将实现逻辑放在了ConcatAdapterController
里面的。需要特别说明的是:
- 在通过ConcatAdapter的构造方法构造一个对象时,我们会发现构造方法上有一个很重要的参数--
Config
,尽管我们可以调用不带Config
的构造方法,但是实际上也会传递一个Config#DEFAULT
对象。这个Config
非常的重要,里面记录了组合Adapter在处理ViewType和StableId上采用的策略。关于这两个细节,这里先不展开,我们后续后内容专门的分析他俩。 ConcatAdapter
不能通过setHasStableIds
方法设置Adapter支持stableId。如果想要支持stableId,要分为两步:首先要在Config
将StableIdMode
设置为ISOLATED_STABLE_IDS
或者SHARED_STABLE_IDS
;其次添加进来的子Adapter必须支持stableId。ConcatAdapter
不能通过setStateRestorationPolicy
方法设置RecyclerView恢复状态的策略。可能有些同学不知道setStateRestorationPolicy
方法是什么,我在这里科普一下。我们都知道Activity在某些情况下会发生重建行为,重建之后需要恢复之前的状态,比如说RecyclerView要恢复到重建之前的位置,但是在这之前,是无脑的恢复,没有讨论RecyclerView数据为空的情况,所以在RecyclerView数据为空的时候,我们直接恢复之前的滑动位置是不会生效的,此时如果想要等到RecyclerView不为空时才生效,就需要setStateRestorationPolicy
方法。从1.2.0版本开始,RecyclerView
增加了一个setStateRestorationPolicy
方法来保证尽管数据为空状态也可以正常恢复。其中StateRestorationPolicy
一共有三种模式,如下:
第二层:ConcatAdapterController
在ConcatAdapterController
内部,每个子Adapter都会被封装成为一个NestedAdapterWrapper
类,所以ConcatAdapterController
的所有回调方法都是直接或者间接通过调用NestedAdapterWrapper
对应的方法实现逻辑。而每个方法里面所需要的NestedAdapterWrapper
对象都是通过两种方式来获取的:
方式1
从ConcatAdapterController
的缓存数组mWrappers
获取:当我们调用addAdapter方法,会将每个Adapter包装成为一个NestedAdapterWrapper
对象,同时会将这个对象添加到mWrappers
数组里面去,可以通过position其他地方直接获取。
方式2
从ViewTypeStorage
里面获取:ViewTypeStorage
会通过不同的ViewType缓存不同的NestedAdapterWrapper
,可以通过ViewType来获取。
区分一下ConcatAdapterController
所有需要NestedAdapterWrapper
对象的方法,只有onCreateViewHolder
方法是通过方式2获取的,其他方法都是通过方式1获取的。正因为如此差别,就会出现一个特别的现象就是,一个子Adapter的onBindViewHolder方法里面带ViewHolder,不一定是自己的onCreateViewHolder方法创建,因为ConcatAdapterController
在onCreateViewHolder
方法里面和onBindViewHolder
方法里面使用的NestedAdapterWrapper
对象不一定是同一个。「这一点大家一定要特别注意」。具体是什么情况下才会出现对象不一样的问题,这个在分析ViewType的时候会重点介绍。
ConcatAdapterController
除了将ConcatAdapter
的回调分发到每个子Adapter里面,还有一个作用就是将每个子Adapter数据变换的通知同步到ConcatAdapter
里面去,因为从类图上来看,ConcatAdapterController
实现了NestedAdapterWrapper.Callback
接口,每个子Adapter都会通过该接口来通知数据变化的信息。
第三层:Helper层
此处主要关注的是其核心处理逻辑:ViewType的处理策略、StableId的处理策略。
1. ViewType的处理策略
将多个Adapter组合到一个Adapter里面,我们需要考虑一个问题,就是如果子Adapter有可能返回相同的ViewType,面对这种情况,ConcatAdapter
应该让哪个子Adapter来创建ViewHolder呢?这是一个非常重要的。我们先来看一下Config针对于ViewType已有处理策略,即isolateViewTypes
不同取值的含义。
取值 | 含义 |
---|---|
true | 表示子Adapter相互隔离ViewType,互不影响。比如说有两个Adapter返回相同的ViewType, 那么还是自己处理自己的,在onBindViewHolder方法里面使用的ViewHolder,肯定是自己的 onCreateViewHolder方法创建出来的。 |
false | 表示所有的子Adapter共享ViewType,以及共享ViewHolder。比如说Adapter A和Adapter B 返回了相同的ViewType,在Adapter A onBindViewHolder 方法里面的ViewHolder有可能是Adapter B的onCreateViewHolder出来的。 |
我们在使用的时候,可以直接通过设置这个字段的值,以达到不同的目的。但是有没有思考过,ConcatAdapter是怎么处理的呢?接下来我们将正式这两种策略的实现原理,不过在这之前我们来了解一下实现ViewType处理策略整体结构。
1.1 ViewTypeStorage
ConcatAdapterContrller在处理ViewType时,会根据我们isolateViewTypes
不同取值创建不同ViewTypeStorage
对象,我们先来看一下这个接口的结构: 我分别解释一下这两个方法,含义如下表:
方法名 | 含义 |
---|---|
getWrapperForGlobalType | 该方法的作用是通过传入进入的ViewType获取一个NestedAdapterWrapper 对象。ConcatAdapterContrller 的onCreateViewHolder方法就是通过该方法获取获取的 NestedAdapterWrapper |
createViewTypeWrapper | 该方法的作用是通过传入的进来NestedAdapterWrapper 的对象,创建一个 ViewTypeLookup 对象。这其中,ViewTypeLookup 会将传入进来的NestedAdapterWrapper 对象缓存起来,方便 getWrapperForGlobalType 方法通过ViewType获取。 |
大家可能会对ViewTypeLookup
有疑惑,在这里,我简单的解释一下,先来看一下ViewTypeLookup
的类图: ViewTypeLookup
的作用就是将localType和globalType相互转换。那么怎么理解这两个type呢?我们可以这样认为:localType
是每个子Adapter返回产生的,globalType
是ConcatAdapter产生的。当ConcatAdapter需要将ViewType传递给子Adapter,就先要将它的globalType
转换成为子Adapter能识别的localType
;同时,ConcatAdapter产生的ViewType并不是它自己产生的,而是调用每个子Adapter的getItemViewType方法获取,然后然后通过localToGlobal方法转换成为globalType
。这一点,我们可以从ConcatAdapterController
里面找到答案:
public int getItemViewType(int globalPosition) {
// 1. 通过position获取一个WrapperAndLocalPosition对象,这里面封装的是
// NestedAdapterWrapper和localPosition。
WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition);
// 2. 调用NestedAdapterWrapper的getItemViewType方法返回一个ItemViewType。
// 这里返回的ViewType就是globalType,NestedAdapterWrapper的内部进行了一次localToGlobal转换。
int itemViewType = wrapperAndPos.mWrapper.getItemViewType(wrapperAndPos.mLocalPosition);
releaseWrapperAndLocalPosition(wrapperAndPos);
return itemViewType;
}
如上方法分为两步,我简单的总结一下:
❝❞
- 先是通过
globalPosition
获取一个WrapperAndLocalPosition
,这里面封装的是 NestedAdapterWrapper和localPosition。很明显,这里用到的NestedAdapterWrapper
是通过上面介绍的方式1获取的。而findWrapperAndLocalPosition
得非常的重要,在ConcatAdapterController
内部的很多地方都在调用,它的作用就是通过globalPosition
找到对应的NestedAdapterWrapper
,这里就不展开细讲了,有兴趣的同学可以了解一下。- 调用
NestedAdapterWrapper
的getItemViewType
。在NestedAdapterWrapper
的getItemViewType
的内部,其实分为两步:首先是调用子Adapter的getItemViewType
方法获取localType
;然后就是调用ViewTypeLookup
的localToGlobal
方法将localType
转换成为globalType
。
从整体来说,ViewTypeStorage
是服务于ConcatAdapter,因此不管子Adapter有多少个,只会有一个ViewTypeStorage
对象;而ViewTypeLookup
是服务于子Adapter,因此有多少个子Adapter,就会创建多少个ViewTypeLookup
对象。而ViewTypeLookup
的创建是在NestedAdapterWrapper
的构造方法里面进行的:
NestedAdapterWrapper(
Adapter adapter,
final Callback callback,
ViewTypeStorage viewTypeStorage,
StableIdStorage.StableIdLookup stableIdLookup) {
// ······
mViewTypeLookup = viewTypeStorage.createViewTypeWrapper(this);
// ······
}
1.2 隔离ViewType
接下来,我将重点分析ViewType的两种策略。首先,我们来看隔离策略。从ConcatAdapterController
的构造方法里面,我们可以知道,隔离策略用到的ViewTypeStorage
的实现类是IsolatedViewTypeStorage
。我们来看一下IsolatedViewTypeStorage
的实现:
class IsolatedViewTypeStorage implements ViewTypeStorage {
SparseArray mGlobalTypeToWrapper = new SparseArray<>();
int mNextViewType = 0;
int obtainViewType(NestedAdapterWrapper wrapper) {
int nextId = mNextViewType++;
mGlobalTypeToWrapper.put(nextId, wrapper);
return nextId;
}
@NonNull
@Override
public NestedAdapterWrapper getWrapperForGlobalType(int globalViewType) {
NestedAdapterWrapper wrapper = mGlobalTypeToWrapper.get(
globalViewType);
if (wrapper == null) {
throw new IllegalArgumentException("Cannot find the wrapper for global"
+ " view type " + globalViewType);
}
return wrapper;
}
@Override
@NonNull
public ViewTypeLookup createViewTypeWrapper(
@NonNull NestedAdapterWrapper wrapper) {
return new WrapperViewTypeLookup(wrapper);
}
void removeWrapper(@NonNull NestedAdapterWrapper wrapper) {
for (int i = mGlobalTypeToWrapper.size() - 1; i >= 0; i--) {
NestedAdapterWrapper existingWrapper = mGlobalTypeToWrapper.valueAt(i);
if (existingWrapper == wrapper) {
mGlobalTypeToWrapper.removeAt(i);
}
}
}
class WrapperViewTypeLookup implements ViewTypeLookup {
private SparseIntArray mLocalToGlobalMapping = new SparseIntArray(1);
private SparseIntArray mGlobalToLocalMapping = new SparseIntArray(1);
final NestedAdapterWrapper mWrapper;
WrapperViewTypeLookup(NestedAdapterWrapper wrapper) {
mWrapper = wrapper;
}
@Override
public int localToGlobal(int localType) {
int index = mLocalToGlobalMapping.indexOfKey(localType);
if (index > -1) {
return mLocalToGlobalMapping.valueAt(index);
}
// get a new key.
int globalType = obtainViewType(mWrapper);
mLocalToGlobalMapping.put(localType, globalType);
mGlobalToLocalMapping.put(globalType, localType);
return globalType;
}
@Override
public int globalToLocal(int globalType) {
int index = mGlobalToLocalMapping.indexOfKey(globalType);
if (index < 0) {
throw new IllegalStateException("requested global type " + globalType + " does"
+ " not belong to the adapter:" + mWrapper.adapter);
}
return mGlobalToLocalMapping.valueAt(index);
}
@Override
public void dispose() {
removeWrapper(mWrapper);
}
}
}
针对于IsolatedViewTypeStorage
, 我们重点分析getWrapperForGlobalType
方法和createViewTypeWrapper
方法。
getWrapperForGlobalType方法:我们可以从上面的实现可以看出来,
NestedAdapterWrapper
对象是从一个数组里面获取,其中key是globalViewType
。那么NestedAdapterWrapper
对象是怎么放进去的呢?我们简单寻找一下调用关系就知道:是在IsolatedViewTypeStorage
的obtainViewType
方法放进去的,整个调用关系如下图:总而言之,就是在getItemViewType
放入进去的。这里,我们需要特别的注意,如果ViewType采用隔离策略,那么子Adapter千万不能返回相同的ViewType。因为我们从实现来看,NestedAdapterWrapper
是依靠ViewType作为存储的,那么如果有两个Adapter返回相同的ViewType,会导致获取NestedAdapterWrapper
不是正确的,也就是前面说的,onCreateViewHolder调用的Adapter和onBindViewHolder的Adapter可能不是同一个对象。这个问题在隔离策略应该严格避免,否则容易出现莫名其妙的错误。createViewTypeWrapper方法:此方法的作用是用来创建
ViewTypeLookup
,从上面的代码中我们可以得知,与IsolatedViewTypeStorage
对应的ViewTypeLookup
实现类是WrapperViewTypeLookup
。从前面的介绍,我们可以知道,createViewTypeWrapper
方法是在NestedAdapterWrapper
的构造方法里面被调用的,在创建的同时还把NestedAdapterWrapper
对象传进来的,这里就为了后来localToGlobal
方法里面存储NestedAdapterWrapper
对象埋下了伏笔。前文已经介绍过,ViewTypeLookup
是面向子Adapter的,所以ViewTypeLookup
记录的NestedAdapterWrapper
对象就是跟它对应的NestedAdapterWrapper
对象。
1.3 共享ViewType
说完了隔离策略的实现,我们再来看看共享策略。从结构来说,共享策略使用的是SharedIdRangeViewTypeStorage
,同时与它对应的ViewTypeLookup
实现类是WrapperViewTypeLookup
;从实现上来说,共享策略在getWrapperForGlobalType
方法也是通过ViewType获取NestedAdapterWrapper
对象,也是在localToGlobal
方面里面将记录的NestedAdapterWrapper
对象存储在一个数组里面,这些跟隔离策略都是一致的。唯一不一致的是,NestedAdapterWrapper
数组采用的是SparseArray
数据数据结构,也就是说,同一个ViewType可能有多个>
NestedAdapterWrapper
对应,这也是共享策略的特色,子Adapter可以返回相同的ViewType。那么相同的ViewType,SharedIdRangeViewTypeStorage
是怎么确定该返回哪一个NestedAdapterWrapper
的呢?我们来简单的看一下getWrapperForGlobalType
方法的实现:
public NestedAdapterWrapper getWrapperForGlobalType(int globalViewType) {
List nestedAdapterWrappers = mGlobalTypeToWrapper.get(
globalViewType);
if (nestedAdapterWrappers == null || nestedAdapterWrappers.isEmpty()) {
throw new IllegalArgumentException("Cannot find the wrapper for global view"
+ " type " + globalViewType);
}
// just return the first one since they are shared
return nestedAdapterWrappers.get(0);
}
看上面的实现,我们可以知道,getWrapperForGlobalType
方法直接返回的是数组第一个元素。所以,共享策略不能保证,onBindViewHolder使用的ViewHolder是自己Adapter的onCreateViewHolder方法创建来的,这一点大家一定要注意。在这里,我有一个疑问,既然始终返回的是数组第一个元素,有必要用一个数组来存储吗?我不清楚Google爸爸是怎么考虑的。
2. 核心处理逻辑:StableId的处理策略
Config里面还有一个配置就是StableId的模式,从官方的文档来看,我们可以知道stableId一共有三个模式,分别如下:
模式 | 含义 |
---|---|
NO_STABLE_IDS | 这个模式比较简单,就是指Adapter不支持stableId。 |
ISOLATED_STABLE_IDS | 表示子Adapter之间采用隔离策略,在这个模式下,子Adapter不同考虑其他 Adapter的存在,因为在这个模式里面,ConcatAdapter 会覆盖子Adapter自 己生成的stableId,由它统一给每个item分配stableId,这样我们定义子Adapter 的时候,就不用其他的Adapter。注意的是,此时子Adapter的getItemId 方法和ViewHolder的getItemId方法的返回值是不一样的,我们如果需要stableId 的话,ViewHolder的getItemId方法是最可靠的。 |
SHARED_STABLE_IDS | 表示子Adapter之间采用共享策略,在这个模式,由子Adapter自己生成stableId, ConcatAdapter不会覆盖子Adapter的stableId。因为stableId的唯一性原则,所 以每个子Adapter在生成stableId时需要考虑其他子Adapter的存在,必须保证生 成的stableId的唯一性。 |
stableId的设计跟ViewType的设计非常的类似,都是一个Storage
类和多个Lookup
类。在stableId 结构中,StableIdStorage
是服务于ConcatAdapter,因为只会创建一个对象;StableIdLookup
服务于子Adapter,因此每个子Adapter都会创建StableIdLookup
对象。 我们来简单的看一下这两个接口的定义,uml类图如下: 两个接口的结构从类图可以看出,我针对于他们的方法特别解释一下:
createStableIdLookup方法:顾名思义,就是创建一个
StableIdLookup
对象。在ConcatAdapterController的构造方法中,首先会根据Config里面配置创建不同的StableIdStorage实现类对象;其次在创建NestedAdapterWrapper
的时候,会直接调用createStableIdLookup
方法创建一个StableIdLookup
对象,与新添加进来的子Adapter绑定,子Adapter需要的StableIdLookup
对象就是在创建的。localToGlobal方法:将子Adapter转换成为ConcatAdapter需要的globalId。因为这个方法实现不同,所以就区分出来了三种策略模式。
我们大致了解了每个模式的含义,我们分别来看一下每个模式的实现。
2.1 隔离策略
在隔离策略中,StableIdStorage
的实现类是IsolatedStableIdStorage
,StableIdLookup
的实现类是IsolatedStableIdStorage
。 在隔离策略中,IsolatedStableIdStorage
会把将每个子Adapter抹平,因此每个子Adapter生成的stableId都会经过localToGlobal
方法转换一次,因此我们直接来看localToGlobal
方法:
@Override
public long localToGlobal(long localId) {
Long globalId = mLocalToGlobalLookup.get(localId);
if (globalId == null) {
globalId = obtainId();
mLocalToGlobalLookup.put(localId, globalId);
}
return globalId;
}
这个方法主要经过两步:
❝❞
- 判断缓存中是否已经有stableId,如果有,直接返回;如果没有则进行第二步。
- 调用
obtainId
获取一个新的stableId。
从这里就可以应证前面所说的,隔离策略会覆盖子Adapter生成的stableId。在隔离策略中,不同的Adapter返回相同的stableId也是没有关系的,因为不同的Adapter拥有不同的StableIdLookup
对象,进而mLocalToGlobalLookup
缓存也是不一样的,所以他们互不影响。
2.2 共享策略
在隔离策略中,StableIdStorage
的实现类是SharedPoolStableIdStorage
,StableIdLookup
的实现类是SameIdLookup
。我们直接来看一下localToGlobal
方法的实现:
@Override
public long localToGlobal(long localId) {
return localId;
}
共享策略的实现很简单,就是将localId作为globalId。从这里,我们就可以知道为啥使用共享策略时,必须保证子Adapter不能生成不同的stableId。
总结
至此,关于RecyclerView新组件:ConcatAdapter讲解完毕。总结如下:
- ConcatAdapter的架构主要分为三层,分别是ConcatAdapter、ConcatAdapterController和Helper。
- Helper层主要是包括:ViewTypeStorage--用来处理ViewType的;StableIdStorage--用来处理stableId;NestedAdapterWrapper--里面封装了子Adapter、ViewTypeStorage和StableIdStorage等相关类。
- ViewType和stableId的处理都采用经典的策略者模式,主要思想是通过接口将ViewType和stabId的处理方式抽象出来,然后不同策略下,使用不同的实现类,这样的实现能保证逻辑清晰,可扩展性高。同时,其中涉及到的
ViewTypeStorage
和StableIdStorage
是服务于ConcatAdapter
,ViewTypeLookup
和StableIdLookup
服务于子Adapter。
推荐阅读
• 耗时2年,Android进阶三部曲第三部《Android进阶指北》出版!
• 『BATcoder』做了多年安卓还没编译过源码?一个视频带你玩转!
• 重生!进阶三部曲第一部《Android进阶之光》第2版 出版!
为了防止失联,欢迎关注我的小号
微信改了推送机制,真爱请星标本公号👇