效果如下:
说明:
实现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
)