diff --git a/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt b/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt index c7a1fde..49b2734 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt @@ -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() { @@ -73,7 +77,9 @@ class VectorCollectionFragment : BaseFragment() 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() val params = mutableMapOf() 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() 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() currentActivity.netViewModel.goodsListState.collect { state -> when (state) { is UiState.Success -> { - var list: List = state.data ?: emptyList() - // TODO: 接口暂无数据时使用假数据进行效果测试,联调完成后删除 + val list: List = 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() } } + /** + * 将数据列表填充到 searchAdapter 并切换为网格布局 + * @param list 查询到的食材列表 + */ + @SuppressLint("NotifyDataSetChanged") + private fun fillSearchList(list: List) { + // 将 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 = 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() 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() dialog.show() } + /** + * 构建单选样式的 ChipGroup + * @param typeList 选项文本列表,列表中每一项作为一个 Chip 的文字 + * @param checkedType 默认选中项的文本,与 typeList 中元素相等的 Chip 会被勾选;为空则无默认选中 + * @return 配置完成的 ChipGroup,每个 Chip 的 id 即为其在 typeList 中的下标 + */ + private fun buildSingleChoiceChipGroup( + typeList: List, + 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") diff --git a/app/src/main/res/layout/list_item_food3.xml b/app/src/main/res/layout/list_item_food3.xml index 3b1a84f..a8868c6 100644 --- a/app/src/main/res/layout/list_item_food3.xml +++ b/app/src/main/res/layout/list_item_food3.xml @@ -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"> @@ -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"