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.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -11,14 +12,16 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.view.Gravity
|
import com.google.android.material.chip.Chip
|
||||||
import android.widget.RadioButton
|
import com.google.android.material.chip.ChipGroup
|
||||||
import android.widget.RadioGroup
|
|
||||||
import androidx.camera.view.PreviewView
|
import androidx.camera.view.PreviewView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.doOnLayout
|
import androidx.core.view.doOnLayout
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.chad.library.adapter4.util.setOnDebouncedItemClick
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
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.base.BaseFragment
|
||||||
import com.shuwei.dish.match.databinding.FragmentVectorCollectionBinding
|
import com.shuwei.dish.match.databinding.FragmentVectorCollectionBinding
|
||||||
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
|
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.CommonDialog
|
||||||
import com.shuwei.dish.match.dialog.Loading
|
import com.shuwei.dish.match.dialog.Loading
|
||||||
import com.shuwei.dish.match.entity.FoodCollectionBean
|
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 com.shuwei.dish.match.utils.ext.toast
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import androidx.core.content.withStyledAttributes
|
import com.shuwei.dish.match.utils.ext.toJsonString
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>() {
|
class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>() {
|
||||||
@@ -73,7 +77,9 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
|||||||
private var checkedItem: GoodsItem? = null
|
private var checkedItem: GoodsItem? = null
|
||||||
private val searchAdapter by lazy {
|
private val searchAdapter by lazy {
|
||||||
Food3Adapter(searchList).apply {
|
Food3Adapter(searchList).apply {
|
||||||
setOnItemClickListener { _, _, position ->
|
// 开启空状态视图能力,否则 stateView 赋值不会生效
|
||||||
|
isStateViewEnable = true
|
||||||
|
setOnDebouncedItemClick { _, _, position ->
|
||||||
val item = searchList[position]
|
val item = searchList[position]
|
||||||
val types = item.rawMaterialsTypes
|
val types = item.rawMaterialsTypes
|
||||||
// 弹出净材种类单选弹窗
|
// 弹出净材种类单选弹窗
|
||||||
@@ -285,8 +291,8 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
|||||||
val params = mutableMapOf<String, RequestBody>()
|
val params = mutableMapOf<String, RequestBody>()
|
||||||
params["goodsId"] = checkedItem!!.goodsId.toRequestBody()
|
params["goodsId"] = checkedItem!!.goodsId.toRequestBody()
|
||||||
params["goodsName"] = checkedItem!!.goodsName!!.toRequestBody()
|
params["goodsName"] = checkedItem!!.goodsName!!.toRequestBody()
|
||||||
//净材种类,逗号分隔
|
//净材种类
|
||||||
// params["rawMaterialsTypes"] = "".toRequestBody()
|
params["rawMaterialsTypes"] = checkedItem!!.rawMaterialsType!!.toRequestBody()
|
||||||
//params["version"] = "1.0.0".toRequestBody()
|
//params["version"] = "1.0.0".toRequestBody()
|
||||||
|
|
||||||
ImageUploader(totalList = vectorList, uploadImage = { batch ->
|
ImageUploader(totalList = vectorList, uploadImage = { batch ->
|
||||||
@@ -302,6 +308,7 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
|||||||
params["foodVector"] = foodVectorJson.toRequestBody()
|
params["foodVector"] = foodVectorJson.toRequestBody()
|
||||||
// 直接 await 单批上传结果,成功返回 id 列表,失败返回 null 触发 onError
|
// 直接 await 单批上传结果,成功返回 id 列表,失败返回 null 触发 onError
|
||||||
val result = currentActivity.netViewModel.uploadFoodVectorDataBatch(files, params)
|
val result = currentActivity.netViewModel.uploadFoodVectorDataBatch(files, params)
|
||||||
|
log("uploadImage返回:result=${result.toJsonString()}")
|
||||||
if (result is UiState.Success) result.data else null
|
if (result is UiState.Success) result.data else null
|
||||||
}, onProgress = { count, batch, idList ->
|
}, onProgress = { count, batch, idList ->
|
||||||
showWaitingDialog("图片上传中$count/$totalFileCount")
|
showWaitingDialog("图片上传中$count/$totalFileCount")
|
||||||
@@ -342,10 +349,86 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
|||||||
currentActivity.netViewModel.goodsListState.collect { state ->
|
currentActivity.netViewModel.goodsListState.collect { state ->
|
||||||
when (state) {
|
when (state) {
|
||||||
is UiState.Success -> {
|
is UiState.Success -> {
|
||||||
var list: List<GoodsItem> = state.data ?: emptyList()
|
val list: List<GoodsItem> = state.data ?: emptyList()
|
||||||
// TODO: 接口暂无数据时使用假数据进行效果测试,联调完成后删除
|
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
list = mutableListOf(
|
// 接口无数据时展示空布局,点击空布局可填充本地假数据用于效果测试
|
||||||
|
loadEmptyView()
|
||||||
|
} else {
|
||||||
|
fillSearchList(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将数据列表填充到 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 = "1", goodsName = "土豆", rawMaterialsType = "土豆丝,土豆条,土豆片,土豆丁"),
|
||||||
GoodsItem(goodsId = "2", goodsName = "胡萝卜", rawMaterialsType = "胡萝卜丝,胡萝卜片,胡萝卜丁"),
|
GoodsItem(goodsId = "2", goodsName = "胡萝卜", rawMaterialsType = "胡萝卜丝,胡萝卜片,胡萝卜丁"),
|
||||||
GoodsItem(goodsId = "3", goodsName = "黄瓜", rawMaterialsType = "黄瓜丝,黄瓜片,黄瓜丁,黄瓜块"),
|
GoodsItem(goodsId = "3", goodsName = "黄瓜", rawMaterialsType = "黄瓜丝,黄瓜片,黄瓜丁,黄瓜块"),
|
||||||
@@ -368,22 +451,6 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
|||||||
GoodsItem(goodsId = "19", goodsName = "木耳"),
|
GoodsItem(goodsId = "19", goodsName = "木耳"),
|
||||||
GoodsItem(goodsId = "20", goodsName = "豆芽"),
|
GoodsItem(goodsId = "20", goodsName = "豆芽"),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
// 将 rawMaterialsType 转存到 rawMaterialsTypes,并清空 rawMaterialsType
|
|
||||||
list.forEach { item ->
|
|
||||||
item.rawMaterialsTypes = item.rawMaterialsType
|
|
||||||
item.rawMaterialsType = null
|
|
||||||
}
|
|
||||||
searchList.clear()
|
|
||||||
searchList.addAll(list)
|
|
||||||
searchAdapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 弹出净材种类单选弹窗
|
* 弹出净材种类单选弹窗
|
||||||
@@ -404,45 +471,14 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
|||||||
dialog.setContent("无净材数据")
|
dialog.setContent("无净材数据")
|
||||||
.setPositiveButton("确认") {}
|
.setPositiveButton("确认") {}
|
||||||
} else {
|
} else {
|
||||||
// 构建单选 RadioGroup 作为自定义内容
|
// 构建单选 ChipGroup 作为自定义内容
|
||||||
val radioGroup = RadioGroup(requireContext()).apply {
|
val chipGroup = buildSingleChoiceChipGroup(typeList, item.rawMaterialsType)
|
||||||
orientation = RadioGroup.VERTICAL
|
dialog.addContentView(chipGroup) { tvContent ->
|
||||||
// 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 ->
|
|
||||||
tvContent.gone()
|
tvContent.gone()
|
||||||
}
|
}
|
||||||
dialog.setPositiveButtonInterceptable("确认") {
|
dialog.setPositiveButtonInterceptable("确认") {
|
||||||
val checkedId = radioGroup.checkedRadioButtonId
|
val checkedId = chipGroup.checkedChipId
|
||||||
if (checkedId == -1) {
|
if (checkedId == View.NO_ID) {
|
||||||
toast("请选择净材种类")
|
toast("请选择净材种类")
|
||||||
// 返回 false 阻止弹窗关闭
|
// 返回 false 阻止弹窗关闭
|
||||||
return@setPositiveButtonInterceptable false
|
return@setPositiveButtonInterceptable false
|
||||||
@@ -469,6 +505,67 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
|||||||
dialog.show()
|
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
|
var clickIndex = -1
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
|||||||
@@ -4,10 +4,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="100dp"
|
android:layout_height="100dp"
|
||||||
android:layout_marginHorizontal="14dp"
|
android:layout_marginHorizontal="14dp"
|
||||||
android:layout_marginVertical="14dp"
|
android:layout_marginVertical="14dp">
|
||||||
android:background="@drawable/bg_item_search"
|
|
||||||
android:paddingHorizontal="15dp"
|
|
||||||
android:paddingVertical="10dp">
|
|
||||||
|
|
||||||
<!-- 食材名称,选中状态通过 CheckedTextView 的 checked 驱动背景/文字色变化 -->
|
<!-- 食材名称,选中状态通过 CheckedTextView 的 checked 驱动背景/文字色变化 -->
|
||||||
<CheckedTextView
|
<CheckedTextView
|
||||||
@@ -21,6 +18,7 @@
|
|||||||
android:textColor="@color/color_item_search"
|
android:textColor="@color/color_item_search"
|
||||||
android:textSize="28sp"
|
android:textSize="28sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
|
android:background="@drawable/bg_item_search"
|
||||||
tools:text="土豆丝" />
|
tools:text="土豆丝" />
|
||||||
|
|
||||||
<!-- 当前选中的净材种类,小字显示,无数据时隐藏 -->
|
<!-- 当前选中的净材种类,小字显示,无数据时隐藏 -->
|
||||||
@@ -30,6 +28,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_gravity="center|bottom"
|
android:layout_gravity="center|bottom"
|
||||||
|
android:layout_marginBottom="3dp"
|
||||||
android:textColor="@color/black999"
|
android:textColor="@color/black999"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
|||||||
Reference in New Issue
Block a user