Android RecyclerView

简介

RecyclerView 简称 RV, 是作为 ListView 和 GridView 的加强版出现的,目的是在有限的屏幕之上展示大量的内容,因此 RecyclerView 的复用机制的实现是它的一个核心部分。RV 常规使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
RecyclerView rv = findViewById(R.id.rv);
// 必选项,设置 RV 的布局管理器,决定 RV 的显示风格。
// 常用的:
// 有线性布局管理器(LinearLayoutManager)、
// 网格布局管理器(GridLayoutManager)、
// 瀑布流布局管理器(StaggeredGridLayoutManager)。
rv.setLayoutManager();
// 必选项,设置 RV 的数据适配器。当数据发生改变时,以通知者的身份,通知 RV 数据改变进行列表刷新操作。
rv.setAdapter();
// 非必选项,设置 RV 中 Item 的动画。
rv.setItemAnimator();
// 非必选项,设置 RV 中 Item 的装饰器,经常用来设置 Item 的分割线。
rv.addItemDecoration();

接下来看下 RV 是如何一步步将每一个 ItemView 显示到屏幕上,然后再分析在显示和滑动过程中,是如何通过缓存复用来提升整体性能的。

##

绘制流程分析

RV 本质上也是一个自定义控件,所以也符合自定义控件的规则。因此也可以沿着分析其 onMeasure -> onLayout -> onDraw 这 3 个方法的路线来深入研究。

onMeasure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
// 通过 mLayout(传入的 LayoutManager)来测量 RV 的大小(测量自身的宽高)
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
// 判断 RV 的宽高是否设置为 match_parent 或者具体数值
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}

if (mState.mLayoutStep == State.STEP_START) {
// RV 的动画相关
dispatchLayoutStep1();
}

mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
// 委托给 LayoutManager 来进行测量
// 表示在 XML 布局文件中,RV 的宽高设置为 wrap_content,则会执行下面的 dispatchLayoutStep2(),其实就是测量 RecyclerView 的子 View 的大小,最终确定 RecyclerView 的实际宽高。
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
...
}
}
...
}

onLayout

1
2
3
4
5
6
7
8
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
// 调用了 dispatchLayout 方法
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
return;
}
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
// 重新布局子 View。如果在 onMeasure 阶段没有执行 dispatchLayoutStep2() 方法去测量子 View,则会在 onLayout 阶段重新执行。
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
mLayout.setExactMeasureSpecsFrom(this);
// 重新布局子 View。如果在 onMeasure 阶段没有执行 dispatchLayoutStep2() 方法去测量子 View,则会在 onLayout 阶段重新执行。
dispatchLayoutStep2();
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
// 触发动画效果
dispatchLayoutStep3();
}
1
2
3
4
5
6
7
8
9
10
11
private void dispatchLayoutStep2() {
...
// Step 2: Run layout
mState.mInPreLayout = false;
// 核心逻辑是调用了 mLayout 的 onLayoutChildren 方法。这个方法是 LayoutManager 中的一个空方法,主要作用是测量 RV 内的子 View 大小,并确定它们所在的位置。LinearLayoutManager、GridLayoutManager,以及 StaggeredLayoutManager 都分别复写了这个方法,并实现了不同方式的布局。
mLayout.onLayoutChildren(mRecycler, mState);

mState.mStructureChanged = false;
mPendingSavedState = null;
...
}

以 LinearLayoutManager 的实现为例:

  • 在 onLayoutChildren 中调用 fill 方法,完成子 View 的测量布局工作;
  • 在 fill 方法中通过 while 循环判断是否还有剩余足够空间来绘制一个完整的子 View;
  • layoutChunk 方法中是子 View 测量布局的真正实现,每次执行完之后需要重新计算 remainingSpace。

layoutChunk 是一个非常核心的方法,这个方法执行一次就填充一个 ItemView 到 RV,部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// 调用了 layoutState.next 方法
View view = layoutState.next(recycler);
...
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
// 从缓存(Recycler)中取出子 ItemView,然后调用 addView 或者 addDisappearingView 将子 ItemView 添加到 RV 中。
if (layoutState.mScrapList == null) {
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);
}
}
// 测量被添加的 RV 中的子 ItemView 的宽高。
measureChildWithMargins(view, 0, 0);
...
// 根据所设置的 Decoration、Margins 等所有选项确定子 ItemView 的显示位置。
layoutDecoratedWithMargins(view, left, top, right, bottom);
...
}

onDraw

1
2
3
4
5
6
7
8
9
@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);
}
}

这个方法很简单,如果有添加 ItemDecoration,则循环调用所有的 Decoration 的 onDraw 方法,将其显示。至于所有的子 ItemView 则是通过 Android 渲染机制递归的调用子 ItemView 的 draw 方法显示到屏幕上。

小结:RV 会将测量 onMeasure 和布局 onLayout 的工作委托给 LayoutManager 来执行,不同的 LayoutManager 会有不同风格的布局显示,这是一种策略模式。


缓存复用原理

缓存复用是 RV 中另一个非常重要的机制,这套机制主要实现了 ViewHolder 的缓存以及复用。

核心代码是在 Recycler 中完成的,它是 RV 中的一个内部类,主要用来缓存屏幕内 ViewHolder 以及部分屏幕外 ViewHolder,部分代码如下:

1
2
3
4
5
6
7
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
}

Recycler 的缓存机制就是通过上图中的这些数据容器来实现的,实际上 Recycler 的缓存也是分级处理的,根据访问优先级从上到下可以分为 4 级,如下:

  • 第一级缓存:mAttachedScrap、mChangedScrap。
  • 第二级缓存:mCachedViews。
  • 第三级缓存:ViewCacheExtension。
  • 第四级缓存:RecycledViewPool。

各级缓存功能
RV 之所以要将缓存分成这么多块,是为了在功能上进行一些区分,并分别对应不同的使用场景。

1、第一级缓存 mAttachedScrap&mChangedScrap

是两个名为 Scrap 的 ArrayList,这两者主要用来缓存屏幕内的 ViewHolder。

比如通过下拉刷新列表中的内容,当刷新被触发时,只需要在原有的 ViewHolder 基础上进行重新绑定新的数据 data 即可,而这些旧的 ViewHolder 就是被保存在 mAttachedScrap 和 mChangedScrap 中。实际上当我们调用 RV 的 notifyXXX 方法时,就会向这两个列表进行填充,将旧 ViewHolder 缓存起来。

2、第二级缓存 mCachedViews

它用来缓存移除屏幕之外的 ViewHolder,默认情况下缓存个数是 2,不过可以通过 setViewCacheSize 方法来改变缓存的容量大小。如果 mCachedViews 的容量已满,则会根据 FIFO 的规则将旧 ViewHolder 抛弃,然后添加新的 ViewHolder。

通常情况下刚被移出屏幕的 ViewHolder 有可能接下来马上就会使用到,所以 RV 不会立即将其设置为无效 ViewHolder,而是会将它们保存到 cache 中,但又不能将所有移除屏幕的 ViewHolder 都视为有效 ViewHolder,所以它的默认容量只有 2 个。

3、第三级缓存 ViewCacheExtension

这是 RV 预留给开发人员的一个抽象类,在这个类中只有一个抽象方法。开发人员可以通过继承 ViewCacheExtension,并复写抽象方法 getViewForPositionAndType 来实现自己的缓存机制。只是一般情况下我们不会自己实现也不建议自己去添加缓存逻辑,因为这个类的使用门槛较高,需要开发人员对 RV 的源码非常熟悉。

4、第四级缓存 RecycledViewPool

RecycledViewPool 同样是用来缓存屏幕外的 ViewHolder,当 mCachedViews 中的个数已满(默认为 2),则从 mCachedViews 中淘汰出来的 ViewHolder 会先缓存到 RecycledViewPool 中。ViewHolder 在被缓存到 RecycledViewPool 时,会将内部的数据清理,因此从 RecycledViewPool 中取出来的 ViewHolder 需要重新调用 onBindViewHolder 绑定数据。这就同最早的 ListView 中的使用 ViewHolder 复用 convertView 的道理是一致的,因此 RV 也算是将 ListView 的优点完美的继承过来。

RecycledViewPool 还有一个重要功能,就是多个 RV 之间可以共享一个 RecycledViewPool,这对于多 tab 界面的优化效果会很显著。需要注意的是,RecycledViewPool 是根据 type 来获取 ViewHolder,每个 type 默认最大缓存 5 个。因此多个 RecyclerView 共享 RecycledViewPool 时,必须确保共享的 RecyclerView 使用的 Adapter 是同一个,或 view type 是不会冲突的。

RV 是如何从缓存中获取 ViewHolder 的

在 layoutChunk 方法中通过调用 layoutState.next 方法拿到某个子 ItemView,然后添加到 RV 中。

1
2
3
4
5
6
7
8
9
10
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
// 获取某个位置需要展示的 View
final View view = recycler.getViewForPosition(mCurrentPosition);
// 将当前绘制的 child 的索引下移一位,配合 while 循环。
mCurrentPosition += mItemDirection;
return view;
}

getViewForPosition 方法最终调用 tryGetViewHolderForPositionByDeadline 方法来查找相应位置上的ViewHolder,在这个方法中会从 4 级缓存中依次查找:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
// 从 Scrap 和 Cache 中查找 ViewHolder
if (holder == null) {
// 从 attach 和 mCachedViews 中获取。
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
...
}
if (holder == null) {
...
// 获取这个位置的数据的类型。 子 Adapter 复写的方法。
final int type = mAdapter.getItemViewType(offsetPosition);
// 如果在 Adapter 中,给 ItemView 设置唯一 ID,则在此处根据 ID 再从 Scrap 和 Cache 中查找。
// stable id 是标识一个 viewholder 的唯一性,即使它做动画改变了位置。
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
...
}
// 从 ViewCacheExtension 中获取 ViewHolder。
if (holder == null && mViewCacheExtension != null) {
// 你返回的 View 要是 RecyclerView.LayoutParamas 属性的。
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
...
}
// 从 RecycledViewPool 中获取
if (holder == null) {
...
holder = getRecycledViewPool().getRecycledView(type);
...
}
if (holder == null) {
...
// 如果各级缓存都没有找到,则调用 Adapter 的 createViewHolder 方法创建一个新的 ViewHolder。
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
...
return holder;
}

何时将 ViewHolder 存入缓存

接下来看下 ViewHolder 被存入各级缓存的场景。

1、第一次 layout
当调用 setLayoutManager 和 setAdapter 之后,RV 会经历第一次 layout 并被显示到屏幕上。

此时并不会有任何 ViewHolder 的缓存,所有的 ViewHolder 都是通过 createViewHolder 创建的。

2、刷新列表
如果通过手势下拉刷新,获取到新的数据 data 之后,我们会调用 notifyXXX 方法通知 RV 数据发生改变,这回 RV 会先将屏幕内的所有 ViewHolder 保存在 Scrap 中。

当缓存执行完之后,后续通过 Recycler 就可以从缓存中获取相应 position 的 ViewHolder(姑且称为旧 ViewHolder),然后将刷新后的数据设置到这些 ViewHolder 上。

最后再将新的 ViewHolder 绘制到 RV 上。

总结
Android RecyclerView 源码中的 2 块核心实现:

RecyclerView 是如何经过测量、布局,最终绘制到屏幕上,其中大部分工作是通过委托给 LayoutManager 来实现的。
RecyclerView 的缓存复用机制,主要是通过内部类 Recycler 来实现。


示例

示例(Kotlin)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/**
* 更加强大且推荐的滚动控件
* 优化了 ListView 存在的不足之处。
* RecyclerView 属于新增控件,要添加依赖。
*/
class RecyclerViewActivity : BaseActivity(){

private val fruitList = ArrayList<FruitBean>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_rv)

initFruit()
// val layoutManager = LinearLayoutManager(this)
// 默认是纵向的,可通过设置来变为横向
// 为什么 LV 无法实现 RV 的效果:ListView 的布局排列是由自身去管理的,而 RecyclerView 是由 LayoutManager。
// LayoutManager 制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能定制出不同排列方式的布局。
// RV 还有 GridLayoutManager(网格布局),StaggeredGridLayoutManager(瀑布流)效果。
// layoutManager.orientation = LinearLayoutManager.HORIZONTAL
val layoutManager = StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL)
// 3 列,布局纵向排列
rv.layoutManager = layoutManager
val adapter = FruitRVAdapter(fruitList)
rv.adapter = adapter

}

private fun initFruit(){
// repeat 函数会将表达式内的内容执行 2 遍。
repeat(3){
fruitList.add(
FruitBean(
getRandomLengthString("Apple"),
R.drawable.ic_launcher_background
)
)
fruitList.add(
FruitBean(
getRandomLengthString("Apple"),
R.drawable.ic_launcher_background
)
)
fruitList.add(
FruitBean(
getRandomLengthString("Apple"),
R.drawable.ic_launcher_background
)
)
fruitList.add(
FruitBean(
getRandomLengthString("Apple"),
R.drawable.ic_launcher_background
)
)
fruitList.add(
FruitBean(
getRandomLengthString("Apple"),
R.drawable.ic_launcher_background
)
)
fruitList.add(
FruitBean(
getRandomLengthString("Apple"),
R.drawable.ic_launcher_background
)
)
fruitList.add(
FruitBean(
getRandomLengthString("Apple"),
R.drawable.ic_launcher_background
)
)
fruitList.add(
FruitBean(
getRandomLengthString("Apple"),
R.drawable.ic_launcher_background
)
)
}
}

// private fun getRandomLengthString(str:String):String{
// val n = (1..20).random()
// val builder = StringBuilder()
// repeat(n){
// builder.append(str)
// }
// return builder.toString()
// }

/**
* 使用扩展函数和运算符重载的功能,来实现上面函数的效果
* 字符串有了和数字相乘的能力
*
* (utils -> String.kt 内实现)
*/
private fun getRandomLengthString(str:String) = str * (1..20).random()

companion object{
fun actionStart(context: Context){
val intent = Intent(context, RecyclerViewActivity::class.java)
context.startActivity(intent)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class FruitRVAdapter(val fruitList: ArrayList<FruitBean>) :
RecyclerView.Adapter<FruitRVAdapter.ViewHolder>(){

inner class ViewHolder(view:View):RecyclerView.ViewHolder(view){
val ivItemFruit : ImageView = view.findViewById(R.id.ivItemFruit)
val tvItemFruit : TextView = view.findViewById(R.id.tvItemFruit)
}

/**
* 创建 ViewHolder 实例
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_fruit,parent,false)
val viewHolder = ViewHolder(view)

viewHolder.itemView.setOnClickListener{
// 获取用户点击的 position。因为此方法没有提供。
val position = viewHolder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context,"you click view ${fruit.name}",Toast.LENGTH_SHORT).show()
}

viewHolder.ivItemFruit.setOnClickListener{
val position = viewHolder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context,"you click img ${fruit.name}",Toast.LENGTH_SHORT).show()

}

return viewHolder
}

/**
* 对子项的数据赋值,会在每个子项被滚动到屏幕内的时候执行。
*/
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.ivItemFruit.setImageResource(fruit.imgId)
holder.tvItemFruit.text = fruit.name
}

/**
* 一共有多少子项
*/
override fun getItemCount() = fruitList.size

}
item_fruit.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="utf-8"?>

<!--瀑布流布局的宽度应是根据布局的列数来自动适配的,而不是固定值。-->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@android:color/holo_blue_light">

<ImageView
android:id="@+id/ivItemFruit"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp" />

<TextView
android:id="@+id/tvItemFruit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:layout_gravity="left"
android:layout_marginTop="10dp" />


</LinearLayout>
1
class FruitBean (val name:String, val imgId:Int)

示例(聊天界面 Kotlin)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class ChatActivity:BaseActivity(), View.OnClickListener {

private val msgList = ArrayList<ChatMsgBean>()

/**
* 可以使用 lateinit 关键字,进行延迟初始化,
* 这样就不用在一开始时将它赋值为 null 了,类型声明可以改成 MsgAdapter。
* 但使用要注意,要确保变量在使用前,已做了初始化。
*/
// private var adapter:MsgAdapter?=null
private lateinit var adapter:MsgAdapter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chat)
initMsg()
val layoutManager = LinearLayoutManager(this)
// RecyclerView
rvChat.layoutManager = layoutManager

// 使用 ::adapter.isInitialized 可用于判断 adapter 变量是否已经初始化,避免重复进行初始化操作。
if (!:: adapter.isInitialized){
adapter = MsgAdapter(msgList)
}
rvChat.adapter = adapter
btnChat.setOnClickListener(this)
}

override fun onClick(v: View?) {
when(v){
btnChat ->{
val content = etChat.text.toString()
if (content.isNotEmpty()){
val msg = ChatMsgBean(
content,
ChatMsgBean.TYPE_SENT
)
msgList.add(msg)
// 当有新增消息时,刷新 RV 中的显示
// 也可以使用 notifyDataSetChanged(),这样不管新增、删除还是修改元素都会刷新界面可见的全部元素,但这样效率相对会差一些。
// 有时,像即使明确知道一些全局变量不会为空,onClick() 会在 onCreate() 之后调用,在 onCreate()中对 adapter 做了初始化。
// 但出于 Kotlin 编译器的要求,还是需要额外做许多的判空处理代码。但做了延迟初始化之后,就不必了。
// adapter?.notifyItemInserted(msgList.size-1)
adapter.notifyItemInserted(msgList.size-1)
// 将 RV 定位到最后一行
rvChat.scrollToPosition(msgList.size-1)
// 清空输入框中的内容
etChat.setText("")
}
}
}
}

private fun initMsg() {
val msg1 = ChatMsgBean(
"Hello",
ChatMsgBean.TYPE_RECEIVED
)
msgList.add(msg1)
val msg2 = ChatMsgBean(
"Hello",
ChatMsgBean.TYPE_SENT
)
msgList.add(msg2)
val msg3 = ChatMsgBean(
"Hello",
ChatMsgBean.TYPE_RECEIVED
)
msgList.add(msg3)
}

companion object{
fun actionStart(context: Context){
val intent = Intent(context, ChatActivity::class.java)
context.startActivity(intent)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 修改成:密封类,结合 RecyclerView 适配器的 ViewHolder
*/
class MsgAdapter (val msgList:List<ChatMsgBean>): RecyclerView.Adapter<MsgViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MsgViewHolder {
return if (viewType == ChatMsgBean.TYPE_RECEIVED) {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.item_msg_left, parent, false)
MsgViewHolder.LeftViewHolder(view)
} else {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.item_msg_right, parent, false)
MsgViewHolder.RightViewHolder(view)
}
}

override fun getItemViewType(position: Int): Int {
val msg = msgList[position]
return msg.type
}

override fun getItemCount() = msgList.size

override fun onBindViewHolder(holder: MsgViewHolder, position: Int) {
val msg = msgList[position]
when(holder){
is MsgViewHolder.LeftViewHolder -> holder.leftMsg.text = msg.content
is MsgViewHolder.RightViewHolder -> holder.rightMsg.text = msg.content
// 密封类不需要 else 分支了
}
}

//class MsgAdapter (val msgList:List<ChatMsgBean>): RecyclerView.Adapter<RecyclerView.ViewHolder>() {

// inner class LeftViewHolder(view: View):RecyclerView.ViewHolder(view){
// val leftMsg :TextView = view.findViewById(R.id.tvMsgLeft)
// }
//
// inner class RightViewHolder(view: View):RecyclerView.ViewHolder(view){
// val rightMsg :TextView = view.findViewById(R.id.tvMsgRight)
// }

// override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = if (viewType == ChatMsgBean.TYPE_RECEIVED){
// val view = LayoutInflater.from(parent.context).inflate(R.layout.item_msg_left,parent,false)
// LeftViewHolder(view)
// }else{
// val view = LayoutInflater.from(parent.context).inflate(R.layout.item_msg_right,parent,false)
// RightViewHolder(view)
// }

// override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
// val msg = msgList[position]
// when(holder){
// is LeftViewHolder -> holder.leftMsg.text = msg.content
// is RightViewHolder -> holder.rightMsg.text = msg.content
// else -> throw IllegalArgumentException()
// }
// }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 密封类,结合 RecyclerView 适配器的 ViewHolder
*/
sealed class MsgViewHolder(view:View):RecyclerView.ViewHolder(view){

class LeftViewHolder(view: View): MsgViewHolder(view){
val leftMsg : TextView = view.findViewById(R.id.tvMsgLeft)
}

class RightViewHolder(view: View): MsgViewHolder(view){
val rightMsg : TextView = view.findViewById(R.id.tvMsgRight)
}

}
1
2
3
4
5
6
7
8
9
10
class ChatMsgBean(val content:String,val type:Int) {

companion object{
// const 关键字定义常量
// 只有 companion object 或顶层方法中才能使用 const 关键字
const val TYPE_RECEIVED = 0
const val TYPE_SENT = 1
}

}

备注

RecyclerView(谷歌)

拉钩教育-Android 工程师进阶 34 讲