Skip to content

简介

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

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

简要功能介绍

ToDoListDemo

  • [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)

然后开始编写RecyclerViewAdapter。新建一个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
    }

}) {

    

}

这里的areContentsTheSameareItemsTheSame两个方法正确写法应该是第一个比较他们的唯一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())
    }
}

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