下拉多级分类弹框

79 阅读4分钟

下拉多级分类弹框

对象元素

data class Category(
    val id: String,
    val name: String,
//    val children: List<Category> = emptyList()
)

条目布局 item_category.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/tv_category_name"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:background="?attr/selectableItemBackground"
    android:ellipsize="end"
    android:gravity="center_vertical"
    android:maxLines="1"
    android:paddingHorizontal="16dp"
    android:textSize="16sp" />

条目适配器 CategoryAdapter

package cn.nio.categraywindowdemo

/**
 * @desc 功能描述
 * @Author Developer
 * @Date 2025/8/1-17:45
 */
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class CategoryAdapter(
    private val themeColor: Int,
    private val categories: MutableList<Category>,
    private val onItemClick: (Category) -> Unit,
) : RecyclerView.Adapter<CategoryAdapter.ViewHolder>() {

    private var selectedPosition = -1

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val nameTextView: TextView = itemView.findViewById(R.id.tv_category_name)

        init {
            itemView.setOnClickListener {
                val position = adapterPosition
                if (position != RecyclerView.NO_POSITION) {
                    selectedPosition = position
                    notifyDataSetChanged()
                    onItemClick(categories[position])
                }
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_category, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val category = categories[position]
        holder.nameTextView.text = category.name

        // 设置选中状态
        holder.itemView.isSelected = position == selectedPosition
        holder.nameTextView.setTextColor(
            if (position == selectedPosition) {
                themeColor
            } else {
                Color.parseColor("#666666")
            }
        )
    }

    override fun getItemCount() = categories.size

    fun setSelectedPosition(position: Int) {
        selectedPosition = position
        notifyDataSetChanged()
    }

    // 添加更新数据的方法
    fun updateData(newData: MutableList<Category>) {
        this.categories.clear()
        if (newData.isNotEmpty()) {
            this.categories.addAll(newData)
        }
        notifyDataSetChanged()
    }
}

弹框布局 popup_category_layout

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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="match_parent"
    android:background="@android:color/transparent">

    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#80000000" />

    <LinearLayout
        android:id="@+id/ll_category_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_level1"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:background="#f6f6f6"
            android:maxHeight="400dp" />

        <View
            android:layout_width="1dp"
            android:layout_height="match_parent"
            android:background="#f8f8f8" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_level2"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:background="#e9e9e9"
            android:maxHeight="400dp"
            android:visibility="gone" />

        <View
            android:layout_width="1dp"
            android:layout_height="match_parent"
            android:background="#f8f8f8"
        />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_level3"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:background="#dcdcdc"
            android:maxHeight="400dp"
            android:visibility="gone" />

    </LinearLayout>

</FrameLayout>

弹框主功能管理类CategoryPopupManager

package cn.nio.categraywindowdemo


import android.content.Context
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.PopupWindow
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel

/**
 * @desc 功能描述
 * @Author Developer
 * @Date 2025/8/1-17:47
 */
// 定义数据加载回调接口
interface DataCallback<T> {
    fun onSuccess(data: T)
    fun onFailure(errorMsg: String)
}

interface CategoryDataSource {
    // 加载一级分类(通过回调返回数据)
    fun loadLevel1Categories(callback: DataCallback<MutableList<Category>>)

    // 加载二级分类(基于一级分类ID,通过回调返回数据)
    fun loadLevel2Categories(level1Id: String, callback: DataCallback<MutableList<Category>>)

    // 加载三级分类(基于一级和二级分类ID,通过回调返回数据)
    fun loadLevel3Categories(
        level1Id: String,
        level2Id: String,
        callback: DataCallback<MutableList<Category>>,
    )
}

class CategoryPopupManager(
    private val context: Context,
    private val themeColor: Int,
) : CoroutineScope by MainScope() {  // 使用MainScope管理生命周期
    private lateinit var popupWindow: PopupWindow
    private lateinit var level1Adapter: CategoryAdapter
    private lateinit var level2Adapter: CategoryAdapter
    private lateinit var level3Adapter: CategoryAdapter
    private lateinit var level1RecyclerView: RecyclerView
    private lateinit var level2RecyclerView: RecyclerView
    private lateinit var level3RecyclerView: RecyclerView

    // 保存当前选中的分类
    private var selectedLevel1: Category? = null
    private var selectedLevel2: Category? = null

    // 回调接口
    var onCategorySelected: ((Category) -> Unit)? = null
    var onLoadError: ((String) -> Unit)? = null  // 错误回调

    // 数据来源接口,由外部设置
    var dataSource: CategoryDataSource? = null

    init {
        initPopupWindow()
    }

    private fun initPopupWindow() {
        val view = View.inflate(context, R.layout.popup_category_layout, null)
        popupWindow = PopupWindow(
            view,
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.WRAP_CONTENT,
            true
        ).apply {
            setBackgroundDrawable(ContextCompat.getDrawable(context, android.R.color.transparent))
            // 弹窗消失时取消所有协程,防止内存泄漏
            setOnDismissListener { cancel() }
        }
        val llCategoryLayout = view.findViewById<LinearLayout>(R.id.ll_category_layout)
        // 初始化RecyclerView
        level1RecyclerView = view.findViewById(R.id.rv_level1)
        level2RecyclerView = view.findViewById(R.id.rv_level2)
        level3RecyclerView = view.findViewById(R.id.rv_level3)

        level1RecyclerView.layoutManager = LinearLayoutManager(context)
        level2RecyclerView.layoutManager = LinearLayoutManager(context)
        level3RecyclerView.layoutManager = LinearLayoutManager(context)

        // 初始化适配器
        level1Adapter = CategoryAdapter(themeColor, mutableListOf()) { category ->
            // 检查是否有二级分类
//            if (category.children.isEmpty()) {
//                // 没有下一级,直接返回当前选择
//                onCategorySelected?.invoke(category)
//                dismiss()
//            } else {
            // 有下一级,保存选中项并加载数据
            selectedLevel1 = category
            loadLevel2Data(category.id)
//            }
        }

        level2Adapter = CategoryAdapter(themeColor, mutableListOf()) { category ->
            // 检查是否有三级分类
//            if (category.children.isEmpty()) {
//                // 没有下一级,直接返回当前选择
//                onCategorySelected?.invoke(category)
//                dismiss()
//            } else {
            // 有下一级,保存选中项并加载数据
            selectedLevel2 = category
            loadLevel3Data(selectedLevel1?.id ?: "", category.id)
//            }
        }

        level3Adapter = CategoryAdapter(themeColor, mutableListOf()) { category ->
            // 三级分类为最后一级,直接返回
            onCategorySelected?.invoke(category)
            dismiss()
        }

        level1RecyclerView.adapter = level1Adapter
        level2RecyclerView.adapter = level2Adapter
        level3RecyclerView.adapter = level3Adapter
        val dp12 = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            12f,
            context.resources.displayMetrics
        )
        createCustomCornerDrawable(
            Color.parseColor("#f6f6f6"),
            0f,
            0f,
            dp12,
            dp12,
        ).let {
            level1RecyclerView.background = it
        }

        createCustomCornerDrawable(
            Color.parseColor("#e9e9e9"),
            0f,
            0f,
            dp12,
            0f,
        ).let {
            level2RecyclerView.background = it
        }
        createCustomCornerDrawable(
            Color.parseColor("#dcdcdc"),
            0f,
            0f,
            dp12,
            0f,
        ).let {
            level3RecyclerView.background = it
        }
        createCustomCornerDrawable(
            Color.parseColor("#ffffff"),
            0f,
            0f,
            dp12,
            dp12,
        ).let {
            llCategoryLayout.background = it
        }
    }

    /**
     * 创建带自定义四边角度的Drawable用于选中状态(纯色)
     */
    private fun createCustomCornerDrawable(
        color: Int,
        topLeft: Float,
        topRight: Float,
        bottomRight: Float,
        bottomLeft: Float,
    ): GradientDrawable {
        return GradientDrawable().apply {
            // 使用单一颜色替代渐变
            setColor(color)
            // 设置四个角的半径
            cornerRadii = floatArrayOf(
                topLeft, topLeft,
                topRight, topRight,
                bottomRight, bottomRight,
                bottomLeft, bottomLeft
            )
            // 移除渐变相关设置
            setStroke(2, color)
        }
    }

    // 加载一级分类数据
    fun loadLevel1Data() {
        val dataSource = dataSource ?: run {
            val errorMsg = "请先设置CategoryDataSource"
            onLoadError?.invoke(errorMsg)
            return
        }

        dataSource.loadLevel1Categories(object : DataCallback<MutableList<Category>> {
            override fun onSuccess(data: MutableList<Category>) {
                level1Adapter.updateData(data)
            }

            override fun onFailure(errorMsg: String) {
                onLoadError?.invoke("加载一级分类失败: $errorMsg")
            }
        })
    }

    // 加载二级分类数据
    private fun loadLevel2Data(level1Id: String) {
        val dataSource = dataSource ?: return
        val currentLevel1 = selectedLevel1 ?: return
        level2Adapter.setSelectedPosition(-1)
        dataSource.loadLevel2Categories(level1Id, object : DataCallback<MutableList<Category>> {
            override fun onSuccess(data: MutableList<Category>) {
                if (data.isEmpty()) {
                    // 接口返回空数据,返回当前一级分类
                    onCategorySelected?.invoke(currentLevel1)
                    dismiss()
                } else {
                    // 正常显示二级分类
                    level2Adapter.updateData(data)
                    level2RecyclerView.visibility = View.VISIBLE
                    // 清空三级列表
                    level3Adapter.updateData(mutableListOf())
                    level3RecyclerView.visibility = View.GONE
                }
            }

            override fun onFailure(errorMsg: String) {
                onLoadError?.invoke("加载二级分类失败: $errorMsg")
            }
        })
    }

    // 加载三级分类数据
    private fun loadLevel3Data(level1Id: String, level2Id: String) {
        val dataSource = dataSource ?: return
        val currentLevel2 = selectedLevel2 ?: return
        level3Adapter.setSelectedPosition(-1)
        dataSource.loadLevel3Categories(
            level1Id,
            level2Id,
            object : DataCallback<MutableList<Category>> {
                override fun onSuccess(data: MutableList<Category>) {
                    if (data.isEmpty()) {
                        // 接口返回空数据,返回当前二级分类
                        onCategorySelected?.invoke(currentLevel2)
                        dismiss()
                    } else {
                        // 正常显示三级分类
                        level3Adapter.updateData(data)
                        level3RecyclerView.visibility = View.VISIBLE
                    }
                }

                override fun onFailure(errorMsg: String) {
                    onLoadError?.invoke("加载三级分类失败: $errorMsg")
                }
            })
    }

    fun show(anchor: View) {
        // 显示前确保已加载数据
        if (level1Adapter.itemCount == 0) {
            loadLevel1Data()
        }
        popupWindow.showAsDropDown(anchor, 0, 0, Gravity.START)
    }

    fun dismiss() {
        popupWindow.dismiss()
    }

    fun isShowing() = popupWindow.isShowing
}

使用示例

一二三级数据可更换为接口获取,如果下一级没有,调用callback.onSuccess(mutableListOf())

package cn.nio.categraywindowdemo

import android.graphics.Color
import android.os.Bundle
import android.widget.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat

class MainActivity : AppCompatActivity() {
    private lateinit var categoryPopupManager: CategoryPopupManager
    private lateinit var anchorView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_main)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
        anchorView = findViewById(R.id.anchor_view)
        categoryPopupManager = CategoryPopupManager(this, Color.parseColor("#0080ff"))

        // 设置弹窗选择回调
        categoryPopupManager.onCategorySelected = { category ->
            anchorView.text = category.name
            Toast.makeText(this, "选择了分类:${category.name}", Toast.LENGTH_SHORT).show()
        }

        // 点击锚点View显示弹窗
        anchorView.setOnClickListener {
            if (categoryPopupManager.isShowing()) {
                categoryPopupManager.dismiss()
            } else {
                categoryPopupManager.show(it)
            }
        }
        categoryPopupManager.dataSource = object : CategoryDataSource {
            override fun loadLevel1Categories(callback: DataCallback<MutableList<Category>>) {
                callback.onSuccess(
                    mutableListOf(
                        Category("1", "一级分类1"),
                        Category("1", "一级分类2"),
                        Category("1", "一级分类3")
                    )
                )
            }

            override fun loadLevel2Categories(
                level1Id: String,
                callback: DataCallback<MutableList<Category>>,
            ) {
                callback.onSuccess(
                    mutableListOf(
                        Category("4", "二级分类1"),
                        Category("5", "二级分类2"),
                        Category("6", "二级分类3")
                    )
                )
            }

            override fun loadLevel3Categories(
                level1Id: String,
                level2Id: String,
                callback: DataCallback<MutableList<Category>>,
            ) {
//                callback.onSuccess(mutableListOf())
                callback.onSuccess(
                    mutableListOf(
                        Category("7", "三级分类1"),
                        Category("8", "三级分类2"),
                        Category("9", "三级分类3")
                    )
                )
            }

        }
    }
}

效果展示

Screen_recording_20250820_162544 00_00_00-00_00_30.gif