feat(fragment): 食材采集页改用 ChipGroup 选择净材并新增列表空布局
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user