Android邻接矩阵实现的商品规格选择

介绍

电商相关app在商品购买页面选择商品,根据不同的规格组合,选择对应的商品

效果预览

sku.gif

解决思路

使用邻接矩阵解决

假设我们有如下规格列表:

specList: [
  { title: "颜色", list: ["紫色", "红色"] },
  { title: "套餐", list: ["套餐一", "套餐二"] },
  { title: "内存", list: ["64G", "128G", "256G"] }
]
specList: [
  { title: "头部", list: ["紫色", "红色"] },
  { title: "身体", list: ["黑色", "绿色", "紫色"] },
  { title: "腿部", list: ["紫色", "粉色"] }, 
  { title: "鞋子", list: ["回力", "飞跃"] }
]
复制代码

可供选择的规格组合:

specCombinationList: [
    { id: "1", specs: ["紫色", "套餐一", "64G"] },
    { id: "2", specs: ["紫色", "套餐一", "128G"] },
    { id: "3", specs: ["紫色", "套餐二", "128G"] },
    { id: "4", specs: ["红色", "套餐二", "256G"] }
  ]
  specCombinationList: [
    { id: "1", specs: ["紫色", "黑色", "紫色", "回力"] },
    { id: "2", specs: ["紫色", "紫色", "紫色", "飞跃"] },
    { id: "3", specs: ["红色", "黑色", "粉色", "飞跃"] }
  ]
复制代码

那么根据上面的数据,我们可以得到如下的邻接矩阵:

邻接矩阵

image.png

那么怎么使用该矩阵对应可选的商品规格呢?当我们选中“紫色”规格时:

选中紫色

当我们接着选中“套餐一”规格时:

选中套餐一

仔细观察该邻接矩阵可以发现:

- 左上顶点到右下底点都为0
- 两个顶点两边的数据是对称的
复制代码

所以为力减少遍历,我们只需要遍历如下的范围:

需要遍历的范围

实现

  1. 创建邻接矩阵的二维数组
var num = 0 // 记录邻接矩阵的大小
specList.forEach {
    num += it.list.size
}
// 二维数组
val matrix = Array(num) { IntArray(num) }
复制代码
  1. 仅遍历三角红框范围
for (row in 0 until num) {
    for (column in 0 until row) {}
}
复制代码
  1. 记录每个类别的数目(颜色:2,套餐:2,内存:3)
val specTypeCount = mutableListOf<Int>() // 记录每个类别的数目
specList.forEach {
    specTypeCount.add(it.list.size)
}
复制代码
  1. 根据row/column的index获取该行/列所属的类别index,例如0->紫色->1,5->128g->3
/**
 * 获取类型index
 */
private fun getTypeIndex(column: Int, specTypeCount: List<Int>): Int {
    var c = 0
    var typeIndex = 0
    for (index in specTypeCount.indices) {
        c += specTypeCount[index]
        if (column < c) {
            typeIndex = index
            break
        }
    }
    return typeIndex
}
复制代码
  1. 根据可供选择组合,拼接出可以选择的规格
[{紫色,红色},{套餐一,套餐二},{64g,128g,256g}]
[{紫色,红色},{黑色,紫色},{回力,飞跃}]
复制代码
private fun getCombinations(specCombinationList: List<SpecCombination>): Array<MutableList<String>> {
    val size = if (specCombinationList.isNullOrEmpty()) 0 else specCombinationList.first().specs.size
    val cs = Array<MutableList<String>>(size) { mutableListOf() }
    specCombinationList.forEach {
            it.specs.forEachIndexed { index, spec ->
                cs[index].add(spec)
            }
    }
    return cs
}
复制代码
  1. 当遍历的row规格不存在可选规格的时候,直接跳过这一行的循环,减少遍历。例如绿色规格不在可选规格中,直接跳过
val rowTypeIndex = getTypeIndex(row, specTypeCount)
if (!combinations[rowTypeIndex].contains(specs[row])) {
    continue
}

// 列同理
val columnTypeIndex = getTypeIndex(column, specTypeCount)
if (!combinations[columnTypeIndex].contains(specs[column])) {
    continue
}
复制代码
  1. 行跟列属于同一类别规格,并且同级的两个规格不相同则可以选择为1。
if (isEqualsType) {
    // 同一类别规格不同就为1(横向为"红",纵向只要specCombinationList包含紫色就为1)
    if (specs[row] != specs[column]) {
        matrix[row][column] = 1
        matrix[column][row] = 1 // 对称
    }
}
复制代码
  1. 行跟列不属于同一类别规格,则需要结合包含row规格,并且在可选择的规格里面匹配。匹配成功可以选择为1。例如当前row规格是飞跃,符合的id有
{ id: "2", specs: ["紫色", "紫色", "紫色", "飞跃"] },
{ id: "3", specs: ["红色", "黑色", "粉色", "飞跃"] }
复制代码

头部类别有紫色、红色,身体类别有紫色、黑色,腿部类别有紫色、粉色。所以对应的矩阵为:

image.png

else {
    // 不同类别规格相同就为1
    val newCombinations = getCombinations(specCombinationList, matchIdList)
    if (newCombinations[columnTypeIndex].contains(specs[column])) {
        matrix[row][column] = 1
        matrix[column][row] = 1 // 对称
    }
}
复制代码
  1. 获取首次可选择的规格矩阵,以及选择的规格交集
fun allOr(matrix: Array<IntArray>): IntArray {
    val m = IntArray(matrix.size) { 0 }
    for (row in matrix.indices) {
        for (column in matrix.indices) {
            m[column] = m[column].or(matrix[row][column])
        }
    }
    return m
}

fun rowAnd(rows: ArrayList<Int>, matrix: Array<IntArray>): IntArray {
    val m = allOr(matrix)
    for (row in rows) {
        for (column in matrix.indices) {
            m[column] = m[column].and(matrix[row][column])
        }
    }
    return m
}
复制代码
  1. ui层面随便写了个布局,实际根据自身情况调整。我这里用的recyclerview+linearlayout,linearlayout(没考虑换行)里面使用textview。textview则要设置三种不同的状态,可选择,已选择,不可选择。

完整代码

国际惯例:直接CV就能看效果

class SkuActivity : AppCompatActivity() {

    private val mBinding by lazy { ActivitySkuBinding.inflate(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(mBinding.root)

        // 规格列表
        val specList = listOf(
            Spec("颜色", listOf("紫色", "红色")),
            Spec("套餐", listOf("套餐一", "套餐二")),
            Spec("内存", listOf("64g", "128g", "256g"))
        )
        // 可供选择的规格组合
        val specCombinationList = listOf(
            SpecCombination("1", listOf("紫色", "套餐一", "64g")),
            SpecCombination("2", listOf("紫色", "套餐一", "128g")),
            SpecCombination("3", listOf("紫色", "套餐二", "128g")),
            SpecCombination("4", listOf("红色", "套餐二", "256g"))
        )
        val specAdapter = SpecAdapter(specList, specCombinationList, SpecSkuUtil.transformMatrix(specList, specCombinationList))
        specAdapter.setOnClickSpecListener(object : SpecAdapter.OnClickSpecListener {
            override fun clickSpec(enabled: Boolean, id: String?) {
                mBinding.btnSubmit.isEnabled = enabled
                if (enabled)
                    Toast.makeText(this@SkuActivity, "规格id=$id", Toast.LENGTH_SHORT).show()
            }

        })
        mBinding.rvSpec.adapter = specAdapter

        val specList2 = listOf(
            Spec("头部", listOf("紫色", "红色")),
            Spec("身体", listOf("黑色", "绿色", "紫色")),
            Spec("腿部", listOf("紫色", "粉色")),
            Spec("鞋子", listOf("回力", "飞跃"))
        )
        val specCombinationList2 = listOf(
            SpecCombination("1", listOf("紫色", "黑色", "紫色", "回力")),
            SpecCombination("2", listOf("紫色", "紫色", "紫色", "飞跃")),
            SpecCombination("3", listOf("红色", "黑色", "粉色", "飞跃"))
        )
        val matrix = SpecSkuUtil.transformMatrix(specList2, specCombinationList2)
        for (one in matrix) {
            val temp = mutableListOf<Int>()
            for (two in one) {
                temp.add(two)
            }
            Log.v("Loren", "$temp")
        }
        val specAdapter2 = SpecAdapter(specList2, specCombinationList2, matrix)
        specAdapter2.setOnClickSpecListener(object : SpecAdapter.OnClickSpecListener {
            override fun clickSpec(enabled: Boolean, id: String?) {
                if (enabled)
                    Toast.makeText(this@SkuActivity, "规格id=$id", Toast.LENGTH_SHORT).show()
            }

        })
        mBinding.rvSpec2.adapter = specAdapter2
    }
}

object SpecSkuUtil {

    /**
     * 获取类型index
     */
    private fun getTypeIndex(column: Int, specTypeCount: List<Int>): Int {
        var c = 0
        var typeIndex = 0
        for (index in specTypeCount.indices) {
            c += specTypeCount[index]
            if (column < c) {
                typeIndex = index
                break
            }
        }
        return typeIndex
    }

    /**
     * 已选中某个规格后的可供选择规则
     * 选中"紫色",返回["1","2","3"]
     * 选中"套餐一",返回["1","2"]
     */
    private fun getMatchIdList(typeIndex: Int, specName: String, specCombinationList: List<SpecCombination>): List<String> {
        return specCombinationList
            .filter { combination -> specName == combination.specs[typeIndex] }
            .map { it.id }
    }

    private fun getCombinations(specCombinationList: List<SpecCombination>, matchIds: List<String>? = null): Array<MutableList<String>> {
        val size = if (specCombinationList.isNullOrEmpty()) 0 else specCombinationList.first().specs.size
        val cs = Array<MutableList<String>>(size) { mutableListOf() }
        specCombinationList.forEach {
            if (matchIds == null || matchIds.contains(it.id)) {
                it.specs.forEachIndexed { index, spec ->
                    cs[index].add(spec)
                }
            }
        }
        return cs
    }

    fun transformMatrix(specList: List<Spec>, specCombinationList: List<SpecCombination>): Array<IntArray> {
        var num = 0 // 记录邻接矩阵的大小
        val specs = mutableListOf<String>() // 记录所有规格
        val specTypeCount = mutableListOf<Int>() // 记录每个类别的数目
        specList.forEach {
            specs.addAll(it.list)
            specTypeCount.add(it.list.size)
            num += it.list.size
        }
        val combinations = getCombinations(specCombinationList)

        // 二维数组
        val matrix = Array(num) { IntArray(num) }

        // 遍历(n*n-n)/2次
        for (row in 0 until num) {
            val rowTypeIndex = getTypeIndex(row, specTypeCount)
            if (!combinations[rowTypeIndex].contains(specs[row])) {
                continue
            }
            for (column in 0 until row) {
                val columnTypeIndex = getTypeIndex(column, specTypeCount)
                if (!combinations[columnTypeIndex].contains(specs[column])) {
                    continue
                }
                val isEqualsType = rowTypeIndex == columnTypeIndex
                val matchIdList = if (!isEqualsType) {
                    getMatchIdList(rowTypeIndex, specs[row], specCombinationList)
                } else emptyList()

                if (isEqualsType) {
                    // 同一类别规格不同就为1(横向为"红",纵向只要specCombinationList包含紫色就为1)
                    if (specs[row] != specs[column]) {
                        matrix[row][column] = 1
                        matrix[column][row] = 1 // 对称
                    }
                } else {
                    // 不同类别规格相同就为1
                    val newCombinations = getCombinations(specCombinationList, matchIdList)
                    if (newCombinations[columnTypeIndex].contains(specs[column])) {
                        matrix[row][column] = 1
                        matrix[column][row] = 1 // 对称
                    }
                }
            }
        }
        return matrix
    }

    /**
     * 默认可选择
     */
    fun allOr(matrix: Array<IntArray>): IntArray {
        val m = IntArray(matrix.size) { 0 }
        for (row in matrix.indices) {
            for (column in matrix.indices) {
                m[column] = m[column].or(matrix[row][column])
            }
        }
        return m
    }

    fun rowAnd(rows: ArrayList<Int>, matrix: Array<IntArray>): IntArray {
        val m = allOr(matrix)
        for (row in rows) {
            for (column in matrix.indices) {
                m[column] = m[column].and(matrix[row][column])
            }
        }
        return m
    }

    /**
     * 可选的规格
     */
    fun getCanSelectSpec(m: IntArray, specList: List<Spec>): MutableList<IntArray> {
        val canSelectSpecs = mutableListOf<IntArray>()
        var start = 0
        specList.forEach {
            val end = start + it.list.size
            canSelectSpecs.add(m.sliceArray(start until end))
            start = end
        }
        return canSelectSpecs
    }
}

class SpecAdapter(private val specList: List<Spec>, private val specCombinationList: List<SpecCombination>, private val matrix: Array<IntArray>) :
    RecyclerView.Adapter<SpecAdapter.ViewHolder>() {

    private var canSelectSpec = SpecSkuUtil.getCanSelectSpec(SpecSkuUtil.allOr(matrix), specList)
    private val alreadySelectSpec = Array(specList.size) { "" }

    interface OnClickSpecListener {
        fun clickSpec(enabled: Boolean, id: String? = null)
    }

    private var onClickSpecListener: OnClickSpecListener? = null

    fun setOnClickSpecListener(listener: OnClickSpecListener) {
        this.onClickSpecListener = listener
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val typeTv: TextView = itemView.findViewById(R.id.tv_type)
        val specLayout: LinearLayout = itemView.findViewById(R.id.ll_spec)
    }

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

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val spec = specList[position]
        holder.typeTv.text = spec.title
        holder.specLayout.removeAllViews()
        spec.list.forEachIndexed { index, text ->
            val specTv = LayoutInflater.from(holder.itemView.context).inflate(R.layout.item_spec_child, holder.specLayout, false) as TextView
            specTv.text = text

            if (alreadySelectSpec[position] == text || canSelectSpec[position][index] == 1) {
                if (alreadySelectSpec[position] == text) {
                    specTv.isSelected = true
                }
                specTv.isEnabled = true
                specTv.setOnClickListener {
                    if (specTv.isSelected) {
                        specTv.isSelected = false
                        updateCanSelectList(position, "")
                    } else {
                        for (i in 0 until holder.specLayout.childCount) {
                            val childAt = holder.specLayout.getChildAt(i) as TextView
                            if (childAt.isSelected) {
                                childAt.isSelected = false
                            }
                        }
                        specTv.isSelected = true
                        updateCanSelectList(position, text)
                    }
                }
            } else {
                specTv.isSelected = false
                specTv.isEnabled = false
                specTv.setOnClickListener(null)
            }

            holder.specLayout.addView(specTv)
        }
    }

    private fun updateCanSelectList(position: Int, needAddSpec: String) {
        val rows = arrayListOf<Int>()
        alreadySelectSpec[position] = needAddSpec

        if (alreadySelectSpec.contains("")) {
            onClickSpecListener?.clickSpec(false)
        } else {
            for (combination in specCombinationList) {
                if (combination.specs.sorted() == alreadySelectSpec.sorted()) {
                    onClickSpecListener?.clickSpec(true, combination.id)
                    break
                }
            }
        }

        alreadySelectSpec.forEachIndexed { index, it ->
            if (it.isNotEmpty()) {
                var total = 0
                specList.forEachIndexed { i, spec ->
                    if (i == index) {
                        rows.add(total + spec.list.indexOf(it))
                    }
                    total += spec.list.size
                }
            }
        }

        canSelectSpec = SpecSkuUtil.getCanSelectSpec(SpecSkuUtil.rowAnd(rows, matrix), specList)
        notifyDataSetChanged()
    }

    override fun getItemCount() = specList.size
}
复制代码

item_spec.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_type"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/text_66" />

    <LinearLayout
        android:id="@+id/ll_spec"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" />

</LinearLayout>
复制代码

item_spec_child.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="4dp"
    android:textSize="12sp"
    android:background="@drawable/selector_spec_bg"
    android:padding="4dp"
    android:textColor="@color/text_color_spec" />
复制代码

背景selector+文字selector

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_enabled="true" android:state_selected="true">
        <shape>
            <stroke android:width="1px" android:color="@color/text_stock_red" />
            <corners android:radius="2dp" />
            <solid android:color="@color/white" />
        </shape>
    </item>
    <item android:drawable="@color/bg_da" android:state_enabled="true" android:state_selected="false" />
    <item android:drawable="@color/bg_da" android:state_enabled="false" />
</selector>

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/text_stock_red" android:state_enabled="true" android:state_selected="true" />
    <item android:color="@color/text_99" android:state_enabled="false" />
    <item android:color="@color/text_33" android:state_enabled="true" android:state_selected="false" />
</selector>
复制代码

一些问题

  • 没有校验可供选择的规格如果错误情况下
  • 因为采用参考文章的数据结构,规格都为string,所以代码中做了规格分类处理。我也没接触过这种业务开发,所以我想正常情况下每个规格会有对应的id,例如{"id":"123","name":"紫色"},所以可以删减很多规格分类处理的代码
  • 更多Android进阶知识,扫码即可领取

 


 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值