简介
最近稍微学了一些RecyclerView
的进阶使用,就做了一个简单的ToDoList
来进行应用。
github地址:https://github.com/generalio/AndroidStudy/tree/main/TaskListDemo
简要功能介绍
- [x] 动态添加任务
- [x] 为每个任务添加任意的子任务
- [x] 任务的展开与收起
- [x] 侧滑删除任务(删除一级任务时子任务会跟着删除)
- [x] 长按拖拽(只有任务被收起时的一级任务能被拖拽)
- [ ] 长按拖拽任务能改变级数
- [ ] 侧滑到一定位置显示删除按钮
实现
activity_main.xml
里面导入布局:
xml
<?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
一级菜单布局:
xml
<?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
二级菜单布局:
xml
<?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
:
kotlin
/**
* text: 一级菜单内容
* TYPE: 类型为一级菜单还是二级菜单
* childList: 子任务列表
* isExpand: 是否为展开状态
*/
data class TaskInfo(val text: String, val TYPE: Int, val childList: MutableList<ChildInfo>, var isExpand: Boolean)
ChildInfo.kt
:
kotlin
data class ChildInfo(val content: String, val TYPE: Int)
然后开始编写RecyclerView
的Adapter
。新建一个TaskRecyclerViewAdapter.kt
,我们这里使用listAdapter
的差分刷新来实现,方便后面的一系列更改数据操作:
kotlin
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
}
}) {
}
这里的areContentsTheSame
和areItemsTheSame
两个方法正确写法应该是第一个比较他们的唯一id
是否一样,第二个再比较各个内容是否一样,这里偷个懒。
然后我们的思路是根据传入不同的TYPE
来加载不同的布局,即重写getItemViewType()
方法:
kotlin
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
}
}
然后我们自定义一个点击接口,并对点击事件进行反馈,回调给外部进行后续处理。
kotlin
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
中:
kotlin
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())
}
}
这样,我们就可以完成一个基本的清单列表了。