Android 实现SKU选择通用方式

效果如下:

在这里插入图片描述

说明:

实现sku的方式一般采用在获取到数据后拆分所有条件的可能性,实现方式参考js的实现,代码如下:

SkuHelp.kt

/**
不考虑服务端的格式类型,将对应格式翻译成如下格式:
     *[
     * { "颜色": "红", "尺码": "大", "型号": "A", "skuId": "3158055" },
     * { "颜色": "白", "尺码": "大", "型号": "A", "skuId": "3158054" },
     * { "颜色": "白", "尺码": "中", "型号": "B", "skuId": "3133859" },
     * { "颜色": "蓝", "尺码": "小", "型号": "C", "skuId": "3516833" }
     *]
     (注:skuId为数据id)
**/
class SkuHelp {

    private val skuArray = "skuArray"

    // 将数据翻译为sku可识别的通用数据,此处为特殊处理方式
    fun transformToBean(goodsSpecArray: List<SpecificationsBean>): Pair<Map<String, String>, List<Map<String, String>>> {
        val rootArray = mutableListOf<MutableMap<String, String>>()
        val tagImgMap = mutableMapOf<String, String>()

        goodsSpecArray.forEach { bean ->
            if (bean.id != null && bean.goodsSpecValue != null) {
                val valueMap = bean.goodsSpecValue.mapFromJson<String, String>()
                valueMap["id"] = bean.id

                // 处理数据中有图片的现象
                valueMap.forEach { (k, v) ->
                    // 判断value上是否有图片存在
                    if (v.contains(",")) {
                        val splitArray = v.split(',')
                        valueMap[k] = splitArray[0]
                        tagImgMap[splitArray[0]] = splitArray[1]
                    }
                }
                rootArray.add(valueMap)
            }
        }

        return Pair(tagImgMap, rootArray)
    }

    /**
     * rootArray 格式为:
     *[
     * { "颜色": "红", "尺码": "大", "型号": "A", "skuId": "3158055" },
     * { "颜色": "白", "尺码": "大", "型号": "A", "skuId": "3158054" },
     * { "颜色": "白", "尺码": "中", "型号": "B", "skuId": "3133859" },
     * { "颜色": "蓝", "尺码": "小", "型号": "C", "skuId": "3516833" }
     *]
     */
    fun initData(rootArray: List<Map<String, String>>): Triple<Map<String, List<String>>, Map<String, Map<String, List<String>>>, List<String>> {
        val keyArray = getSkuKey(rootArray)
        val (allKeyArray, resultMap) = combineAttr(rootArray, keyArray)
        val conditionMap = buildResult(allKeyArray)

        return Triple(resultMap, conditionMap, keyArray)
    }

    /**
     * 处理合并后的条件会有多个特殊字符
     */
    fun trimSplit(trim: String): String {
        // ⊙abc⊙ => abc
        // ⊙a⊙⊙b⊙c⊙ => a⊙b⊙c
        val reLeft = Regex("^$spliter+")
        val reRight = Regex("$spliter+\$")
        val reSplit = Regex("$spliter+")

        return trim.replace(reLeft, "")
            .replace(reRight, "")
            .replace(reSplit, spliter)
    }

    // 获取条件sku
    fun getSkuArray(condition: String, conditionMap: Map<String, Map<String, List<String>>>): List<String>? {
        val newMap = conditionMap[condition]
        return newMap?.get(skuArray)
    }

    // 获取key, 如:颜色,品牌,尺码
    private fun getSkuKey(rootArray: List<Map<String, String>>): List<String> {
        val keyArray = mutableListOf<String>()
        if (rootArray.isNotEmpty()) {
            val valueMap = rootArray[0]
            valueMap.forEach { (k, _) ->
                // 过滤数据为id的项
                if (k == "id") {
                    return@forEach
                }
                keyArray.add(k)
            }
        }
        return keyArray
    }

    val spliter = "\u2299"

    // 计算组合数据
    private fun combineAttr(
        rootArray: List<Map<String, String>>,
        keyArray: List<String>
    ): Pair<List<MutableMap<String, String>>, MutableMap<String, MutableList<String>>> {
        // 将数据转换成可视化数据,通过id查找对应数据
        /**
         * 为了通用,此处将数据打包成如下格式:
        [
        { "颜色": "红", "尺码": "大", "型号": "A", "skuId": "3158055" },
        { "颜色": "白", "尺码": "大", "型号": "A", "skuId": "3158054" },
        { "颜色": "白", "尺码": "中", "型号": "B", "skuId": "3133859" },
        { "颜色": "蓝", "尺码": "小", "型号": "C", "skuId": "3516833" }
        ]
         */
//        val beanArray = mutableListOf<MutableMap<String, String>>()
//        goodsSpecArray.forEach { bean ->
//            if (bean.id != null && bean.goodsSpecValue != null) {
//                val valueMap = bean.goodsSpecValue.mapFromJson<String, String>()
//                valueMap["id"] = bean.id
//                beanArray.add(valueMap)
//            }
//        }

        // 将界面展示数据分离如: {"颜色":["红","白","蓝"],"尺码":["大","中","小"],"型号":["A","B","C"]}
        val resultMap = mutableMapOf<String, MutableList<String>>()
        // 将条件与id整合起来如:[{{path=红⊙大⊙A, sku=3158055}...}]
        val allKeyArray = mutableListOf<MutableMap<String, String>>()

        for (itemMap in rootArray) {
            val valueArray = mutableListOf<String>()
            for (key in keyArray) {
                val array = mutableListOf<String>()
                if (resultMap[key] != null) {
                    array.addAll(resultMap[key]!!)
                }
                if (!array.contains(itemMap[key])) {
                    array.add(itemMap[key]!!)
                }
                resultMap[key] = array
                valueArray.add(itemMap[key]!!)
            }
            allKeyArray.add(
                mutableMapOf(
                    "path" to valueArray.joinToString(separator = spliter),
                    "sku" to itemMap["id"]!!
                )
            )
        }

        return Pair(allKeyArray, resultMap)
    }

    // 合并所有条件
    private fun getAllKeys(allKeyArray: List<Map<String, String>>): List<String> {
        // 如: ["红⊙大⊙A",...]
        val keyArray = mutableListOf<String>()
        allKeyArray.forEach { keyMap ->
            keyArray.add(keyMap["path"]!!)
        }
        return keyArray
    }

    // 生成所有子集是否可选、库存状态 map
    private fun buildResult(allKeyArray: List<Map<String, String>>): MutableMap<String, MutableMap<String, MutableList<String>>> {
        // 将条件整合成一个key的List
        val allKeys = getAllKeys(allKeyArray)
        // 获取所有数据的可能性如: {"蓝色⊙X"={skuArray=[1, 2, 3]}...}
        val resMap = mutableMapOf<String, MutableMap<String, MutableList<String>>>()

        allKeys.forEachIndexed { index, allKey ->
            val sku = allKeyArray[index]["sku"]
            val values = allKey.split(spliter)

            val allSets = powerSet(values)
            // 每个组合的子集
            allSets.forEachIndexed { _, set ->
                val key = set.joinToString(separator = spliter)

                if (resMap[key] != null) {
                    resMap[key]?.get(skuArray)?.add(sku!!)
                } else {
                    resMap[key] = mutableMapOf(
                        skuArray to mutableListOf(sku!!)
                    )
                }
            }
        }

        return resMap
    }

    /**
     * 取得集合的所有子集「幂集」
    arr = [1,2,3]

    i = 0, ps = [[]]:
    j = 0; j < ps.length => j < 1:
    i=0, j=0 ps.push(ps[0].concat(arr[0])) => ps.push([].concat(1)) => [1]
    ps = [[], [1]]

    i = 1, ps = [[], [1]] :
    j = 0; j < ps.length => j < 2
    i=1, j=0 ps.push(ps[0].concat(arr[1])) => ps.push([].concat(2))  => [2]
    i=1, j=1 ps.push(ps[1].concat(arr[1])) => ps.push([1].concat(2)) => [1,2]
    ps = [[], [1], [2], [1,2]]

    i = 2, ps = [[], [1], [2], [1,2]]
    j = 0; j < ps.length => j < 4
    i=2, j=0 ps.push(ps[0].concat(arr[2])) => ps.push([3])    => [3]
    i=2, j=1 ps.push(ps[1].concat(arr[2])) => ps.push([1, 3]) => [1, 3]
    i=2, j=2 ps.push(ps[2].concat(arr[2])) => ps.push([2, 3]) => [2, 3]
    i=2, j=3 ps.push(ps[3].concat(arr[2])) => ps.push([2, 3]) => [1, 2, 3]
    ps = [[], [1], [2], [1,2], [3], [1, 3], [2, 3], [1, 2, 3]]
     */
    private fun powerSet(set: List<String>): List<List<String>> {
        //已知所求集合的幂集会有2^n个元素
        val size = 2 shl set.size
        val powerSet: MutableList<List<String>> = ArrayList(size)
        //首先空集肯定是集合的幂集
        powerSet.add(Collections.emptyList())
        for (element in set) {
            //计算当前元素与已存在幂集的组合
            val preSize = powerSet.size
            for (i in 0 until preSize) {
                val combineSubset: MutableList<String> = ArrayList(powerSet[i])
                combineSubset.add(element)
                powerSet.add(combineSubset)
            }
        }
        return powerSet
    }
}

SpecificationsDialog.kt

/**
 * 商品规格选择sku
 */
class SpecificationsDialog(private val contextX: Context) :
    AlertDialog(contextX, R.style.DialogWindowStyle_Shop_bg) {
    private val binding: PdDialogLayoutSpecificationsBinding =
        PdDialogLayoutSpecificationsBinding.inflate(layoutInflater)

    private val adapter: SpecificationsDialogAdapter = SpecificationsDialogAdapter()

    // 存储有库存,以id建立的字典方便为了获取具体数据
    private val mapModel = mutableMapOf<String, SpecificationsBean>()

    // 需要满足的条件总数
    private var totalConditions = 0

    // adapter数据源
    private val skuBeanArray = mutableListOf<ShowSKUBean>()

    // 缓存之前存储的条件 - 选中条件,key是(如:品牌:x1), value是goodsSpecValue
    private val cacheValueMap = LinkedHashMap<String, String>()

    // =
    private val cacheConditionMap: MutableMap<String, Map<String, List<String>>> = mutableMapOf()

    // sku 列的名称,如:品牌,..
    private val keysArray: MutableList<String> = mutableListOf()

    // 具体sku数据,用于提交
    private var cacheModel: SpecificationsBean? = null

    private val skuHelp = SkuHelp()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        initDefWindows()
        initView()

        initTestData()
        binding.pdTvStock.text = "1000"
    }

    // 重置所有属性
    private fun resetData() {
        cacheModel = null
        totalConditions = 0
        keysArray.clear()
        cacheConditionMap.clear()
        cacheValueMap.clear()
        skuBeanArray.clear()
        mapModel.clear()
    }

    // 初始化数据
    fun initData(beanArray: List<SpecificationsBean>) {
        resetData()

        // 将数据转换成map,用id对应数据方便查找
        beanArray.forEachIndexed { index, bean ->
            if (index == 0) {
                cacheModel = bean
            }
            mapModel[bean.id!!] = bean
        }

        val (tagImgMap, goodsSpecMap) = skuHelp.transformToBean(beanArray)
        val (uiMap, conditionMap, keysArray) = skuHelp.initData(goodsSpecMap)

        this.keysArray.addAll(keysArray)

        // 需要满足的条件总数
        totalConditions = uiMap.size
        uiMap.forEach { (k, vList) ->
            val dvList = vList.map { v ->
                if (tagImgMap[v] == null) {
                    DialogSpecsValue(value = v)
                } else {
                    DialogSpecsValue(value = v, img = tagImgMap[v])
                }
            }
            skuBeanArray.add(ShowSKUBean(k, dvList))
        }

        cacheConditionMap.putAll(conditionMap)
        defSelected()
        adapter.setList(skuBeanArray)
    }

    // 默认选中数据中第一条
    private fun defSelected() {
        cacheModel?.let { bean ->
            if (bean.goodsSpecValue == null) return@let
            val valueMap = bean.goodsSpecValue.mapFromJson<String, String>()
            keysArray.forEach { key ->
                val value = valueMap[key] ?: return@forEach
                if (value.contains(",")) {
                    val splitArray = value.split(',')
                    cacheValueMap[key] = splitArray[0]
                } else {
                    cacheValueMap[key] = value
                }
            }

            defSettingSelected()
        }
    }

    // TODO 暂时用于测试,部分逻辑可以通用
    // 模拟数据
    private fun initTestData() {
        val goodsSpecArray = mutableListOf(
            SpecificationsBean(
                "1",
                "0",
                goodsDefaluePrice = 100f,
                goodsPrice = 85.6f,
                goodsNums = 10000,
                goodsSpecValue = "{\"颜色\":\"蓝色\",\"尺码\":\"X\",\"风格\":\"时尚\"}"
            ),
            SpecificationsBean(
                "2",
                "0",
                goodsDefaluePrice = 100f,
                goodsPrice = 87.2f,
                goodsNums = 10000,
                goodsSpecValue = "{\"颜色\":\"蓝色\",\"尺码\":\"X\",\"风格\":\"简约\"}"
            ),
            SpecificationsBean(
                "3",
                "0",
                goodsDefaluePrice = 100f,
                goodsPrice = 87.2f,
                goodsNums = 10000,
                goodsSpecValue = "{\"颜色\":\"蓝色\",\"尺码\":\"X\",\"风格\":\"欧式\"}"
            ),
            SpecificationsBean(
                "4",
                "0",
                goodsDefaluePrice = 100f,
                goodsPrice = 67.2f,
                goodsNums = 10000,
                goodsSpecValue = "{\"颜色\":\"蓝色\",\"尺码\":\"M\",\"风格\":\"简约\"}"
            ),
            SpecificationsBean(
                "5",
                "0",
                goodsDefaluePrice = 100f,
                goodsPrice = 57.2f,
                goodsNums = 10000,
                goodsSpecValue = "{\"颜色\":\"蓝色\",\"尺码\":\"M\",\"风格\":\"欧式\"}"
            ),
            SpecificationsBean(
                "6",
                "0",
                goodsDefaluePrice = 100f,
                goodsPrice = 87.2f,
                goodsNums = 10000,
                goodsSpecValue = "{\"颜色\":\"红色\",\"尺码\":\"X\",\"风格\":\"时尚\"}"
            ),
            SpecificationsBean(
                "7",
                "0",
                goodsDefaluePrice = 100f,
                goodsPrice = 83.2f,
                goodsNums = 10000,
                goodsSpecValue = "{\"颜色\":\"红色\",\"尺码\":\"M\",\"风格\":\"时尚\"}"
            ),
        )

        resetData()

        // 将数据转换成map,用id对应数据方便查找
        goodsSpecArray.forEachIndexed { index, bean ->
            if (index == 0) {
                cacheModel = bean
            }
            mapModel[bean.id!!] = bean
        }

        val (tagImgMap, goodsSpecMap) = skuHelp.transformToBean(goodsSpecArray)
        val (uiMap, conditionMap, keysArray) = skuHelp.initData(goodsSpecMap)

        this.keysArray.addAll(keysArray)

        // 需要满足的条件总数
        totalConditions = uiMap.size
        uiMap.forEach { (k, vList) ->
            val dvList = vList.map { v ->
                if (tagImgMap[v] == null) {
                    DialogSpecsValue(value = v)
                } else {
                    DialogSpecsValue(value = v, img = tagImgMap[v])
                }
            }
            skuBeanArray.add(ShowSKUBean(k, dvList))
        }

        cacheConditionMap.putAll(conditionMap)
        defSelected()
        adapter.setList(skuBeanArray)
    }

    private fun initView() {
        binding.pdItvBack.setOnClickListener { dismiss() }
        binding.pdRvSpecs.adapter = adapter
        val layoutManager = LinearLayoutManager(contextX)
        layoutManager.orientation = LinearLayoutManager.VERTICAL
        binding.pdRvSpecs.layoutManager = layoutManager

        adapter.setItemClick { holder, p, dialogSpecsValue, key ->
            // 将选中条件缓存
            if (!dialogSpecsValue.isSelect) {
                cacheValueMap[key] = dialogSpecsValue.value
            } else {
                cacheValueMap.remove(key)
            }

            settingSelected(key, dialogSpecsValue)
            adapter.notifyDataSetChanged()

            // 判断是否需要给出价格
            if (this.totalConditions == cacheValueMap.size) {
                val synthesisKey = obtainConditionSplicing()
                val valueArray = skuHelp.getSkuArray(synthesisKey, cacheConditionMap)
                var id = ""
                if (valueArray.isNullOrEmpty()) {
                    "无此属性搭配".showToast()
                    return@setItemClick
                } else {
                    id = valueArray[0]
                }
                cacheModel = mapModel[id]
                Log.e("fyc", " bean >>>> $cacheModel")
            } else {
                cacheModel = null
            }
        }
    }

    private fun initDefWindows() {
        setCanceledOnTouchOutside(true)
        setCancelable(true)

        val localWindow = this.window
        localWindow?.setWindowAnimations(R.style.DialogWindowStyle)
        localWindow?.setGravity(Gravity.BOTTOM)
        localWindow?.setBackgroundDrawableResource(android.R.color.transparent)
        val lp = localWindow!!.attributes
        val wh =
            intArrayOf(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
        lp.width = wh[0]
        lp.height = wh[1]
        localWindow.attributes = lp
    }

    // 更新所有属性状态
    private fun updateStatus() {

        val cacheValueArray = mutableListOf<String>()
        cacheValueMap.forEach { (_, v) ->
            cacheValueArray.add(v)
        }

        skuBeanArray.forEachIndexed { i, bean ->
            val copyArray = Array(totalConditions){""}
            for (z in 0..totalConditions) {
                if (z < cacheValueArray.size) {
                    copyArray[z] = cacheValueArray[z]
                }
            }

            bean.valueList.forEach valueTag@ { valueBean ->
                // 选中项,忽略
                if (cacheValueMap[bean.key] == valueBean.value) {
                    // continue
                    return@valueTag
                }

                Log.e("fyc", "${bean.key} :  ${valueBean.value}")
                copyArray[i] = valueBean.value

                // 合并成条件
                val conditionKey = skuHelp.trimSplit(copyArray.joinToString(separator = skuHelp.spliter))
                // 获取合适的条件
                if (cacheConditionMap[conditionKey] == null) {
                    valueBean.isCanSelect = false
                    valueBean.isSelect = false
                } else {
                    valueBean.isCanSelect = true
                    valueBean.isSelect = false
                }
            }
        }
    }

    // 设置选中项 - item点击事件
    private fun settingSelected(key: String, bean: DialogSpecsValue) {
        // 遍历现有数据判断可选项
        skuBeanArray.forEach { model ->
            // 判断当前项目是否具备选中项
            val selectionVal = cacheValueMap[model.key]
            model.valueList.forEach { dialogSpecsValue ->
                // 表示当前项目被选中
                if (selectionVal == dialogSpecsValue.value) {
                    dialogSpecsValue.isSelect = true
                }
                // 当前选中项,是无效选项触发
                if (selectionVal == dialogSpecsValue.value && !dialogSpecsValue.isCanSelect) {
                    cacheValueMap.clear()
                    cacheValueMap[key] = bean.value
                    dialogSpecsValue.isSelect = true
                    dialogSpecsValue.isCanSelect = true
                }
            }
        }

        // 对当前条件进行排序,防止出现,选中组合混乱
        val selectMap = LinkedHashMap<String, String>()
        keysArray.forEach { dataKey ->
            if (cacheValueMap[dataKey] != null) {
                selectMap[dataKey] = cacheValueMap[dataKey]!!
            }
        }
        cacheValueMap.clear()
        cacheValueMap.putAll(selectMap)

        updateStatus()
    }

    // 设置选中项
    private fun defSettingSelected() {
        // 遍历现有数据判断可选项
        skuBeanArray.forEach { model ->
            // 判断当前项目是否具备选中项
            val selectionVal = cacheValueMap[model.key]
            model.valueList.forEach { dialogSpecsValue ->
                // 表示当前项目被选中
                if (selectionVal == dialogSpecsValue.value) {
                    dialogSpecsValue.isSelect = true
                }
            }
        }

        updateStatus()
    }

    // 将选中的条件组成和实际可选条件获取key
    private fun obtainConditionSplicing(): String {
        val splicingArray = mutableListOf<String>()
        cacheValueMap.forEach { (_, v) ->
            splicingArray.add(v)
        }
        return splicingArray.joinToString(separator = skuHelp.spliter)
    }
}

SpecificationsBean.kt

/**
 * 商铺规格
 */
@JsonClass(generateAdapter=true)
data class SpecificationsBean(
    // 商品规格id
    val id: String?,
    // 商品id
    val goodsId: String?,
    // 商品规格值
    val goodsSpecValue: String?,
    // 商品价格
    val goodsPrice: Float?,
    // 商品原价
    val goodsDefaluePrice: Float?,
    // 商品数量 - 库存
    val goodsNums: Int?
)

ShowSKUBean.kt

/**
 * 将map数据转换成rv可识别的数据
 */
data class ShowSKUBean(
    // 如:品牌
    val key: String,
    // 如:x1,x2...
    val valueList: List<DialogSpecsValue>
)

DialogSpecsValue.kt

/**
 * dialog-sku展示项
 */
data class DialogSpecsValue(
    // 是否能选择
    var isCanSelect: Boolean = true,
    // 是否能选中
    var isSelect: Boolean = false,
    var value: String = "",
    // 对应数据图片,没有可为空
    var img: String? = null
) 

总结:代码比较多,需要给位耐心看完,可以实现图片中一样的效果,懒得废话太多,sku实现的思路并不复杂,麻烦的是如何写算法处理,此处只展示一种sku实现方式,这种方式的弊端是吃内存,但至少效果可以实现,一般来讲后端会对应作出限制,理论上内存这块不用太过于担心,由于我的数据中如:‘颜色:蓝色,/static/x/x.jpg’ value可能携带图片此处处理方式是针对于需要图片的方式,如果不需要处理图片,直接将图片处理的代码删除即可,希望本文对大家有所帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值