简介

最近稍微学了一些RecyclerView的进阶使用,就做了一个简单的ToDoList来进行应用。

 github地址

https://github.com/generalio/AndroidStudy/tree/main/TaskListDemo

简要功能介绍

ToDoListDemo

实现

activity_main.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
29
30
31
32
33
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="7"/>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">

<Button
android:id="@+id/btn_main_addParent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加任务"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

</LinearLayout>

再布置好一级菜单和二级菜单的两个布局:

item_parent.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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_marginTop="20dp"
android:layout_marginEnd="20dp"
android:layout_marginStart="20dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
app:cardBackgroundColor="#00ffff"
app:cardPreventCornerOverlap="true">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/parent_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="一级列表title"
android:textSize="18dp"
android:paddingBottom="10dp"
android:paddingTop="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<ImageView
android:id="@+id/parent_isExpand"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_folder"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginEnd="8dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>


</com.google.android.material.card.MaterialCardView>

item_child.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
29
30
31
32
33
34
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_marginTop="20dp"
android:layout_marginEnd="20dp"
android:layout_marginStart="54dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
app:cardBackgroundColor="#00ffcc"
app:cardPreventCornerOverlap="true">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:id="@+id/child_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="二级列表title"
android:textSize="18dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>


</com.google.android.material.card.MaterialCardView>

然后设置两个数据类分别存放一级菜单信息和二级菜单信息:

TaskInfo.kt:

1
2
3
4
5
6
7
/**
* text: 一级菜单内容
* TYPE: 类型为一级菜单还是二级菜单
* childList: 子任务列表
* isExpand: 是否为展开状态
*/
data class TaskInfo(val text: String, val TYPE: Int, val childList: MutableList<ChildInfo>, var isExpand: Boolean)

ChildInfo.kt:

1
data class ChildInfo(val content: String, val TYPE: Int)

然后开始编写RecyclerViewAdapter。新建一个TaskRecyclerViewAdapter.kt,我们这里使用listAdapter的差分刷新来实现,方便后面的一系列更改数据操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TaskRecyclerViewAdapter() : ListAdapter<TaskInfo, RecyclerView.ViewHolder>(object : DiffUtil.ItemCallback<TaskInfo>() {
override fun areContentsTheSame(oldItem: TaskInfo, newItem: TaskInfo): Boolean {
return oldItem == newItem
}

override fun areItemsTheSame(oldItem: TaskInfo, newItem: TaskInfo): Boolean {
return oldItem == newItem
}

}) {



}

这里的areContentsTheSameareItemsTheSame两个方法正确写法应该是第一个比较他们的唯一id是否一样,第二个再比较各个内容是否一样,这里偷个懒。

然后我们的思路是根据传入不同的TYPE来加载不同的布局,即重写getItemViewType()方法:

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
class TaskRecyclerViewAdapter() : ListAdapter<TaskInfo, RecyclerView.ViewHolder>(object : DiffUtil.ItemCallback<TaskInfo>() {
override fun areContentsTheSame(oldItem: TaskInfo, newItem: TaskInfo): Boolean {
return oldItem == newItem
}

override fun areItemsTheSame(oldItem: TaskInfo, newItem: TaskInfo): Boolean {
return oldItem == newItem
}

}) {

private val TYPE_PARENT = 1
private val TYPE_CHILD = 2

//绑定控件
inner class parentViewHolder(view: View): RecyclerView.ViewHolder(view) {
val parentTitle: TextView = view.findViewById(R.id.parent_title)
val parentIsExpand: ImageView = view.findViewById(R.id.parent_isExpand)
}

inner class childViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val childTitle: TextView = view.findViewById(R.id.child_title)
}

//处理数据,更新标题以及展开状态
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val task = getItem(position)
//这里的when会自动向下转型获取到引用
when(holder) {
is parentViewHolder -> {
holder.parentTitle.text = task.text
if(task.isExpand) {
holder.parentIsExpand.setImageResource(R.drawable.ic_expand)
} else {
holder.parentIsExpand.setImageResource(R.drawable.ic_folder)
}
}

is childViewHolder -> {
holder.childTitle.text = task.text
}
}
}

//通过接收不同的TYPE来加载不同的布局
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
if(viewType == TYPE_PARENT) {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_parent, parent, false)
return parentViewHolder(view)
} else {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_child, parent, false)
return childViewHolder(view)
}
}

//重写这个方法来返回不同的TYPE类型
override fun getItemViewType(position: Int): Int {
return getItem(position).TYPE
}

}

然后我们自定义一个点击接口,并对点击事件进行反馈,回调给外部进行后续处理。

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
class TaskRecyclerViewAdapter(private val itemClickListener: OnItemClickListener) : ListAdapter<TaskInfo, RecyclerView.ViewHolder>(object : DiffUtil.ItemCallback<TaskInfo>() {
override fun areContentsTheSame(oldItem: TaskInfo, newItem: TaskInfo): Boolean {
return oldItem == newItem
}

override fun areItemsTheSame(oldItem: TaskInfo, newItem: TaskInfo): Boolean {
return oldItem == newItem
}

}) {

//点击事件接口
interface OnItemClickListener {
fun onExpandClick(position: Int)
fun onItemClick(position: Int)
}

private val TYPE_PARENT = 1
private val TYPE_CHILD = 2

//绑定控件
inner class parentViewHolder(view: View): RecyclerView.ViewHolder(view) {
val parentTitle: TextView = view.findViewById(R.id.parent_title)
val parentIsExpand: ImageView = view.findViewById(R.id.parent_isExpand)
//在ViewHolder里面使用init{}初始化点击事件,这样这个点击事件的匿名内部类只会生成一次
init {
parentIsExpand.setOnClickListener {
if(getItem(adapterPosition).isExpand) {
parentIsExpand.setImageResource(R.drawable.ic_folder)
} else {
parentIsExpand.setImageResource(R.drawable.ic_expand)
}
//展开按钮点击的回调
itemClickListener.onExpandClick(adapterPosition)
}
//整个view点击事件的回调
view.setOnClickListener {
itemClickListener.onItemClick(adapterPosition)
}
}
}

inner class childViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val childTitle: TextView = view.findViewById(R.id.child_title)
}

//处理数据,更新标题以及展开状态
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val task = getItem(position)
//这里的when会自动向下转型获取到引用
when(holder) {
is parentViewHolder -> {
holder.parentTitle.text = task.text
if(task.isExpand) {
holder.parentIsExpand.setImageResource(R.drawable.ic_expand)
} else {
holder.parentIsExpand.setImageResource(R.drawable.ic_folder)
}
}

is childViewHolder -> {
holder.childTitle.text = task.text
}
}
}

//通过接收不同的TYPE来加载不同的布局
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
if(viewType == TYPE_PARENT) {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_parent, parent, false)
return parentViewHolder(view)
} else {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_child, parent, false)
return childViewHolder(view)
}
}

//重写这个方法来返回不同的TYPE类型
override fun getItemViewType(position: Int): Int {
return getItem(position).TYPE
}

}

然后,回到我们的MainActivity.kt中:

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
class MainActivity : AppCompatActivity(), TaskRecyclerViewAdapter.OnItemClickListener {

lateinit var recyclerview: RecyclerView
lateinit var recyclerViewAdapter: TaskRecyclerViewAdapter
lateinit var mBtnAddParent: Button
private val taskInfoList: MutableList<TaskInfo> = mutableListOf()

@SuppressLint("InflateParams", "MissingInflatedId")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

//绑定控件
recyclerview = findViewById(R.id.rv)
mBtnAddParent = findViewById(R.id.btn_main_addParent)

//初始化任务列表
taskInfoList.add(TaskInfo("1.点下方按钮添加任务", 1, mutableListOf(), false))
taskInfoList.add(TaskInfo("2.单击任务可添加子任务", 1, mutableListOf(), false))
taskInfoList.add(TaskInfo("3.侧滑删除任务", 1, mutableListOf(), false))

//初始化RecyclerView
recyclerview.layoutManager = LinearLayoutManager(this)
recyclerViewAdapter = TaskRecyclerViewAdapter(this)
//recyclerview.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
recyclerview.adapter = recyclerViewAdapter
recyclerViewAdapter.submitList(taskInfoList.toList())

//添加一些RecyclerView的ItemTouchHelper来进行侧滑和长按移动操作
addHelper()

//添加任务
mBtnAddParent.setOnClickListener {
showAddParentDialog()
}
}

fun addHelper() {
val helper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
//能够上下拖拽
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
//能够左右滑动
val swipeFlags = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
return makeMovementFlags(dragFlags, swipeFlags)
}

override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
//滑动操作
val from = viewHolder.adapterPosition
val to = target.adapterPosition
if(!taskInfoList[from].isExpand && !taskInfoList[to].isExpand) {
Collections.swap(taskInfoList, from, to)
recyclerViewAdapter.submitList(taskInfoList.toList())
}
return true
}

override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
//侧滑删除操作
val position = viewHolder.adapterPosition
//这里用折叠来模拟删除子任务的操作
folder(position)
taskInfoList.removeAt(position)
recyclerViewAdapter.submitList(taskInfoList.toList())
}

})
helper.attachToRecyclerView(recyclerview)
}

//展示对话框来添加任务
fun showAddParentDialog() {
val dialogView = layoutInflater.inflate(R.layout.dialog_change, null)
val dialogText: EditText = dialogView.findViewById(R.id.et_change_content)
val dialogBuilder = AlertDialog.Builder(this)
.setTitle("添加任务")
.setView(dialogView)
.setPositiveButton("确认") {dialog, which -> addParentTask(dialogText.text.toString()) }
.setNegativeButton("取消") {dialog, which -> }
dialogBuilder.show()
}

//添加任务
fun addParentTask(title: String) {
taskInfoList.add(TaskInfo(title, 1, mutableListOf(), false))
recyclerViewAdapter.submitList(taskInfoList.toList())
}

//添加子任务
fun addChildTask(position: Int, content: String) {
if(taskInfoList[position].isExpand) {
taskInfoList[position].childList.add(ChildInfo(content, 2))
val newPosition = position + taskInfoList[position].childList.size
taskInfoList.add(newPosition, TaskInfo(content, 2, mutableListOf(), false))
recyclerViewAdapter.submitList(taskInfoList.toList())
} else {
taskInfoList[position].childList.add(ChildInfo(content, 2))
recyclerViewAdapter.submitList(taskInfoList.toList())
}
}

//展开按钮点击的回调处
override fun onExpandClick(position: Int) {
if(taskInfoList[position].isExpand) {
taskInfoList[position].isExpand = false
folder(position)
} else {
taskInfoList[position].isExpand = true
expand(position)
}
}

//单击整个view添加子任务
override fun onItemClick(position: Int) {
showAddChildDialog(position)
}

//添加子任务的窗口
fun showAddChildDialog(position: Int) {
val dialogView = layoutInflater.inflate(R.layout.dialog_change, null)
val dialogText: EditText = dialogView.findViewById(R.id.et_change_content)
val dialogBuilder = AlertDialog.Builder(this)
.setTitle("添加子任务")
.setView(dialogView)
.setPositiveButton("确认") {dialog, which -> addChildTask(position, dialogText.text.toString()) }
.setNegativeButton("取消") {dialog, which -> }
dialogBuilder.show()
}

//展开,即将子任务全部添加进RecyclerView里面并将TYPE设为二级菜单
fun expand(position: Int) {
var nowPosition = position
for(childInfo in taskInfoList[position].childList) {
taskInfoList.add(nowPosition + 1, TaskInfo(childInfo.content, childInfo.TYPE, mutableListOf(), false))
nowPosition++
}
recyclerViewAdapter.submitList(taskInfoList.toList())
}

//折叠,即将子任务从任务列表里面删除
fun folder(position: Int) {
for(childInfo in taskInfoList[position].childList) {
if(position + 1 < taskInfoList.size) {
taskInfoList.removeAt(position + 1)
}

}
recyclerViewAdapter.submitList(taskInfoList.toList())
}
}

这样,我们就可以完成一个基本的清单列表了。