introduce
E-commerce related apps select commodities on the commodity purchase page and select corresponding commodities according to different combinations of specifications
Results the preview
solution
Use the adjacency matrix
Suppose we have the following specification list:
specList: [
{ title: "Color", list: ["Purple"."Red"] },
{ title: "Package", list: ["Set number one"."Package two"] },
{ title: "Memory", list: ["64G"."128G"."256G"] }
]
specList: [
{ title: "Head", list: ["Purple"."Red"] },
{ title: "Body", list: ["Black"."Green"."Purple"] },
{ title: "Legs", list: ["Purple"."Pink"] },
{ title: "Shoes", list: ["Back"."Leap forward"]}]Copy the code
Available specification combinations:
specCombinationList: [
{ id: "1", specs: ["Purple"."Set number one"."64G"] },
{ id: "2", specs: ["Purple"."Set number one"."128G"] },
{ id: "3", specs: ["Purple"."Package two"."128G"] },
{ id: "4", specs: ["Red"."Package two"."256G"] }
]
specCombinationList: [
{ id: "1", specs: ["Purple"."Black"."Purple"."Back"] },
{ id: "2", specs: ["Purple"."Purple"."Purple"."Leap forward"] },
{ id: "3", specs: ["Red"."Black"."Pink"."Leap forward"]}]Copy the code
Then, according to the above data, we can get the following adjacency matrix:
So how do you use this matrix to correspond to optional product specifications? When we select the “purple” specification:
When we next select the “Package 1” specification:
A careful observation of the adjacency matrix shows that:
- 0 from top left vertex to bottom right vertex - the data on both sides of the two vertices are symmetricCopy the code
So to reduce traversal, we only need to traverse the following range:
implementation
- Creates a two-dimensional array of adjacency matrices
var num = 0 // Record the size of the adjacency matrix
specList.forEach {
num += it.list.size
}
// A two-dimensional array
val matrix = Array(num) { IntArray(num) }
Copy the code
- Only the triangle red box is traversed
for (row in 0 until num) {
for (column in 0 until row) {}
}
Copy the code
- Record the number of each category (color: 2, Package: 2, Memory: 3)
SpecTypeCount = mutableListOf<Int>() // Record the number of categories spectypecount.foreach {spectypecount.add (it.list.size)}Copy the code
- Obtain the category index of the row/column based on the index of row/column, for example, 0-> purple ->1,5->128g->3
/** * get type 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
}
Copy the code
- According to the optional combination, splice out optional specifications
[} {purple, red, 2} {package a, combo, {64 g, 128 g, 256 g}] [} {purple, red,} {black, purple, {warrior, leap}]Copy the code
private fun getCombinations(specCombinationList: List<SpecCombination>): Array<MutableList<String>> {
val size = if (specCombinationList.isNullOrEmpty()) 0 else specCombinationList.first().specs.size
valcs = Array<MutableList<String>>(size) { mutableListOf() } specCombinationList.forEach { it.specs.forEachIndexed { index, spec -> cs[index].add(spec) } }return cs
}
Copy the code
- If the row specification traversed does not have an optional specification, the row is skipped to reduce traversal. For example, if the green specifications are not available, skip this step
val rowTypeIndex = getTypeIndex(row, specTypeCount) if (! Column [rowTypeIndex]. Contains (specs[row]) {continue} // Column column = column column specTypeCount) if (! combinations[columnTypeIndex].contains(specs[column])) { continue }Copy the code
- If the row and column belong to the same category and the two specifications of the same level are different, select 1.
if (isEqualsType) {
// If the same category is different, it is 1 (horizontal is "red", vertical is 1 if the specCombinationList contains purple)
if(specs[row] ! = specs[column]) { matrix[row][column] =1
matrix[column][row] = 1 / / symmetric}}Copy the code
- Rows and columns that do not belong to the same category specifications need to be combined with contain ROW specifications and matched within the optional specifications. If the match is successful, the value can be 1. For example, if the current row specification is leap, the conforming ID is
{id: "2", specs: [" purple ", "purple", "purple", "leap"]}, {id: "3", specs: [" red ", "black", "pink", "leap"]}Copy the code
The head category is purple and red, the body category is purple and black, the legs category is purple and pink. So the corresponding matrix is:
else {
// If different categories have the same specifications, the value is 1
val newCombinations = getCombinations(specCombinationList, matchIdList)
if (newCombinations[columnTypeIndex].contains(specs[column])) {
matrix[row][column] = 1
matrix[column][row] = 1 / / symmetric}}Copy the code
- Gets the first selectable specification matrix, and the selected specification intersection
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
}
Copy the code
- UI level just write a layout, actually adjust according to their own situation. I’m using recyclerView + LinearLayout here, which uses TextView inside the linearLayout. Textview has three different states, selectable, selected, and unselectable.
The complete code
International practice: direct CV can see the effect
class SkuActivity : AppCompatActivity() {
private val mBinding by lazy { ActivitySkuBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
setContentView(mBinding.root)
// List of specifications
val specList = listOf(
Spec("Color", listOf("Purple"."Red")),
Spec("Package", listOf("Set number one"."Package two")),
Spec("Memory", listOf("64g"."128g"."256g")))// Optional specification combinations
val specCombinationList = listOf(
SpecCombination("1", listOf("Purple"."Set number one"."64g")),
SpecCombination("2", listOf("Purple"."Set number one"."128g")),
SpecCombination("3", listOf("Purple"."Package two"."128g")),
SpecCombination("4", listOf("Red"."Package two"."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 = "specifications$id", Toast.LENGTH_SHORT).show()
}
})
mBinding.rvSpec.adapter = specAdapter
val specList2 = listOf(
Spec("Head", listOf("Purple"."Red")),
Spec("Body", listOf("Black"."Green"."Purple")),
Spec("Legs", listOf("Purple"."Pink")),
Spec("Shoes", listOf("Back"."Leap forward")))val specCombinationList2 = listOf(
SpecCombination("1", listOf("Purple"."Black"."Purple"."Back")),
SpecCombination("2", listOf("Purple"."Purple"."Purple"."Leap forward")),
SpecCombination("3", listOf("Red"."Black"."Pink"."Leap forward")))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 = "specifications$id", Toast.LENGTH_SHORT).show()
}
})
mBinding.rvSpec2.adapter = specAdapter2
}
}
object SpecSkuUtil {
/** * get type 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
}
/ * * * * has selected a certain specifications to choose from after the rules to select "purple", return (" 1 ", "2", "3"] * selected "plan a", return (" 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 // Record the size of the adjacency matrix
val specs = mutableListOf<String>() // Record all specifications
val specTypeCount = mutableListOf<Int> ()// Record the number of each category
specList.forEach {
specs.addAll(it.list)
specTypeCount.add(it.list.size)
num += it.list.size
}
val combinations = getCombinations(specCombinationList)
// A two-dimensional array
val matrix = Array(num) { IntArray(num) }
// iterate over (n*n-n)/2 times
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) {
// If the same category is different, it is 1 (horizontal is "red", vertical is 1 if the specCombinationList contains purple)
if(specs[row] ! = specs[column]) { matrix[row][column] =1
matrix[column][row] = 1 / / symmetric}}else {
// If different categories have the same specifications, the value is 1
val newCombinations = getCombinations(specCombinationList, matchIdList)
if (newCombinations[columnTypeIndex].contains(specs[column])) {
matrix[row][column] = 1
matrix[column][row] = 1 / / symmetric}}}}return matrix
}
/** ** The default value is */
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
}
/** * Optional specifications */
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(a) = specList.size
}
Copy the code
item_spec.xml
<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>
Copy the code
item_spec_child.xml
<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" />
Copy the code
Background selector plus text selector
<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>
<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>
Copy the code
Some of the problems
- No check available specifications if wrong case
- Because the data structure of the reference article is used, the specification is string, so the specification classification is processed in the code. I’m not familiar with this business development either, so I think normally there would be an ID for each specification, for example {” ID “:”123″,”name”:” purple “}, so I can cut out a lot of specification classification code
Reference documents and some pictures
- Learn front-end SKU algorithm in minutes (multi-specification selection of goods)