介绍
电商相关app在商品购买页面选择商品,根据不同的规格组合,选择对应的商品
效果预览
解决思路
使用邻接矩阵解决
假设我们有如下规格列表:
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: ["红色", "黑色", "粉色", "飞跃"] }
]
复制代码
那么根据上面的数据,我们可以得到如下的邻接矩阵:
那么怎么使用该矩阵对应可选的商品规格呢?当我们选中“紫色”规格时:
当我们接着选中“套餐一”规格时:
仔细观察该邻接矩阵可以发现:
- 左上顶点到右下底点都为0
- 两个顶点两边的数据是对称的
复制代码
所以为力减少遍历,我们只需要遍历如下的范围:
实现
- 创建邻接矩阵的二维数组
var num = 0 // 记录邻接矩阵的大小
specList.forEach {
num += it.list.size
}
// 二维数组
val matrix = Array(num) { IntArray(num) }
复制代码
- 仅遍历三角红框范围
for (row in 0 until num) {
for (column in 0 until row) {}
}
复制代码
- 记录每个类别的数目(颜色:2,套餐:2,内存:3)
val specTypeCount = mutableListOf<Int>() // 记录每个类别的数目
specList.forEach {
specTypeCount.add(it.list.size)
}
复制代码
- 根据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
}
复制代码
- 根据可供选择组合,拼接出可以选择的规格
[{紫色,红色},{套餐一,套餐二},{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
}
复制代码
- 当遍历的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。
if (isEqualsType) {
// 同一类别规格不同就为1(横向为"红",纵向只要specCombinationList包含紫色就为1)
if (specs[row] != specs[column]) {
matrix[row][column] = 1
matrix[column][row] = 1 // 对称
}
}
复制代码
- 行跟列不属于同一类别规格,则需要结合包含row规格,并且在可选择的规格里面匹配。匹配成功可以选择为1。例如当前row规格是飞跃,符合的id有
{ id: "2", specs: ["紫色", "紫色", "紫色", "飞跃"] },
{ id: "3", specs: ["红色", "黑色", "粉色", "飞跃"] }
复制代码
头部类别有紫色、红色,身体类别有紫色、黑色,腿部类别有紫色、粉色。所以对应的矩阵为:
else {
// 不同类别规格相同就为1
val newCombinations = getCombinations(specCombinationList, matchIdList)
if (newCombinations[columnTypeIndex].contains(specs[column])) {
matrix[row][column] = 1
matrix[column][row] = 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
}
复制代码
- 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进阶知识,扫码即可领取