业务逻辑:
点击选择,弹出弹窗,列表数据由后台提供,不限层级,可叠加无限层子级;
点击item展开收起,点击尾部icon单选选中,点击[确定]为最终选中,收起弹窗;
搜索框输入字符时,实时检索数据显示;
搜索出的列表点击直接选中,收起弹窗。
弹窗代码:
class DepartListDialog {class Builder(context: Context) : BasicDialog.Builder<Builder>(context) {private val inputView: EditText? by lazy { findViewById(R.id.et_input) }private val titleView: TextView? by lazy { findViewById(R.id.tv_title) }private val leftView: TextView? by lazy { findViewById(R.id.tv_left) }private val rightView: TextView? by lazy { findViewById(R.id.tv_right) }private val clearView: ImageView? by lazy { findViewById(R.id.iv_del) }private val llEmpty: LinearLayout? by lazy { findViewById(R.id.ll_empty) }private var onLeftClick: ((BasicDialog) -> Unit)? = nullprivate var onRightClick: ((String, BasicDialog) -> Unit)? = nullprivate var isFilterEmail = falseprivate var query = ""private var onItemClick: ((DepartModel, Int, BasicDialog?) -> Unit)? = nullprivate var onItemSelectClick: ((View, Int, DepartModel) -> Unit)? = nullprivate var onSearchItemClick: ((ArrayList<String>, DepartModel, Int, BasicDialog?) -> Unit)? = nullprivate var onInputListener: ((String, BasicDialog?) -> Unit)? = nullprivate val rvSearch: RecyclerView? by lazy { findViewById(R.id.rv_search) }private val recyclerView: RecyclerView? by lazy { findViewById(R.id.recycler_view) }private var recyclerAdapter: DepartTreeAdapter? = nullprivate var treeItemList = arrayListOf<DepartModel>()private val rvSearchAdapter: CommonAdapter<DepartModel>? by lazy {CommonAdapter<DepartModel>(R.layout.item_string).apply {convert = { holder, position, item ->item?.let {var str = item.namevar cList = item.children ?: arrayListOf()while (cList.isNotEmpty()) {str += "/" + cList[0].namecList = cList[0].children ?: arrayListOf()}val tvName = holder.getView<TextView>(R.id.tv_name)setPartText(tvName, str, query,getColor(R.color.color_909090), getColor(R.color.color_303030))}}setOnItemClickListener { adapter, _, position ->adapter.getItem(position)?.let {val ids = arrayListOf<String>()var bean = itids.add(bean.id)var str = bean.namewhile (bean.children != null && bean.children?.isNotEmpty() == true) {bean = bean.children!![0]ids.add(bean.id)str += "/" + bean.name}val nameArr = str.split("/")if (nameArr.size > 2) {bean.name ="……/" + nameArr[nameArr.size - 2] + "/" + nameArr[nameArr.size - 1]} else {bean.name = str}onSearchItemClick?.invoke(ids, bean, position, getDialog())dismiss()}}}}init {setContentView(R.layout.dialog_common_input_data)setAnimStyle(AnimAction.ANIM_BOTTOM)setGravity(Gravity.BOTTOM)setCancelable(false)setCanceledOnTouchOutside(false)setOnClickListener(leftView, rightView, clearView)setRightBtnEnable(false)recyclerAdapter = DepartTreeAdapter(context, treeItemList)recyclerView?.adapter = recyclerAdapterrecyclerAdapter?.setOnItemClickListener(object : DepartTreeAdapter.OnItemClickListener {override fun onItemClick(view: View, position: Int, model: DepartModel) {
// showToast("当前点击的数据为 ${model.name}")}})recyclerAdapter?.onItemChildClickListener =object : DepartTreeAdapter.OnItemChildClickListener {override fun onClick(view: View, position: Int, model: DepartModel) {onItemSelectClick?.invoke(view, position, model)setRightBtnEnable(true)}}rvSearch?.adapter = rvSearchAdapterinputView?.addTextChangedListener(object : TextWatcher {override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}override fun afterTextChanged(p0: Editable?) {query = inputView?.text?.trim().toString()if (query.isEmpty()) {rvSearchAdapter?.submitList(arrayListOf())rvSearchAdapter?.notifyDataSetChanged()recyclerView?.visibility = View.VISIBLEval v = if ((recyclerView?.adapter?.itemCount?: 0) < 1) View.VISIBLE else View.GONEllEmpty?.visibility = v} else {onInputListener?.invoke(query, getDialog())recyclerView?.visibility = View.INVISIBLE}clearView?.isVisible = query.isNotEmpty()}})}fun setRightBtnEnable(enable: Boolean): Builder = apply {val color = if (enable) {if (UiUtil.isDarkMode(getContext())) getColor(R.color.color_E16C5F) else getColor(R.color.color_DD594A)} else {if (UiUtil.isDarkMode(getContext())) getColor(R.color.color_606060) else getColor(R.color.color_909090)}rightView?.setTextColor(color)rightView?.isEnabled = enable}fun setData(data: ArrayList<DepartModel>): Builder = apply {recyclerAdapter?.submitList(data)recyclerAdapter?.notifyDataSetChanged()val v = if (data.isEmpty()) View.VISIBLE else View.GONEllEmpty?.visibility = v}fun resetDefault() {recyclerAdapter?.setDefaultAdapter()}fun setSelectItem(index: Int): Builder = apply {recyclerAdapter?.notifyItemChanged(index)}fun setSearchData(data: ArrayList<DepartModel>): Builder = apply {rvSearchAdapter?.submitList(data)rvSearchAdapter?.notifyDataSetChanged()val v =if (data.isEmpty() && recyclerView?.visibility == View.INVISIBLE) View.VISIBLE else View.GONEllEmpty?.visibility = v}fun setSelectSearchItem(index: Int): Builder = apply {rvSearchAdapter?.notifyItemChanged(index)}fun setOnItemClick(onItemClick: ((DepartModel, Int, BasicDialog?) -> Unit)): Builder =apply {this.onItemClick = onItemClick}fun setOnItemSelectClick(onItemSelectClick: ((View, Int, DepartModel) -> Unit)): Builder =apply {this.onItemSelectClick = onItemSelectClick}fun setOnSearchItemClick(onSearchItemClick: ((ArrayList<String>, DepartModel, Int, BasicDialog?) -> Unit)): Builder =apply {this.onSearchItemClick = onSearchItemClick}fun setOnInputListener(onInputListener: ((String, BasicDialog?) -> Unit)): Builder =apply {this.onInputListener = onInputListener}override fun onClick(view: View) {when (view) {leftView -> {getDialog()?.let {onLeftClick?.invoke(it)}dismiss()}rightView -> {val input = inputView?.text?.toString()?.trim() ?: ""getDialog()?.let {onRightClick?.invoke(input, it)}dismiss()}clearView -> {inputView?.text?.clear()}}}// 设置标题fun setTitle(title: String): Builder = apply {titleView?.text = title}// 设置左边的文本fun setLeftText(leftStr: String): Builder = apply {leftView?.text = leftStr}// 设置右边的文本fun setRightText(rightStr: String): Builder = apply {rightView?.text = rightStr}// 设置输入类型fun setInputType(inputType: Int): Builder = apply {inputView?.inputType = inputType}// 设置输入最大长度fun setInputLength(maxLength: Int): Builder = apply {inputView?.filters = arrayOf(InputFilter.LengthFilter(maxLength))}// 设置邮箱字符过滤fun needFilterEmail(): Builder = apply {isFilterEmail = true}// 设置默认输入fun setDefaultInput(str: String): Builder = apply {inputView?.setText(str)}// 设置输入提示语fun setInputHint(hint: String): Builder = apply {inputView?.hint = hint}// 设置左边按钮点击事件fun setLeftClick(onLeftClick: ((BasicDialog) -> Unit)): Builder = apply {this.onLeftClick = onLeftClick}// 设置右边按钮点击事件fun setRightClick(onRightClick: ((String, BasicDialog) -> Unit)): Builder = apply {this.onRightClick = onRightClick}}
}
弹窗布局文件:
<?xml version="1.0" encoding="utf-8"?>
<com.ruffian.library.widget.RRelativeLayout 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:layout_width="match_parent"android:layout_height="match_parent"android:layout_marginTop="@dimen/dp_56"android:layout_gravity="bottom"app:background_normal="?first_bg_color"app:corner_radius_top_left="@dimen/dp_24"app:corner_radius_top_right="@dimen/dp_24"tools:viewBindingIgnore="true"><TextViewandroid:id="@+id/tv_left"android:layout_width="wrap_content"android:layout_height="@dimen/dp_54"android:gravity="center"android:paddingHorizontal="@dimen/dp_17"android:text="@string/cancel"android:textColor="?third_text_color"android:textSize="@dimen/sp_14" /><TextViewandroid:id="@+id/tv_right"android:layout_width="wrap_content"android:layout_height="@dimen/dp_54"android:layout_alignParentEnd="true"android:gravity="center"android:paddingHorizontal="@dimen/dp_17"android:text="@string/confirm"android:enabled="false"android:textColor="?first_btn_bg_color"android:textSize="@dimen/sp_14" /><TextViewandroid:id="@+id/tv_title"android:layout_width="wrap_content"android:layout_height="@dimen/dp_54"android:layout_centerHorizontal="true"android:gravity="center"android:paddingHorizontal="@dimen/dp_17"android:text="@string/depart_select"android:textColor="?first_text_color"android:textSize="@dimen/sp_18" /><Viewandroid:id="@+id/v_divider"android:layout_below="@+id/tv_title"android:layout_marginHorizontal="@dimen/dp_16"android:layout_width="match_parent"android:layout_height="0.5dp"android:background="?first_divider_color" /><com.ruffian.library.widget.RRelativeLayoutandroid:id="@+id/rl_et"android:layout_width="match_parent"android:layout_height="@dimen/dp_32"app:corner_radius="@dimen/dp_4"android:layout_below="@+id/v_divider"android:layout_margin="@dimen/dp_16"app:background_normal="?second_btn_bg_color"><ImageViewandroid:id="@+id/iv_search"android:layout_width="@dimen/dp_16"android:layout_height="@dimen/dp_16"android:layout_centerVertical="true"android:contentDescription="@null"android:layout_marginStart="@dimen/dp_10"android:src="?icon_edit_search" /><EditTextandroid:id="@+id/et_input"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_marginStart="@dimen/dp_10"android:layout_toStartOf="@+id/iv_del"android:layout_toEndOf="@+id/iv_search"android:background="@color/transparent"android:importantForAutofill="no"android:inputType="text"android:textColor="?first_text_color"android:hint="@string/please_input_depart_name"android:textColorHint="?first_hint_text_color"android:textSize="@dimen/sp_14"tools:ignore="LabelFor" /><ImageViewandroid:id="@+id/iv_del"android:layout_width="@dimen/dp_32"android:layout_height="@dimen/dp_32"android:layout_alignParentEnd="true"android:contentDescription="@null"android:paddingHorizontal="@dimen/dp_8"android:paddingVertical="@dimen/dp_8"android:visibility="invisible"android:src="?icon_clear_edit" /></com.ruffian.library.widget.RRelativeLayout><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recycler_view"android:layout_below="@+id/rl_et"app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"android:layout_width="match_parent"android:layout_height="wrap_content"/><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/rv_search"android:layout_below="@+id/rl_et"app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"android:layout_width="match_parent"android:layout_height="wrap_content"android:background="?first_bg_color"/><LinearLayoutandroid:visibility="gone"android:id="@+id/ll_empty"android:layout_below="@+id/rl_et"android:layout_width="match_parent"android:layout_height="@dimen/dp_280"android:background="?first_bg_color"android:gravity="center"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent"><ImageViewandroid:layout_width="@dimen/dp_180"android:layout_height="@dimen/dp_160"android:src="?img_empty"/></LinearLayout></com.ruffian.library.widget.RRelativeLayout>
树形结构的适配器:
class DepartTreeAdapter(private val mContext: Context,private val treeItemList: ArrayList<DepartModel>
) :RecyclerView.Adapter<DepartTreeAdapter.KTTreeViewHolder>() {private var selectPosition = RecyclerView.NO_POSITIONprivate var onItemClickListener: OnItemClickListener? = nullvar onItemChildClickListener: OnItemChildClickListener? = nullprivate var settedAdapter = falsefun setSelectPosition(position: Int) {selectPosition = positionnotifyDataSetChanged()}fun submitList(data: ArrayList<DepartModel>) {treeItemList.clear()treeItemList.addAll(data)notifyDataSetChanged()}fun setDefaultAdapter() {notifyDataSetChanged()settedAdapter = true}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): KTTreeViewHolder {val binding =ItemDepartmentBinding.inflate(LayoutInflater.from(parent.context), parent, false)return KTTreeViewHolder(binding)}override fun onBindViewHolder(holder: KTTreeViewHolder, position: Int) {val treeItem = treeItemList[position]holder.bind(treeItem)}override fun getItemCount(): Int {return treeItemList.size}inner class KTTreeViewHolder(val binding: ItemDepartmentBinding) :RecyclerView.ViewHolder(binding.root) {fun bind(treeItem: DepartModel) {// 根据层级设置左边距val leftPadding = (treeItem.level - 1) * 50binding.llIcon.setPadding(leftPadding, 0, 0, 0)binding.tvName.text = treeItem.nameval icon =if (TestActivity.lastSelectBean?.id == treeItem.id) R.mipmap.ic_selected else R.mipmap.ic_unselectbinding.ivSelect.setImageResource(icon)//设置默认已选中数据的adapterif (settedAdapter && TestActivity.lastSelectBean?.id == treeItem.id) {settedAdapter = falseTestActivity.curAdapter = this@DepartTreeAdapter}// 根据展开状态设置箭头图标if (treeItem.children != null && treeItem.children?.isNotEmpty() == true) {if (treeItem.isOpen) {binding.ivNext.rotation = 180f} else {binding.ivNext.rotation = 0f}} else {binding.llIcon.visibility = View.INVISIBLE}// 设置点击事件itemView.setOnClickListener {// 切换展开状态treeItem.isOpen = !treeItem.isOpen// 局部刷新,只刷新当前点击的位置和其子项的数据notifyItemChanged(bindingAdapterPosition)onItemClickListener?.onItemClick(itemView, bindingAdapterPosition, treeItem)
// setSelectPosition(bindingAdapterPosition)}// 如果没有数据子列表不显示binding.recyclerView.visibility = if (treeItem.isOpen) View.VISIBLE else View.GONE// 如果有子数据,设置适配器并显示箭头if (treeItem.children != null && treeItem.children?.isNotEmpty() == true) {binding.ivNext.visibility = View.VISIBLEbinding.recyclerView.visibility = if (treeItem.isOpen) View.VISIBLE else View.GONE// 计算子项的级别,加上适当的偏移量//给了level 不用计算
// val childLevel = treeItem.level + 1
// treeItem.children!!.forEach { child ->
// child.level = childLevel
// }binding.recyclerView.layoutManager = LinearLayoutManager(mContext)val childAdapter = DepartTreeAdapter(mContext, treeItem.children!!)binding.recyclerView.adapter = childAdapterchildAdapter.setOnItemClickListener(object : OnItemClickListener {override fun onItemClick(view: View, position: Int, model: DepartModel) {onItemClickListener?.onItemClick(view, position, model)}})childAdapter.onItemChildClickListener = object : OnItemChildClickListener {override fun onClick(view: View, position: Int, model: DepartModel) {TestActivity.lastSelectBean = modelTestActivity.curAdapter?.notifyDataSetChanged()childAdapter.notifyDataSetChanged()TestActivity.curAdapter = childAdapterif (model.name.contains("……")) {onItemChildClickListener?.onClick(view, position, model)} else {var name = ""if (model.level > 2) {name = "……/"}name += "${treeItem.name}/${model.name}"val selModel = DepartModel(model.id, name)onItemChildClickListener?.onClick(view, position, selModel)}}}} else { // 没有子数据,隐藏箭头binding.ivNext.visibility = View.GONEbinding.recyclerView.visibility = View.GONE}binding.ivSelect.setOnClickListener {TestActivity.lastSelectBean = treeItemTestActivity.curAdapter?.notifyDataSetChanged()notifyDataSetChanged()TestActivity.curAdapter = this@DepartTreeAdapteronItemChildClickListener?.onClick(it, bindingAdapterPosition, treeItem)}// 根据是否带有右箭头来设置背景颜色
// itemView.setBackgroundColor(
// ContextCompat.getColor(
// mContext,
// if (binding.ivNext.visibility == View.VISIBLE
// && bindingAdapterPosition == selectPosition
// ) R.color.color_public_bg else R.color.white
// )
// )}}fun setOnItemClickListener(listener: OnItemClickListener) {this.onItemClickListener = listener}interface OnItemClickListener {fun onItemClick(view: View, position: Int, model: DepartModel)}interface OnItemChildClickListener {fun onClick(view: View, position: Int, model: DepartModel)}
}
adapter绑定的item布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="wrap_content"xmlns:app="http://schemas.android.com/apk/res-auto"android:orientation="vertical"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="center_vertical"android:paddingVertical="@dimen/dp_16"android:paddingHorizontal="@dimen/dp_16"><LinearLayoutandroid:id="@+id/ll_icon"android:layout_width="wrap_content"android:layout_height="wrap_content"><ImageViewandroid:id="@+id/iv_next"android:layout_width="@dimen/dp_12"android:layout_height="@dimen/dp_12"android:src="?ic_expand_normal" /></LinearLayout><TextViewandroid:id="@+id/tv_name"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="@dimen/dp_6"android:layout_weight="1"android:textColor="?first_text_color"android:textSize="@dimen/sp_14" /><ImageViewandroid:id="@+id/iv_select"android:layout_width="@dimen/dp_24"android:layout_height="@dimen/dp_24"android:src="?ic_unselect" /></LinearLayout><Viewandroid:id="@+id/v_divider"android:layout_width="match_parent"android:layout_height="0.5dp"android:background="?first_divider_color"android:layout_marginHorizontal="@dimen/dp_16" /><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recyclerView"app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"android:layout_width="match_parent"android:layout_height="wrap_content" />
</LinearLayout>
实体类:
data class DepartModel(var id: String = "",var name: String = "",var pid: String = "",var children: ArrayList<DepartModel>? = arrayListOf(),var level: Int = 1,var isOpen: Boolean = false
)
页面选中后的UI需求如下:
TestActivity中设置:
private var departData = arrayListOf<DepartModel>()private lateinit var builder: DepartListDialog.Builderprivate var tmpSelectDepart = DepartModel()private var selectDepart = DepartModel()//编辑页面中传入的已选择部门idprivate var department_id = ""//编辑页面中传入的已选择部门list(包含已选择的所有父级子级)private var department_list = arrayListOf<IdModel>()override fun initView() {builder = DepartListDialog.Builder(this)//如果是编辑页面进入,取出已选择数据选中if (isEditPage) {if (department_list.size > 0) {department_list.reverse()var departName = ""department_list.forEach {if (department_list.indexOf(it) > 1) return@forEachdepartName = it.name + "/" + departNameif (it.id.equals(department_id)) {tmpSelectDepart = DepartModel(id = it.id, name = it.name)}}if (department_list.size > 2) {departName = "……/${departName}"}departName = departName.substring(0, departName.length - 1)setDepartTxt(departName)tmpSelectDepart.name = departName}selectDepart.id = department_idlastSelectBean = DepartModel(id = department_id)}}override fun observeViewModel() {viewModel.departList.observe(this) {departData = ArrayList(it)if (isEditPage) {//从[编辑]跳转,把选中的item展开if (department_list.size > 1) {val ids = arrayListOf<String>()department_list.forEach { selDepart -> ids.add(selDepart.id) }departData.forEach { dd ->if (ids.contains(dd.id)) {//find选中部门的最外层级//循环把所有子级都展开dd.isOpen = truevar cList = dd.children ?: arrayListOf()var i = 0//大于10级就不读了while (i++ < 10 && cList.isNotEmpty()) {//子级中find选中部门的内层级cList.forEach { childDd ->if (ids.contains(childDd.id)) {//find选中部门的内层级
// if(childDd.id==department_id){
// childDd.isSelect=true
// }//子集也打开childDd.isOpen = true//接着赋值新的list循环cList = childDd.children ?: arrayListOf()}}}}}}}builder.setData(departData)if (jumpType == 2) {builder.resetDefault()}showDepartDialog()}viewModel.departSearchList.observe(this) {builder.setSearchData(ArrayList(it))}}private fun showDepartDialog() {if (isEditPage && lastSelectBean?.id?.isNotEmpty() == true) builder.setRightBtnEnable(true)builder.setOnItemSelectClick { view, pos, item ->tmpSelectDepart = item}.setRightClick { s, basicDialog ->if (tmpSelectDepart.id != selectDepart.id) {//切换了部门resetPos()}selectDepart = tmpSelectDepartsetDepartTxt(selectDepart.name)}.setOnSearchItemClick { ids, departModel, pos, basicDialog ->if (departModel.id != selectDepart.id) {//切换了部门resetPos()}tmpSelectDepart = departModelselectDepart = departModellastSelectBean = DepartModel(id = departModel.id, name = departModel.name)setDepartTxt(departModel.name)//将选中状态更新到部门列表builder.resetDefault()//从搜索中选中的,将数据open状态全部重置,使弹窗重新打开就显示出选中itemdepartData.forEach { dd ->if (ids.contains(dd.id)) {//find选中部门的最外层级//循环把所有子级都展开dd.isOpen = truevar cList = dd.children ?: arrayListOf()var i = 0//大于10级就不读了while (i++ < 10 && cList.isNotEmpty()) {//子级中find选中部门的内层级cList.forEach { childDd ->if (ids.contains(childDd.id)) {//find选中部门的内层级//子集也打开childDd.isOpen = true//接着赋值新的list循环cList = childDd.children ?: arrayListOf()} else {childDd.isOpen = false}}}} else {dd.isOpen = false}}}.setOnInputListener { s, basicDialog ->viewModel.searchDepartmentList("0", s)}.addOnDismissListener(object : BasicDialog.OnDismissListener {override fun onDismiss(dialog: BasicDialog?) {setProgressShow(true)builder.setSearchData(arrayListOf())builder.setDefaultInput("")}}).setInputType(InputType.TYPE_CLASS_TEXT).show()setProgressShow(false)}/*** 设置部门名称文字变色*/private fun setDepartTxt(content: String) {val weekStr = content.substring(0, content.lastIndexOf("/") + 1)val weekColor = ContextCompat.getColor(this, R.color.color_BFBFBF)val normalColor = ContextCompat.getColor(this, R.color.color_303030)setPartText(binding.tvDepart, content, weekStr, normalColor, weekColor)}companion object {var curAdapter: DepartTreeAdapter? = nullvar lastSelectBean: DepartModel? = null}
这里解释下为什么放了静态的curAdapter和lastSelectBean吧(如果不需要选中效果的话可以不要这块儿)
因为嵌套了多层子级RecyclerView和配套adapter,如果本来选中第一层,改选第二层,那么现在你去notifyAdapter的时候,只能通知到当前第二层的子Adapter,原来第一层已选中的Adapter没法通知刷新到,UI上就无法取消选择,所以直接把它放在Activity里暂存,这样Adapter更新后,Activity里的依然是上次选择时用的那一个。
lastSelectBean为什么不放在Adapter里呢,原因跟上面一样,Adapter一加载子级就是新的Adapter了,lastSelectBean也就变成新的了。
当然这种方法很der哈,但是咱就是工资也很der,这样很合理