项目中遇到类似,省/市/县/乡/村 这种多层级数据展示的效果.原先处理过两层或者多层数据嵌套展示,没有处理过这种多层数据嵌套展示的效果所以在此记录一下,增强点记忆.
需求:省/市/县/乡/村 多层展示单选最小单位选中
思路:
- 数据整理,数据处理(list 无线嵌套list)
- 用RecyclerView 子条目也是RecyclerView 承载数据
难点:
- 底层数据获取上层数据的属性(必要属性建议拼接携带)
- 不同层级点击事件处理(建议只处理需要点击的和展开缩放的)
- RecyclerView 数据刷新闪烁问题(局部刷新,不刷新整条或者整个RecycerView)
功能实现
1. 数据类 NodeInfo.class
package com.wu.material.activity.rv.model
/**
* @author wkq
*
* @date 2022年03月15日 9:35
*
*@des
*
*/
data class NodeInfo(
var isExpen: Boolean,
val description: String,
val domainid: Int,
val full_name: String,
val iCode: String,
val id: Int,
val label_icon: String,
val name: String,
val orderindex: Int,
val parentName: String,
val parentid: Int,
val regtype: Int,
val sCode: String,
val sn: String,
val usecode: Boolean,
//嵌套的list(重要)
val nodes: List<NodeInfo>
)
2. Adapter 数据
2.1 DemoTreeAdapter.class
package com.wu.material.activity.rv.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.wu.material.R
import com.wu.material.activity.rv.model.NodeInfo
import com.wu.material.activity.rv.model.TreeInfo
/**
* @author wkq
*
* @date 2022年03月14日 13:28
*
*@des
*
*/
class DemoTreeAdapter(mContext: Context) : RecyclerView.Adapter<KtViewHolder>() {
var mContext: Context? = null
var listData = ArrayList<NodeInfo>()
init {
this.mContext = mContext
}
var listener: ItemClickListener? = null
fun setOnItemClickListener(listener: ItemClickListener) {
this.listener = listener
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): KtViewHolder {
var view =
LayoutInflater.from(mContext).inflate(R.layout.item_rv_tree_content, parent, false)
var viewHolder = KtViewHolder(view)
return viewHolder
}
fun getItem(position: Int): NodeInfo {
return listData.get(position)
}
// 局部刷新
override fun onBindViewHolder(holder: KtViewHolder, position: Int, payloads: MutableList<Any>) {
super.onBindViewHolder(holder, position, payloads)
var info = listData.get(position)
var iconImageView = holder.getAddressTypeImageView()
var recycleView = holder.getRecyclerView()
if (payloads != null && payloads!!.size > 0 && "1".equals(payloads.get(0))) {
if (info.nodes.size == 0) {
iconImageView.visibility = View.GONE
} else {
iconImageView.visibility = View.VISIBLE
if (info.isExpen) {
recycleView.visibility = View.VISIBLE
} else {
recycleView.visibility = View.GONE
}
}
if (info.isExpen) {
iconImageView.setBackgroundResource(R.mipmap.iv_address_delete)
} else {
iconImageView.setBackgroundResource(R.mipmap.iv_address_add)
}
} else {
super.onBindViewHolder(holder, position, payloads);
}
}
override fun onBindViewHolder(holder: KtViewHolder, position: Int) {
var mHolder = holder
var info = listData.get(position)
var titleTextView = mHolder.getTextView()
titleTextView.text = listData.get(position).name
var recycleView = mHolder.getRecyclerView()
var iconImageView = mHolder.getAddressTypeImageView()
recycleView.layoutManager = LinearLayoutManager(mContext)
var contentAdapter = DemoTreeAdapter(mContext!!)
recycleView.adapter = contentAdapter
if (info.nodes.size == 0) {
iconImageView.visibility = View.GONE
} else {
iconImageView.visibility = View.VISIBLE
if (info.isExpen) {
recycleView.visibility = View.VISIBLE
} else {
recycleView.visibility = View.GONE
}
contentAdapter.addItems(info.nodes)
}
if (info.isExpen) {
iconImageView.setBackgroundResource(R.mipmap.iv_address_delete)
} else {
iconImageView.setBackgroundResource(R.mipmap.iv_address_add)
}
contentAdapter.setOnItemClickListener(object : DemoTreeAdapter.ItemClickListener {
override fun onItemClick(position: Int) {
var info = contentAdapter.getItem(position)
if (info.nodes.size == 0) {
Toast.makeText(mContext, info.full_name, Toast.LENGTH_SHORT).show()
} else {
info.isExpen = !info.isExpen
contentAdapter.notifyItemChanged(position, "1")
}
}
})
titleTextView!!.setOnClickListener {
if (listener != null) {
listener!!.onItemClick(position)
}
}
}
override fun getItemCount(): Int {
return listData.size
}
override fun getItemViewType(position: Int): Int {
return super.getItemViewType(position)
}
fun addItems(items: List<NodeInfo>) {
listData.addAll(items)
//刷新数据
notifyDataSetChanged()
}
interface ItemClickListener {
fun onItemClick(position: Int)
}
}
2.2 adapter子条目的布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp">
<LinearLayout
android:id="@+id/ll_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:paddingLeft="10dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/iv_icon_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="@mipmap/iv_address_add" />
<ImageView
android:id="@+id/iv_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"
android:background="@mipmap/iv_address_icon" />
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:lines="1"
android:text="这是测试Item"
android:textColor="#333333" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ll_top" />
</androidx.constraintlayout.widget.ConstraintLayout>
2.3 holder类
package com.wu.material.activity.rv.adapter
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.wu.material.R
/**
* @author wkq
*
* @date 2022年03月14日 13:29
*
*@des
*
*/
class KtViewHolder(view: View) : RecyclerView.ViewHolder(view) {
var rootView: View? = null
init {
rootView = view
}
fun getTextView(): TextView {
var tvContent = rootView!!.findViewById<TextView>(R.id.tv_content)
return tvContent
}
fun getImageView(): ImageView {
return rootView!!.findViewById<ImageView>(R.id.iv_icon)
}
fun getAddressTypeImageView(): ImageView {
return rootView!!.findViewById<ImageView>(R.id.iv_icon_type)
}
fun getRecyclerView(): RecyclerView {
return rootView!!.findViewById<RecyclerView>(R.id.rv_content)
}
}
3. 主要实现
package com.wu.material.activity.rv
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.gson.Gson
import com.wu.material.R
import com.wu.material.activity.rv.adapter.DemoTreeAdapter
import com.wu.material.activity.rv.model.NodeInfo
import com.wu.material.activity.rv.model.TreeContentInfo
import com.wu.material.activity.rv.model.TreeInfo
import com.wu.material.databinding.ActivityRecyclerviewBinding
/**
* @author wkq
*
* @date 2022年02月17日 10:43
*
*@des
*
*/
class RecyclerTreeViewActivity : AppCompatActivity() {
var datas="{\n" +
" \"id\": 46,\n" +
" \"name\": \"沧县\",\n" +
" \"description\": \"0\",\n" +
" \"domainid\": 0,\n" +
" \"sCode\": \"1\",\n" +
" \"iCode\": null,\n" +
" \"orderindex\": 0,\n" +
" \"parentid\": 40,\n" +
" \"parentName\": null,\n" +
" \"regtype\": 0,\n" +
" \"sn\": null,\n" +
" \"usecode\": false,\n" +
" \"full_name\": \"沧县\",\n" +
" \"label_icon\": \"rule_icon_region\",\n" +
" \"nodes\": [\n" +
" {\n" +
" \"id\": 49,\n" +
" \"name\": \"保定县\",\n" +
" \"description\": null,\n" +
" \"domainid\": 0,\n" +
" \"sCode\": \"1\",\n" +
" \"iCode\": null,\n" +
" \"orderindex\": 0,\n" +
" \"parentid\": 42,\n" +
" \"parentName\": null,\n" +
" \"regtype\": 0,\n" +
" \"sn\": null,\n" +
" \"usecode\": false,\n" +
" \"full_name\": \"沧县\\\\保定县\",\n" +
" \"label_icon\": \"rule_icon_region\",\n" +
" \"nodes\": [\n" +
" {\n" +
" \"id\": 50,\n" +
" \"name\": \"保定乡\",\n" +
" \"description\": null,\n" +
" \"domainid\": 0,\n" +
" \"sCode\": \"1\",\n" +
" \"iCode\": null,\n" +
" \"orderindex\": 0,\n" +
" \"parentid\": 49,\n" +
" \"parentName\": null,\n" +
" \"regtype\": 0,\n" +
" \"sn\": null,\n" +
" \"usecode\": false,\n" +
" \"full_name\": \"沧县\\\\保定县\\\\保定乡\",\n" +
" \"label_icon\": \"rule_icon_region\",\n" +
" \"nodes\": [\n" +
" {\n" +
" \"id\": 52,\n" +
" \"name\": \"保定村村村村村村\",\n" +
" \"description\": null,\n" +
" \"domainid\": 0,\n" +
" \"sCode\": \"1\",\n" +
" \"iCode\": null,\n" +
" \"orderindex\": 0,\n" +
" \"parentid\": 50,\n" +
" \"parentName\": null,\n" +
" \"regtype\": 0,\n" +
" \"sn\": null,\n" +
" \"usecode\": false,\n" +
" \"full_name\": \"沧县\\\\保定县\\\\保定乡\\\\保定村村村村村村\",\n" +
" \"label_icon\": \"rule_icon_region\",\n" +
" \"nodes\": []\n" +
" }\n" +
" ]\n" +
" }\n" +
" ]\n" +
" },\n" +
" {\n" +
" \"id\": 53,\n" +
" \"name\": \"灵寿乡\",\n" +
" \"description\": null,\n" +
" \"domainid\": 0,\n" +
" \"sCode\": \"1\",\n" +
" \"iCode\": null,\n" +
" \"orderindex\": 0,\n" +
" \"parentid\": 44,\n" +
" \"parentName\": null,\n" +
" \"regtype\": 0,\n" +
" \"sn\": null,\n" +
" \"usecode\": false,\n" +
" \"full_name\": \"沧县\\\\灵寿乡\",\n" +
" \"label_icon\": \"rule_icon_region\",\n" +
" \"nodes\": []\n" +
" },\n" +
" {\n" +
" \"id\": 54,\n" +
" \"name\": \"柏乡3\",\n" +
" \"description\": null,\n" +
" \"domainid\": 0,\n" +
" \"sCode\": \"1\",\n" +
" \"iCode\": null,\n" +
" \"orderindex\": 0,\n" +
" \"parentid\": 46,\n" +
" \"parentName\": null,\n" +
" \"regtype\": 0,\n" +
" \"sn\": null,\n" +
" \"usecode\": false,\n" +
" \"full_name\": \"沧县\\\\柏乡3\",\n" +
" \"label_icon\": \"rule_icon_region\",\n" +
" \"nodes\": []\n" +
" },\n" +
" {\n" +
" \"id\": 48,\n" +
" \"name\": \"柏乡2\",\n" +
" \"description\": \"0\",\n" +
" \"domainid\": 0,\n" +
" \"sCode\": \"123\",\n" +
" \"iCode\": null,\n" +
" \"orderindex\": 0,\n" +
" \"parentid\": 46,\n" +
" \"parentName\": null,\n" +
" \"regtype\": 0,\n" +
" \"sn\": null,\n" +
" \"usecode\": false,\n" +
" \"full_name\": \"沧县\\\\柏乡2\",\n" +
" \"label_icon\": \"rule_icon_region\",\n" +
" \"nodes\": []\n" +
" },\n" +
" {\n" +
" \"id\": 47,\n" +
" \"name\": \"柏乡1\",\n" +
" \"description\": \"1234\",\n" +
" \"domainid\": 0,\n" +
" \"sCode\": \"1234\",\n" +
" \"iCode\": null,\n" +
" \"orderindex\": 0,\n" +
" \"parentid\": 46,\n" +
" \"parentName\": null,\n" +
" \"regtype\": 0,\n" +
" \"sn\": null,\n" +
" \"usecode\": false,\n" +
" \"full_name\": \"沧县\\\\柏乡1\",\n" +
" \"label_icon\": \"rule_icon_region\",\n" +
" \"nodes\": []\n" +
" }\n" +
" ]\n" +
" }"
var binding: ActivityRecyclerviewBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ActivityRecyclerviewBinding>(this, R.layout.activity_recyclerview)
initView()
}
private fun initView() {
var mAdapter = DemoTreeAdapter(this)
//设置LayoutManager
var linearLayoutManager = LinearLayoutManager(this)
binding!!.rvContent.layoutManager = linearLayoutManager
//设置 Adapter
binding!!.rvContent.adapter = mAdapter
var listData = ArrayList<NodeInfo>()
//刷新数据
var gson= Gson();
var info= gson.fromJson(datas,NodeInfo::class.java)
listData.add(info)
mAdapter.addItems(listData)
mAdapter.setOnItemClickListener(object : DemoTreeAdapter.ItemClickListener {
override fun onItemClick(position: Int) {
var info = mAdapter.getItem(position)
info.isExpen = !info.isExpen
mAdapter.notifyItemChanged(position, "1")
}
})
}
}
总结
这里简单的运用RecyclerView 实现了简单的树形结构以及处理了经常遇到的问题,一般需求能满足,要是复杂页面需要定制处理
看都看了点个赞再走吧