feat(fragment): 食材采集页改用 ChipGroup 选择净材并新增列表空布局

This commit is contained in:
2026-05-11 10:20:54 +08:00
parent dc947df6e3
commit 879af8426f
2 changed files with 176 additions and 80 deletions
@@ -3,6 +3,7 @@ package com.shuwei.dish.match.ui.fragment
import android.annotation.SuppressLint
import android.content.Intent
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.net.Uri
import android.util.Log
@@ -11,14 +12,16 @@ import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.view.Gravity
import android.widget.RadioButton
import android.widget.RadioGroup
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.core.view.doOnLayout
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.chad.library.adapter4.util.setOnDebouncedItemClick
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -28,6 +31,7 @@ import com.shuwei.dish.match.adapter.Food3Adapter
import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.databinding.FragmentVectorCollectionBinding
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.Loading
import com.shuwei.dish.match.entity.FoodCollectionBean
@@ -50,7 +54,7 @@ import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.toast
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import androidx.core.content.withStyledAttributes
import com.shuwei.dish.match.utils.ext.toJsonString
@SuppressLint("NotifyDataSetChanged")
class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>() {
@@ -73,7 +77,9 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
private var checkedItem: GoodsItem? = null
private val searchAdapter by lazy {
Food3Adapter(searchList).apply {
setOnItemClickListener { _, _, position ->
// 开启空状态视图能力,否则 stateView 赋值不会生效
isStateViewEnable = true
setOnDebouncedItemClick { _, _, position ->
val item = searchList[position]
val types = item.rawMaterialsTypes
// 弹出净材种类单选弹窗
@@ -285,8 +291,8 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
val params = mutableMapOf<String, RequestBody>()
params["goodsId"] = checkedItem!!.goodsId.toRequestBody()
params["goodsName"] = checkedItem!!.goodsName!!.toRequestBody()
//净材种类,逗号分隔
// params["rawMaterialsTypes"] = "".toRequestBody()
//净材种类
params["rawMaterialsTypes"] = checkedItem!!.rawMaterialsType!!.toRequestBody()
//params["version"] = "1.0.0".toRequestBody()
ImageUploader(totalList = vectorList, uploadImage = { batch ->
@@ -302,6 +308,7 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
params["foodVector"] = foodVectorJson.toRequestBody()
// 直接 await 单批上传结果,成功返回 id 列表,失败返回 null 触发 onError
val result = currentActivity.netViewModel.uploadFoodVectorDataBatch(files, params)
log("uploadImage返回:result=${result.toJsonString()}")
if (result is UiState.Success) result.data else null
}, onProgress = { count, batch, idList ->
showWaitingDialog("图片上传中$count/$totalFileCount")
@@ -342,41 +349,13 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
currentActivity.netViewModel.goodsListState.collect { state ->
when (state) {
is UiState.Success -> {
var list: List<GoodsItem> = state.data ?: emptyList()
// TODO: 接口暂无数据时使用假数据进行效果测试,联调完成后删除
val list: List<GoodsItem> = state.data ?: emptyList()
if (list.isEmpty()) {
list = mutableListOf(
GoodsItem(goodsId = "1", goodsName = "土豆", rawMaterialsType = "土豆丝,土豆条,土豆片,土豆丁"),
GoodsItem(goodsId = "2", goodsName = "胡萝卜", rawMaterialsType = "胡萝卜丝,胡萝卜片,胡萝卜丁"),
GoodsItem(goodsId = "3", goodsName = "黄瓜", rawMaterialsType = "黄瓜丝,黄瓜片,黄瓜丁,黄瓜块"),
GoodsItem(goodsId = "4", goodsName = "洋葱", rawMaterialsType = "洋葱丝,洋葱丁,洋葱圈"),
GoodsItem(goodsId = "5", goodsName = "白菜", rawMaterialsType = "白菜丝,白菜块,白菜叶"),
GoodsItem(goodsId = "6", goodsName = "豆腐", rawMaterialsType = "豆腐块,豆腐丁"),
GoodsItem(goodsId = "7", goodsName = "猪肉", rawMaterialsType = "猪肉丝,猪肉片,猪肉丁,猪肉块"),
GoodsItem(goodsId = "8", goodsName = "鸡胸肉", rawMaterialsType = "鸡胸肉丝,鸡胸肉片,鸡胸肉丁"),
GoodsItem(goodsId = "9", goodsName = "牛肉", rawMaterialsType = "牛肉丝,牛肉片,牛肉块"),
GoodsItem(goodsId = "10", goodsName = "虾仁", rawMaterialsType = "整虾仁,切段虾仁"),
GoodsItem(goodsId = "11", goodsName = "茄子", rawMaterialsType = "茄子丝,茄子片,茄子块,茄子条"),
GoodsItem(goodsId = "12", goodsName = "青椒", rawMaterialsType = "青椒丝,青椒片,青椒块"),
GoodsItem(goodsId = "13", goodsName = "西红柿", rawMaterialsType = "西红柿片,西红柿块,西红柿丁"),
GoodsItem(goodsId = "14", goodsName = "芹菜", rawMaterialsType = "芹菜段,芹菜丝"),
GoodsItem(goodsId = "15", goodsName = "莲藕", rawMaterialsType = "莲藕片,莲藕丁,莲藕丝"),
// 以下5条 rawMaterialsType 为空,测试无净材数据场景
GoodsItem(goodsId = "16", goodsName = "菠菜"),
GoodsItem(goodsId = "17", goodsName = "生菜"),
GoodsItem(goodsId = "18", goodsName = "香菇"),
GoodsItem(goodsId = "19", goodsName = "木耳"),
GoodsItem(goodsId = "20", goodsName = "豆芽"),
)
// 接口无数据时展示空布局,点击空布局可填充本地假数据用于效果测试
loadEmptyView()
} else {
fillSearchList(list)
}
// 将 rawMaterialsType 转存到 rawMaterialsTypes,并清空 rawMaterialsType
list.forEach { item ->
item.rawMaterialsTypes = item.rawMaterialsType
item.rawMaterialsType = null
}
searchList.clear()
searchList.addAll(list)
searchAdapter.notifyDataSetChanged()
}
else -> Unit
}
@@ -385,6 +364,94 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
}
}
/**
* 将数据列表填充到 searchAdapter 并切换为网格布局
* @param list 查询到的食材列表
*/
@SuppressLint("NotifyDataSetChanged")
private fun fillSearchList(list: List<GoodsItem>) {
// 将 rawMaterialsType 转存到 rawMaterialsTypes,并清空 rawMaterialsType
list.forEach { item ->
item.rawMaterialsTypes = item.rawMaterialsType
item.rawMaterialsType = null
}
// 空布局切过 LinearLayoutManager,此处恢复为 2 列 GridLayoutManager
if (binding.rvSearchFood.layoutManager !is GridLayoutManager) {
binding.rvSearchFood.layoutManager = GridLayoutManager(context, 2)
}
// 清空 stateView 否则空视图会一直占位导致无法显示列表项
searchAdapter.stateView = null
searchList.clear()
searchList.addAll(list)
searchAdapter.notifyDataSetChanged()
}
private var emptyViewBinding: LayoutEmptyViewBinding? = null
/**
* 展示空布局
* 默认 GridLayoutManager 会把 stateView 限制在单列宽度内,切换为 LinearLayoutManager 让空布局横向铺满;
* 点击空布局时临时填充假数据用于效果测试,联调完成后可去除点击逻辑
*/
@SuppressLint("NotifyDataSetChanged")
private fun loadEmptyView() {
try {
searchList.clear()
searchAdapter.notifyDataSetChanged()
// 切换为线性布局让空视图横向铺满,避免 Grid 模式下空布局只占一列
binding.rvSearchFood.layoutManager = LinearLayoutManager(requireContext())
if (emptyViewBinding == null) {
emptyViewBinding = LayoutEmptyViewBinding.inflate(
LayoutInflater.from(requireContext()),
binding.rvSearchFood,
false
)
}
emptyViewBinding?.let {
it.tvContent.text = "暂无数据"
it.tvSubContent.text = "点击此处重新查询"
// 点击空布局填充本地假数据,便于 UI 效果测试
it.root.setOnClickListener {
fillSearchList(buildMockGoodsList())
}
binding.rvSearchFood.post {
it.root.minimumHeight = binding.rvSearchFood.height
searchAdapter.stateView = it.root
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* 构造用于效果测试的食材假数据
* TODO: 联调完成后删除
*/
private fun buildMockGoodsList(): List<GoodsItem> = mutableListOf(
GoodsItem(goodsId = "1", goodsName = "土豆", rawMaterialsType = "土豆丝,土豆条,土豆片,土豆丁"),
GoodsItem(goodsId = "2", goodsName = "胡萝卜", rawMaterialsType = "胡萝卜丝,胡萝卜片,胡萝卜丁"),
GoodsItem(goodsId = "3", goodsName = "黄瓜", rawMaterialsType = "黄瓜丝,黄瓜片,黄瓜丁,黄瓜块"),
GoodsItem(goodsId = "4", goodsName = "洋葱", rawMaterialsType = "洋葱丝,洋葱丁,洋葱圈"),
GoodsItem(goodsId = "5", goodsName = "白菜", rawMaterialsType = "白菜丝,白菜块,白菜叶"),
GoodsItem(goodsId = "6", goodsName = "豆腐", rawMaterialsType = "豆腐块,豆腐丁"),
GoodsItem(goodsId = "7", goodsName = "猪肉", rawMaterialsType = "猪肉丝,猪肉片,猪肉丁,猪肉块"),
GoodsItem(goodsId = "8", goodsName = "鸡胸肉", rawMaterialsType = "鸡胸肉丝,鸡胸肉片,鸡胸肉丁"),
GoodsItem(goodsId = "9", goodsName = "牛肉", rawMaterialsType = "牛肉丝,牛肉片,牛肉块"),
GoodsItem(goodsId = "10", goodsName = "虾仁", rawMaterialsType = "整虾仁,切段虾仁"),
GoodsItem(goodsId = "11", goodsName = "茄子", rawMaterialsType = "茄子丝,茄子片,茄子块,茄子条"),
GoodsItem(goodsId = "12", goodsName = "青椒", rawMaterialsType = "青椒丝,青椒片,青椒块"),
GoodsItem(goodsId = "13", goodsName = "西红柿", rawMaterialsType = "西红柿片,西红柿块,西红柿丁"),
GoodsItem(goodsId = "14", goodsName = "芹菜", rawMaterialsType = "芹菜段,芹菜丝"),
GoodsItem(goodsId = "15", goodsName = "莲藕", rawMaterialsType = "莲藕片,莲藕丁,莲藕丝"),
// 以下5条 rawMaterialsType 为空,测试无净材数据场景
GoodsItem(goodsId = "16", goodsName = "菠菜"),
GoodsItem(goodsId = "17", goodsName = "生菜"),
GoodsItem(goodsId = "18", goodsName = "香菇"),
GoodsItem(goodsId = "19", goodsName = "木耳"),
GoodsItem(goodsId = "20", goodsName = "豆芽"),
)
/**
* 弹出净材种类单选弹窗
* @param item 当前点击的食材项
@@ -404,45 +471,14 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
dialog.setContent("无净材数据")
.setPositiveButton("确认") {}
} else {
// 构建单选 RadioGroup 作为自定义内容
val radioGroup = RadioGroup(requireContext()).apply {
orientation = RadioGroup.VERTICAL
// RadioGroup 本身居中,子项 wrap_content 宽度,整体图标+文字自然居中
gravity = Gravity.CENTER_HORIZONTAL
typeList.forEachIndexed { index, typeName ->
val rb = RadioButton(requireContext()).apply {
id = index
text = typeName
textSize = 26f
layoutParams = RadioGroup.LayoutParams(
RadioGroup.LayoutParams.WRAP_CONTENT,
RadioGroup.LayoutParams.WRAP_CONTENT
)
setPadding(30.dp, 15.dp, 30.dp, 15.dp)
// 移除默认点击水波纹背景,改用 foreground 实现 selectableItemBackground 效果
setBackgroundResource(0)
val attrs = intArrayOf(android.R.attr.selectableItemBackground)
context.withStyledAttributes(null, attrs) {
foreground = getDrawable(0)
}
// 若当前项已有选中值则默认勾选对应选项
if (typeName == item.rawMaterialsType) isChecked = true
// 通过 InsetDrawable 在 buttonDrawable 右侧追加 80dp 间距,实现图标与文字的间距控制
buttonDrawable?.let { orig ->
buttonDrawable = android.graphics.drawable.InsetDrawable(
orig, 30.dp, 0, 30.dp, 0
)
}
}
addView(rb)
}
}
dialog.addContentView(radioGroup) { tvContent ->
// 构建单选 ChipGroup 作为自定义内容
val chipGroup = buildSingleChoiceChipGroup(typeList, item.rawMaterialsType)
dialog.addContentView(chipGroup) { tvContent ->
tvContent.gone()
}
dialog.setPositiveButtonInterceptable("确认") {
val checkedId = radioGroup.checkedRadioButtonId
if (checkedId == -1) {
val checkedId = chipGroup.checkedChipId
if (checkedId == View.NO_ID) {
toast("请选择净材种类")
// 返回 false 阻止弹窗关闭
return@setPositiveButtonInterceptable false
@@ -469,6 +505,67 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
dialog.show()
}
/**
* 构建单选样式的 ChipGroup
* @param typeList 选项文本列表,列表中每一项作为一个 Chip 的文字
* @param checkedType 默认选中项的文本,与 typeList 中元素相等的 Chip 会被勾选;为空则无默认选中
* @return 配置完成的 ChipGroup,每个 Chip 的 id 即为其在 typeList 中的下标
*/
private fun buildSingleChoiceChipGroup(
typeList: List<String>,
checkedType: String?
): ChipGroup {
return ChipGroup(requireContext()).apply {
isSingleSelection = true
isSelectionRequired = false
setPadding(5.dp, 40.dp, 5.dp, 40.dp)
chipSpacingVertical = 10.dp
chipSpacingHorizontal = 20.dp
typeList.forEachIndexed { index, typeName ->
val chip = Chip(requireContext()).apply {
id = index
text = typeName
textSize = 22f
isCheckable = true
chipStartPadding = 20.dp.toFloat()
chipEndPadding = 20.dp.toFloat()
chipMinHeight = (textSize + 15.dp * 2).toFloat()
// 选中/未选中态背景色:主题绿 / 浅灰
chipBackgroundColor = ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)
),
intArrayOf(
ContextCompat.getColor(requireContext(), R.color.dish_green),
ContextCompat.getColor(requireContext(), R.color.gray_eb)
)
)
// 选中/未选中态文字色:白 / 深灰
setTextColor(
ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)
),
intArrayOf(
ContextCompat.getColor(requireContext(), R.color.white),
ContextCompat.getColor(requireContext(), R.color.black333)
)
)
)
// 隐藏默认的左侧勾选图标,让填充色作为唯一选中反馈
isCheckedIconVisible = false
// 去除描边
chipStrokeWidth = 0f
// 若当前项已有选中值则默认勾选对应选项
isChecked = (typeName == checkedType)
}
addView(chip)
}
}
}
var clickIndex = -1
@SuppressLint("NotifyDataSetChanged")
+3 -4
View File
@@ -4,10 +4,7 @@
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginHorizontal="14dp"
android:layout_marginVertical="14dp"
android:background="@drawable/bg_item_search"
android:paddingHorizontal="15dp"
android:paddingVertical="10dp">
android:layout_marginVertical="14dp">
<!-- 食材名称,选中状态通过 CheckedTextView 的 checked 驱动背景/文字色变化 -->
<CheckedTextView
@@ -21,6 +18,7 @@
android:textColor="@color/color_item_search"
android:textSize="28sp"
android:textStyle="bold"
android:background="@drawable/bg_item_search"
tools:text="土豆丝" />
<!-- 当前选中的净材种类,小字显示,无数据时隐藏 -->
@@ -30,6 +28,7 @@
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center|bottom"
android:layout_marginBottom="3dp"
android:textColor="@color/black999"
android:textSize="18sp"
android:visibility="gone"