feat(activity): 新增食材识别页面,重构识别流程并优化多设备布局适配

This commit is contained in:
2026-04-16 18:05:46 +08:00
parent ad63639f66
commit 7373ccacbb
13 changed files with 413 additions and 48 deletions
+5
View File
@@ -111,6 +111,11 @@
android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity
android:name="com.shuwei.dish.match.ui.FoodRecognizeActivity"
android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity
android:name="com.shuwei.dish.match.ui.SlaveActivity"
android:theme="@style/Theme.DishMatch.NoSplash"
@@ -66,7 +66,9 @@ class Scale18GridAdapter : BaseQuickAdapter<ScaleData, Scale18GridAdapter.VH>(mu
b.root.layoutParams = (b.root.layoutParams as ViewGroup.MarginLayoutParams).also {
it.width = cellSize
it.height = cellSize
it.setMargins(margin, margin, margin, margin)
// 第1行:顶部间距为0,底部间距2dp,左右保持原有 margin
if (viewType == 0) it.setMargins(margin, 0, margin, 2.dp)
else it.setMargins(margin, margin, margin, margin)
}
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
b.tvAddr.textSize = (baseSp * 0.16f).coerceIn(9f, 14f)
@@ -26,6 +26,7 @@ import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.net.NetViewModel
import com.shuwei.dish.match.ui.InitActivity
import com.shuwei.dish.match.utils.ActivityManager
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.invisible
import com.shuwei.dish.match.utils.ext.visible
@@ -65,6 +66,10 @@ open class BaseActivity : AppCompatActivity() {
// systemBarsBehavior =
// WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE // 滑动时临时显示
// }
// 主设备垂直 padding 30dp,子设备 15dp
val verticalPadding = if (GlobalData.deviceRole == DeviceRole.MASTER) 30.dp else 10.dp
binding.tvLeftTime.setPadding(0, verticalPadding, 0, verticalPadding)
binding.tvRightTime.setPadding(0, verticalPadding, 0, verticalPadding)
binding.llTitleBar.gone()
binding.ivBack.setOnClickListener { finish() }
}
@@ -7,7 +7,11 @@ import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.View
import android.view.Window
import android.widget.LinearLayout
import androidx.annotation.StyleRes
import androidx.core.view.updateLayoutParams
import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.DialogCommonBinding
import com.shuwei.dish.match.utils.ext.dp
import java.lang.ref.WeakReference
@@ -55,12 +59,17 @@ open class CommonDialog(
binding = DialogCommonBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.run {
val screenWidth = context.resources.displayMetrics.widthPixels
val isMaster = GlobalData.deviceRole == DeviceRole.MASTER
attributes = attributes.apply {
width = 660.dp
width = if (isMaster) (screenWidth * 0.72f).toInt() else (screenWidth * 0.60f).toInt()
}
setBackgroundDrawable(ColorDrawable())
setCancelable(false)
}
val verticalMargin = if (GlobalData.deviceRole == DeviceRole.MASTER) 35.dp else 20.dp
binding.tvTitle.updateLayoutParams<LinearLayout.LayoutParams> { topMargin = verticalMargin }
binding.tvContent.updateLayoutParams<LinearLayout.LayoutParams> { bottomMargin = verticalMargin }
binding.btnLeft.setOnClickListener {
negativeClick?.invoke()
dismiss()
@@ -66,10 +66,10 @@ class LoadingDialog(
setCancelable(true)
window?.run {
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
);
// setFlags(
// WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
// WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
// );
}
setOnDismissListener {
onDismiss()
@@ -0,0 +1,213 @@
package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.recyclerview.widget.GridLayoutManager
import com.shuwei.dish.match.adapter.GoodsInfoSearchAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityFoodRecognizeBinding
import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.utils.ActivityManager
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.visible
/**
* 食材识别结果页面
* 展示识别到的食材照片、食材列表、选中食材名称及秤1重量
*/
class FoodRecognizeActivity : BaseActivity() {
companion object {
private const val TAG = "FoodRecognizeActivity"
private const val EXTRA_IMAGE_URI = "extra_image_uri"
const val EXTRA_SELECTED_ITEM = "extra_selected_item"
/** 内存缓存食材列表,避免通过 Intent 序列化传递大数据 */
private var pendingGoodsList: ArrayList<CookFoodGoodsEntity>? = null
/**
* 启动本页面,通过 onResult 回调返回用户选中的食材
* @param activity 宿主 Activity
* @param imageUri 食材照片路径(可为空)
* @param goodsList 识别到的食材列表
* @param onResult 页面关闭后回调,携带用户选中的食材(未选中时为 null)
*/
fun start(
activity: BaseActivity,
imageUri: String? = null,
goodsList: ArrayList<CookFoodGoodsEntity> = arrayListOf(),
onResult: (CookFoodGoodsEntity?) -> Unit = {}
) {
pendingGoodsList = goodsList
val intent = Intent(activity, FoodRecognizeActivity::class.java).apply {
putExtra(EXTRA_IMAGE_URI, imageUri)
}
activity.startActivity(intent) { resultIntent ->
@Suppress("DEPRECATION")
val item = resultIntent?.getSerializableExtra(EXTRA_SELECTED_ITEM) as? CookFoodGoodsEntity
onResult(item)
}
}
/**
* 关闭当前识别页面,并将选中食材通过 Activity Result 回传
* 由外部(如重量归零时)调用
*/
fun close() {
ActivityManager.getActivityStack()
.filterIsInstance<FoodRecognizeActivity>()
.lastOrNull()
?.finishWithResult()
}
}
private lateinit var binding: ActivityFoodRecognizeBinding
private val list = mutableListOf<CookFoodGoodsEntity>()
private val adapter = GoodsInfoSearchAdapter(list).apply {
setOnItemClickListener { _, _, position ->
// 更新选中状态
list.forEachIndexed { i, item -> item.isClicked = (i == position) }
notifyDataSetChanged()
// 同步更新选中食材名称
binding.tvSelectedFood.text = list[position].goodsName ?: "-"
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityFoodRecognizeBinding.inflate(layoutInflater)
setContentView(binding.root)
setHeaderBackground()
// 显示标题栏
setTitleBar(titleBarAction = { it.visible() }, titleAction = { it.text = "食材识别" })
initData()
initRecyclerView()
initClickListeners()
registerWeightListener()
}
/**
* 从 Intent 读取数据并初始化页面
*/
private fun initData() {
// 加载食材照片
val imageUri = intent.getStringExtra(EXTRA_IMAGE_URI)
if (!imageUri.isNullOrBlank()) {
binding.ivFoodPhoto.setImageURI(Uri.parse(imageUri))
}
// 取出内存缓存的食材列表,取用后立即清空
val goodsList = pendingGoodsList?.also { pendingGoodsList = null }
if (!goodsList.isNullOrEmpty()) {
list.addAll(goodsList)
// 默认选中第一项
list[0].isClicked = true
binding.tvSelectedFood.text = list[0].goodsName ?: "-"
}
}
/**
* 初始化 RecyclerView
*/
private fun initRecyclerView() {
binding.recyclerView.run {
layoutManager = GridLayoutManager(this@FoodRecognizeActivity, 2)
adapter = this@FoodRecognizeActivity.adapter
}
updateListVisibility()
}
/**
* 根据列表是否有数据控制 RecyclerView 显隐
*/
private fun updateListVisibility() {
if (list.isEmpty()) {
binding.recyclerView.gone()
} else {
binding.recyclerView.visible()
}
}
/**
* 初始化按钮点击事件
*/
private fun initClickListeners() {
// 更换食材:打开食材搜索弹窗
binding.btnChangeFood.clickWithDebounce {
val currentName = if (list.isNotEmpty()) list.firstOrNull { it.isClicked }?.goodsName else null
FoodSearchDialog(
activity = this,
defGoodsName = currentName,
onItemSelected = { item ->
// 用搜索结果替换当前选中食材
updateSelectedFood(item)
}
).show()
}
// 取消:关闭页面
binding.btnCancel.setOnClickListener { finish() }
}
/**
* 用新食材替换当前选中项(或追加到列表首位)
*/
@SuppressLint("NotifyDataSetChanged")
private fun updateSelectedFood(item: CookFoodGoodsEntity) {
item.isClicked = true
val clickedIndex = list.indexOfFirst { it.isClicked && it.goodsId != item.goodsId }
if (clickedIndex >= 0) {
list[clickedIndex].isClicked = false
}
// 若列表中已存在则直接选中,否则插入到首位
val existIndex = list.indexOfFirst { it.goodsId == item.goodsId }
if (existIndex >= 0) {
list.forEachIndexed { i, e -> e.isClicked = (i == existIndex) }
} else {
list.add(0, item)
}
adapter.notifyDataSetChanged()
binding.tvSelectedFood.text = item.goodsName ?: "-"
updateListVisibility()
}
/**
* 注册秤1重量监听
*/
private fun registerWeightListener() {
WeightUtil.addWeightListener(TAG) { address, _, weight ->
if (address == AddressUtil.ONE) {
runOnUiThread {
binding.tvWeight.text = "$weight g"
}
}
}
}
/**
* 携带当前选中食材关闭页面
*/
private fun finishWithResult() {
val selected = list.firstOrNull { it.isClicked }
val intent = Intent().apply {
selected?.let { putExtra(EXTRA_SELECTED_ITEM, it) }
}
setResult(RESULT_OK, intent)
finish()
}
override fun onDestroy() {
super.onDestroy()
WeightUtil.removeWeightListener(TAG)
}
}
@@ -12,7 +12,6 @@ import com.shuwei.dish.match.adapter.DishPartAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityPrepareCookBinding
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.FoodRecognizeDialog
import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
@@ -158,6 +157,10 @@ class PrepareCookActivity : BaseActivity() {
}
private val isTakingPhoto = AtomicBoolean(false)
private var isRecognizeOpen = false
// 页面可见时允许识别,onPause 后禁止,onResume 后恢复
private var isRecognizeEnabled = true
private var lastPhotoUri: Uri? = null
private var lastWeight = 0.0
private var currentWeight = 0.0
@@ -168,10 +171,11 @@ class PrepareCookActivity : BaseActivity() {
private fun recognizeFood(weight: Double) {
this.currentWeight = weight
if (weight < WEIGHT_RECOGNIZE_VALUE) {
if (recognizeDialog.isShowing) recognizeDialog.dismiss()
isRecognizeOpen = false
FoodRecognizeActivity.close()
return
}
if (isTakingPhoto.get() || recognizeDialog.isShowing || abs(lastWeight - weight) <= WEIGHT_CHANGE_VALUE) return
if (!isRecognizeEnabled || isTakingPhoto.get() || isRecognizeOpen || abs(lastWeight - weight) <= WEIGHT_CHANGE_VALUE) return
isTakingPhoto.set(true)
cameraUtils.takePhoto(
succCallback = cameraSuccessCallback,
@@ -194,9 +198,13 @@ class PrepareCookActivity : BaseActivity() {
// materialType =
isNewDishType = true
isOriginalData = false
useWeight = lastWeight
isSetFinished = true
})
onFoodItemClick(list.size - 1)
binding.rvDishPartList.smoothScrollToPosition(list.size - 1)
val positon = list.size - 1
dishPartAdapter.notifyItemInserted(positon)
onFoodItemClick(positon)
binding.rvDishPartList.smoothScrollToPosition(positon)
}
/**
@@ -298,18 +306,24 @@ class PrepareCookActivity : BaseActivity() {
private var firstReqSize = 0
private val list = mutableListOf<CookFoodGoodsEntity>()
private val recognizeDialog by lazy {
FoodRecognizeDialog(
activity = this,
onDismiss = { isTakingPhoto.set(false) },
onItemSelected = foodSelectCallback
)
}
private val dishPartAdapter by lazy {
DishPartAdapter(list).apply {
setOnItemClickListener { _, _, positon ->
onFoodItemClick(positon)
}
addOnItemChildClickListener(R.id.ivClearIcon) { _, _, positon ->
if (list[positon].isOriginalData) {
list[positon].run {
isNewDishType = false
isOriginalData = false
useWeight = 0.0
isSetFinished = false
}
notifyItemChanged(positon)
return@addOnItemChildClickListener
}
removeAt(positon)
}
}
}
@@ -388,11 +402,14 @@ class PrepareCookActivity : BaseActivity() {
override fun onResume() {
super.onResume()
isRecognizeEnabled = true
isRecognizeOpen = false
cameraUtils.bind()
}
override fun onPause() {
super.onPause()
isRecognizeEnabled = false
cameraUtils.unbind()
}
@@ -438,6 +455,7 @@ class PrepareCookActivity : BaseActivity() {
*/
private val cameraSuccessCallback: (Uri) -> Unit = { uri ->
Log.d(TAG, "takePhoto success")
lastPhotoUri = uri
lifecycleScope.launch {
withContext(Dispatchers.IO) {
val bitmap = ImageUtil.uriToBitmap(this@PrepareCookActivity, uri)
@@ -485,7 +503,13 @@ class PrepareCookActivity : BaseActivity() {
foodId = index.toString()
))
}
recognizeDialog.loadData(foodList)
FoodRecognizeActivity.start(
activity = this,
imageUri = lastPhotoUri?.toString(),
goodsList = ArrayList(foodList),
onResult = { item -> item?.let { foodSelectCallback(it) } }
)
isRecognizeOpen = true
isTakingPhoto.set(false)
lastWeight = currentWeight
}
@@ -2,8 +2,10 @@ package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import android.widget.LinearLayout
import androidx.activity.addCallback
import com.google.android.flexbox.AlignItems
import com.google.android.flexbox.FlexDirection
@@ -74,7 +76,11 @@ class SlaveActivity : BaseActivity() {
val smallSize = usable / 9
val largeSize = (usable - smallSize * 6) / 2
val rvHeight = smallSize * 3 + 3 * 4.dp
binding.rvScaleList.layoutParams = binding.rvScaleList.layoutParams.also { it.height = rvHeight }
binding.rvScaleList.updateLayoutParams<LinearLayout.LayoutParams> {
height = rvHeight
weight = 0f
topMargin = 20.dp
}
binding.rvScaleList.layoutManager = FlexboxLayoutManager(this).apply {
flexDirection = FlexDirection.COLUMN
flexWrap = FlexWrap.WRAP
@@ -86,6 +92,7 @@ class SlaveActivity : BaseActivity() {
binding.rvScaleList.adapter = scale22Adapter
}
ScaleDeviceConfig.DEVICE_ID_18 -> {
binding.rvScaleList.updateLayoutParams<LinearLayout.LayoutParams> { topMargin = 0 }
binding.rvScaleList.layoutManager = GridLayoutManager(this, 6).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = 1
@@ -97,6 +104,7 @@ class SlaveActivity : BaseActivity() {
binding.rvScaleList.adapter = scale18Adapter
}
else -> {
binding.rvScaleList.updateLayoutParams<LinearLayout.LayoutParams> { topMargin = 20.dp }
binding.rvScaleList.layoutManager = LinearLayoutManager(this)
linearAdapter.setOnItemClickListener { _, _, position ->
val item = linearAdapter.items.getOrNull(position) ?: return@setOnItemClickListener
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

@@ -21,8 +21,6 @@
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:gravity="center_vertical"
android:paddingTop="30dp"
android:paddingBottom="30dp"
android:textColor="@color/black"
android:textSize="28sp"
app:layout_constraintStart_toStartOf="parent"
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:background="@color/white">
<!-- 食材照片,权重占满剩余空间 -->
<ImageView
android:id="@+id/ivFoodPhoto"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="centerCrop"
tools:ignore="ContentDescription"
tools:src="@color/bg_color" />
<!-- 识别到的食材列表,无数据时隐藏 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:maxHeight="200dp"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="4"
tools:listitem="@layout/list_item_search_goods_info"
tools:visibility="visible" />
<!-- 选中的食材名称 -->
<TextView
android:id="@+id/tvSelectedFood"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginTop="12dp"
android:gravity="center"
android:text="-"
android:textColor="@color/black333"
android:textSize="32sp" />
<!-- 秤1重量 -->
<TextView
android:id="@+id/tvWeight"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginTop="8dp"
android:background="@drawable/shape_white_12_corners"
android:gravity="center"
android:text="0.0 g"
android:textColor="@color/black333"
android:textSize="32sp" />
<!-- 更换食材 -->
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnChangeFood"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginTop="30dp"
android:background="@drawable/shape_green_bg"
android:text="更换食材"
android:textColor="@color/white"
android:textSize="32sp"
android:textStyle="bold" />
<!-- 取消 -->
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCancel"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginTop="30dp"
android:layout_marginBottom="30dp"
android:background="@drawable/shape_white_12_corners"
android:text="取消"
android:textColor="@color/black333"
android:textSize="32sp"
android:textStyle="bold" />
</LinearLayout>
+21 -17
View File
@@ -4,43 +4,47 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="12dp"
android:paddingHorizontal="5dp"
tools:background="@color/bg_color">
<!-- IP 与主设备状态同行显示 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<!-- 本机IP地址 -->
<TextView
android:id="@+id/tvDeviceIp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="0dp"
android:textSize="22sp"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/home_title"
tools:text="IP192.168.1.100" />
<!-- 主设备连接状态 -->
<TextView
android:id="@+id/tvMasterStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:textSize="16sp"
tools:text="主设备:未连接" />
</LinearLayout>
<!-- 设备信息 -->
<TextView
android:id="@+id/tvDeviceId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="0dp"
android:textSize="18sp"
android:textSize="16sp"
android:textColor="@color/home_sub_title"
tools:text="设备:device_02" />
<!-- 主设备连接状态 -->
<TextView
android:id="@+id/tvMasterStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="0dp"
android:layout_marginBottom="12dp"
android:textSize="16sp"
tools:text="主设备:未连接" />
<!-- 本机秤数据列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvScaleList"
@@ -46,11 +46,22 @@
<ImageView
android:id="@+id/ivOperateIcon"
android:layout_width="60dp"
android:layout_width="80dp"
android:layout_height="60dp"
android:layout_marginEnd="30dp"
tools:src="@drawable/ic_dish_selected"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/ivClearIcon"
android:layout_marginEnd="10dp"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/ivClearIcon"
android:layout_width="80dp"
android:layout_height="60dp"
tools:src="@drawable/ic_dish_clear"
android:layout_marginEnd="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />