5 Commits
Author SHA1 Message Date
lvmengandClaude Sonnet 4.6 82217b4fb1 feat(activity): 合并采集功能至 PrepareCookActivity 并优化识别页重量处理逻辑
- 将 DishSamplingActivity 逻辑合并至 PrepareCookActivity,根据 cookMode 区分采集/制作模式
- 布局替换:activity_prepare_cook 第一子 View 改为搜索栏(etInputDish + ivDishSearch),删除第二个子 LinearLayout
- InitActivity / HomeActivity / SamplingListActivity 改用 PrepareCookActivity 替代 DishSamplingActivity
- FoodRecognizeActivity 新增 selectedPosition 字段,避免通过 isClicked 线性查找索引
- 优化重量更新逻辑:重量增加立即同步,重量减少延迟 3 秒更新,防止关页前误覆盖用量
- foodSelectCallback 改为累加同 goodsId 的 useWeight,不再限制重复添加

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 18:26:20 +08:00
lvmengandClaude Sonnet 4.6 72ecc8fcd6 fix(scale): 修复 RS485 多联秤地址 ≥ 10 时清零命令发送到错误设备的问题
- 新增 FixedSensorScale 继承 SensorScale,通过反射重写 zeroTwo/tareTwo
- 修复根因:SDK 将 int 地址直接拼接为十进制字符串再按十六进制解析,
  导致地址 10 发送字节 0x10(=16号秤)而非 0x0A(=10号秤)
- 修复方式:String.format("%02X", address) 确保正确的十六进制编码
- Weigher2 替换 mSensorScale 字段类型为 FixedSensorScale
- WeightUtil.tareTwo 移除 isTaring 防重复逻辑,简化为直接调用 zeroTwo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 14:58:27 +08:00
lvmeng 99d670390a fix(scale): 解决称重设备清零功能问题
- 修复了ScaleServiceManager中的WeightUtil引用路径错误
- 添加了WebSocket服务器中设备ID验证失败的日志记录
- 修复了Weigher2中字符串比较的错误写法,使用equals方法替代==
- 更新了WeightUtil中清零操作的日志记录和异常处理机制
2026-04-20 10:01:05 +08:00
lvmeng d3b69c4e46 fix(fragment): 修复 weightSnapshot key 因 Int 溢出导致主设备高亮失效的问题
将 snapshotKey 从 Int(address + hashCode*1000 易溢出)改为 String(deviceId#address),
避免不同设备的基准值互相覆盖,导致 delta 永远无法正确计算
2026-04-20 09:08:50 +08:00
lvmeng 193acff1e0 feat(activity): 为子设备 SlaveActivity 添加调料秤重量增加高亮提醒功能
检测本机秤稳定后重量增加超过10g,高亮对应格子(红底白字),15秒后自动恢复
2026-04-20 09:08:44 +08:00
22 changed files with 554 additions and 434 deletions
@@ -9,7 +9,9 @@ import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemDishCookBinding import com.shuwei.dish.match.databinding.ListItemDishCookBinding
import com.shuwei.dish.match.entity.CookFoodGoodsEntity import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace
import com.shuwei.dish.match.utils.ext.visible
import java.text.DecimalFormat import java.text.DecimalFormat
class DishPartAdapter(list: MutableList<CookFoodGoodsEntity>) : class DishPartAdapter(list: MutableList<CookFoodGoodsEntity>) :
@@ -36,12 +38,16 @@ class DishPartAdapter(list: MutableList<CookFoodGoodsEntity>) :
if (item.isSamplingPage) R.color.black999 else R.color.dish_green if (item.isSamplingPage) R.color.black999 else R.color.dish_green
) )
) )
ivOperateIcon.run {
if (item.isSamplingPage) gone() else visible()
}
ivClearIcon.visible()
ivClearIcon.setImageResource(
if (item.isOriginalData) R.drawable.ic_dish_clear
else R.drawable.ic_delete
)
ivOperateIcon.setImageResource( ivOperateIcon.setImageResource(
if (item.isSamplingPage)
//采集页面
R.drawable.ic_delete
else
//制作页面
if (item.isSetFinished) R.drawable.ic_dish_selected if (item.isSetFinished) R.drawable.ic_dish_selected
else R.drawable.ic_dish_unselected else R.drawable.ic_dish_unselected
) )
@@ -53,5 +59,4 @@ class DishPartAdapter(list: MutableList<CookFoodGoodsEntity>) :
} }
} }
} }
} }
@@ -9,6 +9,9 @@ import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemSearchGoodsInfoBinding import com.shuwei.dish.match.databinding.ListItemSearchGoodsInfoBinding
import com.shuwei.dish.match.entity.CookFoodGoodsEntity import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.roundedDecimalPlace
import com.shuwei.dish.match.utils.ext.visible
class GoodsInfoSearchAdapter(private var list: MutableList<CookFoodGoodsEntity>) : class GoodsInfoSearchAdapter(private var list: MutableList<CookFoodGoodsEntity>) :
BaseQuickAdapter<CookFoodGoodsEntity, GoodsInfoSearchAdapter.VH>(list) { BaseQuickAdapter<CookFoodGoodsEntity, GoodsInfoSearchAdapter.VH>(list) {
@@ -18,20 +21,29 @@ class GoodsInfoSearchAdapter(private var list: MutableList<CookFoodGoodsEntity>)
position: Int, position: Int,
item: CookFoodGoodsEntity? item: CookFoodGoodsEntity?
) { ) {
item ?: return
holder.binding.tvGoodsInfo.run { holder.binding.tvGoodsInfo.run {
text = item?.goodsName text = item.goodsName
setBackgroundResource( setBackgroundResource(
if (item?.isClicked == true) R.drawable.shape_green_stroke if (item.isClicked == true) R.drawable.shape_green_stroke
else R.drawable.shape_white_12_corners else R.drawable.shape_white_12_corners
) )
setTextColor( setTextColor(
ContextCompat.getColor( ContextCompat.getColor(
context, context,
if (item?.isClicked == true) R.color.dish_green if (item.isClicked == true) R.color.dish_green
else R.color.black666 else R.color.black666
) )
) )
} }
if (item.foodScore == -1){
holder.binding.tvFoodScore.gone()
} else {
holder.binding.tvFoodScore.let {
it.visible()
it.text = "${(item.foodScore / 100.0).roundedDecimalPlace(2)}%"
}
}
} }
override fun onCreateViewHolder( override fun onCreateViewHolder(
@@ -1,12 +1,14 @@
package com.shuwei.dish.match.adapter package com.shuwei.dish.match.adapter
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.annotation.SuppressLint
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemScale22RowBinding import com.shuwei.dish.match.databinding.ListItemScale22RowBinding
import com.shuwei.dish.match.scale.ScaleData import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig import com.shuwei.dish.match.scale.ScaleDeviceConfig
@@ -25,6 +27,9 @@ class Scale18GridAdapter : BaseQuickAdapter<ScaleData, Scale18GridAdapter.VH>(mu
/** 用 address=0 的哨兵表示空位 */ /** 用 address=0 的哨兵表示空位 */
private val empty = ScaleData("", 0, 0.0, -1, 0) private val empty = ScaleData("", 0, 0.0, -1, 0)
/** 当前高亮的格子位置,-1 表示无高亮 */
private var highlightedPosition: Int = -1
/** 点击回调,在 onCreateViewHolder 中绑定,不受 notifyItemChanged 影响 */ /** 点击回调,在 onCreateViewHolder 中绑定,不受 notifyItemChanged 影响 */
var onItemClick: ((ScaleData) -> Unit)? = null var onItemClick: ((ScaleData) -> Unit)? = null
@@ -91,6 +96,38 @@ class Scale18GridAdapter : BaseQuickAdapter<ScaleData, Scale18GridAdapter.VH>(mu
holder.b.tvAddr.text = if (valid) "${item!!.address}" else "" holder.b.tvAddr.text = if (valid) "${item!!.address}" else ""
holder.b.tvWeight.text = if (valid) "${item!!.weight}g" else "" holder.b.tvWeight.text = if (valid) "${item!!.weight}g" else ""
holder.b.tvState.text = if (valid) stateStr(item!!.state) else "" holder.b.tvState.text = if (valid) stateStr(item!!.state) else ""
// 高亮格子:红底白字;其余恢复默认
if (position == highlightedPosition) {
holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
val white = ContextCompat.getColor(holder.b.root.context, R.color.white)
holder.b.tvAddr.setTextColor(white)
holder.b.tvWeight.setTextColor(white)
holder.b.tvState.setTextColor(white)
} else {
holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell)
val normal = ContextCompat.getColor(holder.b.root.context, R.color.home_title)
holder.b.tvAddr.setTextColor(normal)
holder.b.tvWeight.setTextColor(normal)
holder.b.tvState.setTextColor(normal)
}
}
/**
* 高亮指定位置的格子(红底白字),同时清除上一个高亮
* @param position 要高亮的格子索引
*/
fun highlightPosition(position: Int) {
val prev = highlightedPosition
highlightedPosition = position
if (prev >= 0) notifyItemChanged(prev)
if (position >= 0) notifyItemChanged(position)
}
/**
* 清除所有高亮,恢复默认样式
*/
fun clearHighlight() {
highlightPosition(-1)
} }
private fun stateStr(state: Int) = when (state) { private fun stateStr(state: Int) = when (state) {
@@ -4,10 +4,12 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.FlexboxLayoutManager
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemScale22RowBinding import com.shuwei.dish.match.databinding.ListItemScale22RowBinding
import com.shuwei.dish.match.scale.ScaleData import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig import com.shuwei.dish.match.scale.ScaleDeviceConfig
@@ -32,6 +34,9 @@ class Scale22GridAdapter(
/** 用 address=0 的哨兵表示空位 */ /** 用 address=0 的哨兵表示空位 */
private val empty = ScaleData("", 0, 0.0, -1, 0) private val empty = ScaleData("", 0, 0.0, -1, 0)
/** 当前高亮的格子位置,-1 表示无高亮 */
private var highlightedPosition: Int = -1
/** 点击回调,在 onCreateViewHolder 中绑定,不受 notifyItemChanged 影响 */ /** 点击回调,在 onCreateViewHolder 中绑定,不受 notifyItemChanged 影响 */
var onItemClick: ((ScaleData) -> Unit)? = null var onItemClick: ((ScaleData) -> Unit)? = null
@@ -93,6 +98,38 @@ class Scale22GridAdapter(
holder.b.tvAddr.text = if (valid) "${item!!.address}" else "" holder.b.tvAddr.text = if (valid) "${item!!.address}" else ""
holder.b.tvWeight.text = if (valid) "${item!!.weight}g" else "" holder.b.tvWeight.text = if (valid) "${item!!.weight}g" else ""
holder.b.tvState.text = if (valid) stateStr(item!!.state) else "" holder.b.tvState.text = if (valid) stateStr(item!!.state) else ""
// 高亮格子:红底白字;其余恢复默认
if (position == highlightedPosition) {
holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
val white = ContextCompat.getColor(holder.b.root.context, R.color.white)
holder.b.tvAddr.setTextColor(white)
holder.b.tvWeight.setTextColor(white)
holder.b.tvState.setTextColor(white)
} else {
holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell)
val normal = ContextCompat.getColor(holder.b.root.context, R.color.home_title)
holder.b.tvAddr.setTextColor(normal)
holder.b.tvWeight.setTextColor(normal)
holder.b.tvState.setTextColor(normal)
}
}
/**
* 高亮指定位置的格子(红底白字),同时清除上一个高亮
* @param position 要高亮的格子索引
*/
fun highlightPosition(position: Int) {
val prev = highlightedPosition
highlightedPosition = position
if (prev >= 0) notifyItemChanged(prev)
if (position >= 0) notifyItemChanged(position)
}
/**
* 清除所有高亮,恢复默认样式
*/
fun clearHighlight() {
highlightPosition(-1)
} }
private fun stateStr(state: Int) = when (state) { private fun stateStr(state: Int) = when (state) {
@@ -86,4 +86,7 @@ class CookFoodGoodsEntity(
//true-原始数据,false-非原始数据 //true-原始数据,false-非原始数据
@Ignore @Ignore
var isOriginalData: Boolean = true var isOriginalData: Boolean = true
@Ignore
var foodScore: Int = -1
} }
@@ -5,6 +5,7 @@ import android.util.Log
import com.shuwei.dish.match.base.DeviceRole import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.utils.NetworkUtil import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.WeightUtil
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
/** /**
@@ -165,7 +166,7 @@ object ScaleServiceManager {
*/ */
fun sendTare(deviceId: String, address: Int): Boolean { fun sendTare(deviceId: String, address: Int): Boolean {
return if (deviceId == GlobalData.deviceId) { return if (deviceId == GlobalData.deviceId) {
com.shuwei.dish.match.utils.WeightUtil.tareTwo(address) WeightUtil.tareTwo(address)
true true
} else { } else {
val cmd = ScaleCommand(ScaleCommand.CMD_TARE, deviceId, address) val cmd = ScaleCommand(ScaleCommand.CMD_TARE, deviceId, address)
@@ -137,15 +137,18 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
try { try {
val cmd = gson.fromJson(message, ScaleCommand::class.java) val cmd = gson.fromJson(message, ScaleCommand::class.java)
// 校验指令目标设备是否为本机 // 校验指令目标设备是否为本机
if (cmd.deviceId != deviceId) return if (cmd.deviceId != deviceId) {
Log.d(TAG, "收到清零指令, 但设备号不正确, deviceId=${cmd.deviceId}")
return
}
when (cmd.cmd) { when (cmd.cmd) {
ScaleCommand.CMD_TARE -> { ScaleCommand.CMD_TARE -> {
Log.d(TAG, "收到清零指令, address=${cmd.address}") Log.d(TAG, "收到清零指令, address=${cmd.address}")
com.shuwei.dish.match.utils.WeightUtil.tareTwo(cmd.address) WeightUtil.tareTwo(cmd.address)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "解析指令失败: ${e.message}") Log.w(TAG, "收到清零指令, 解析指令失败: ${e.message}")
} }
} }
@@ -16,6 +16,7 @@ import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.visible import com.shuwei.dish.match.utils.ext.visible
import androidx.core.net.toUri
/** /**
* 食材识别结果页面 * 食材识别结果页面
@@ -71,12 +72,25 @@ class FoodRecognizeActivity : BaseActivity() {
private val list = mutableListOf<CookFoodGoodsEntity>() private val list = mutableListOf<CookFoodGoodsEntity>()
/** 当前选中的列表位置,-1 表示无选中 */
private var selectedPosition = -1
/** 秤1当前实时重量 */
private var currentWeight = 0.0
/** 重量减少时延迟更新的任务,页面关闭前取消可防止用错误重量 */
private var pendingWeightRunnable: Runnable? = null
/** 当前待延迟更新的目标重量,避免相同重量重复重置计时器 */
private var pendingWeight = 0.0
private val adapter = GoodsInfoSearchAdapter(list).apply { private val adapter = GoodsInfoSearchAdapter(list).apply {
setOnItemClickListener { _, _, position -> setOnItemClickListener { _, _, position ->
// 更新选中状态
list.forEachIndexed { i, item -> item.isClicked = (i == position) } list.forEachIndexed { i, item -> item.isClicked = (i == position) }
// 点击选择时记录当前秤重量
list[position].useWeight = currentWeight
notifyDataSetChanged() notifyDataSetChanged()
// 同步更新选中食材名称 selectedPosition = position
binding.tvSelectedFood.text = list[position].goodsName ?: "-" binding.tvSelectedFood.text = list[position].goodsName ?: "-"
} }
} }
@@ -85,6 +99,7 @@ class FoodRecognizeActivity : BaseActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityFoodRecognizeBinding.inflate(layoutInflater) binding = ActivityFoodRecognizeBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
registerWeightListener()
setHeaderBackground() setHeaderBackground()
// 显示标题栏 // 显示标题栏
@@ -93,7 +108,6 @@ class FoodRecognizeActivity : BaseActivity() {
initData() initData()
initRecyclerView() initRecyclerView()
initClickListeners() initClickListeners()
registerWeightListener()
} }
/** /**
@@ -103,7 +117,7 @@ class FoodRecognizeActivity : BaseActivity() {
// 加载食材照片 // 加载食材照片
val imageUri = intent.getStringExtra(EXTRA_IMAGE_URI) val imageUri = intent.getStringExtra(EXTRA_IMAGE_URI)
if (!imageUri.isNullOrBlank()) { if (!imageUri.isNullOrBlank()) {
binding.ivFoodPhoto.setImageURI(Uri.parse(imageUri)) binding.ivFoodPhoto.setImageURI(imageUri.toUri())
} }
// 取出内存缓存的食材列表,取用后立即清空 // 取出内存缓存的食材列表,取用后立即清空
@@ -111,6 +125,7 @@ class FoodRecognizeActivity : BaseActivity() {
if (!goodsList.isNullOrEmpty()) { if (!goodsList.isNullOrEmpty()) {
list.addAll(goodsList) list.addAll(goodsList)
// 默认选中第一项 // 默认选中第一项
selectedPosition = 0
list[0].isClicked = true list[0].isClicked = true
binding.tvSelectedFood.text = list[0].goodsName ?: "-" binding.tvSelectedFood.text = list[0].goodsName ?: "-"
} }
@@ -144,14 +159,11 @@ class FoodRecognizeActivity : BaseActivity() {
private fun initClickListeners() { private fun initClickListeners() {
// 更换食材:打开食材搜索弹窗 // 更换食材:打开食材搜索弹窗
binding.btnChangeFood.clickWithDebounce { binding.btnChangeFood.clickWithDebounce {
val currentName = if (list.isNotEmpty()) list.firstOrNull { it.isClicked }?.goodsName else null val currentName = list.getOrNull(selectedPosition)?.goodsName
FoodSearchDialog( FoodSearchDialog(
activity = this, activity = this,
defGoodsName = currentName, defGoodsName = currentName,
onItemSelected = { item -> onItemSelected = { item -> updateSelectedFood(item) }
// 用搜索结果替换当前选中食材
updateSelectedFood(item)
}
).show() ).show()
} }
@@ -164,17 +176,14 @@ class FoodRecognizeActivity : BaseActivity() {
*/ */
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
private fun updateSelectedFood(item: CookFoodGoodsEntity) { 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 } val existIndex = list.indexOfFirst { it.goodsId == item.goodsId }
if (existIndex >= 0) { if (existIndex >= 0) {
list.forEachIndexed { i, e -> e.isClicked = (i == existIndex) } list.forEachIndexed { i, e -> e.isClicked = (i == existIndex) }
selectedPosition = existIndex
} else { } else {
list.add(0, item) list.add(0, item)
list.forEachIndexed { i, e -> e.isClicked = (i == 0) }
selectedPosition = 0
} }
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
binding.tvSelectedFood.text = item.goodsName ?: "-" binding.tvSelectedFood.text = item.goodsName ?: "-"
@@ -183,22 +192,55 @@ class FoodRecognizeActivity : BaseActivity() {
/** /**
* 注册秤1重量监听 * 注册秤1重量监听
* 只有重量高于识别阈值时才更新选中食材的 useWeight,
* 低于阈值说明食材已被取走,保留上次有效值等待页面关闭
*/ */
private fun registerWeightListener() { private fun registerWeightListener() {
WeightUtil.addWeightListener(TAG) { address, _, weight -> WeightUtil.addWeightListener(TAG) { address, state, weight ->
if (address == AddressUtil.ONE) { if (address == AddressUtil.ONE && state == WeightUtil.STATE_STABLE) {
runOnUiThread { runOnUiThread {
binding.tvWeight.text = "$weight g" currentWeight = weight
binding.tvWeight.run {
if (tag != weight) {
text = "$weight g"
tag = weight
}
}
if (weight >= PrepareCookActivity.WEIGHT_RECOGNIZE_VALUE) {
adapter.items.firstOrNull { it.isClicked }?.let { item ->
if (weight > (item.useWeight ?: 0.0)) {
// 重量增加:立即更新
cancelPendingWeightUpdate()
item.useWeight = currentWeight
} else if (pendingWeight != weight) {
// 重量减少且目标值变化:重置延迟任务,3 秒无新变化后更新
cancelPendingWeightUpdate()
pendingWeight = weight
pendingWeightRunnable = Runnable {
item.useWeight = weight
pendingWeightRunnable = null
}.also { handler.postDelayed(it, 3000) }
}
}
}
} }
} }
} }
} }
/**
* 取消待执行的延迟重量更新任务
*/
private fun cancelPendingWeightUpdate() {
pendingWeightRunnable?.let { handler.removeCallbacks(it) }
pendingWeightRunnable = null
}
/** /**
* 携带当前选中食材关闭页面 * 携带当前选中食材关闭页面
*/ */
private fun finishWithResult() { private fun finishWithResult() {
val selected = list.firstOrNull { it.isClicked } val selected = list.getOrNull(selectedPosition)
val intent = Intent().apply { val intent = Intent().apply {
selected?.let { putExtra(EXTRA_SELECTED_ITEM, it) } selected?.let { putExtra(EXTRA_SELECTED_ITEM, it) }
} }
@@ -208,6 +250,7 @@ class FoodRecognizeActivity : BaseActivity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
WeightUtil.removeWeightListener(TAG) cancelPendingWeightUpdate()
// WeightUtil.removeWeightListener(TAG)
} }
} }
@@ -158,8 +158,8 @@ class HomeActivity : BaseActivity() {
return@countCookFood return@countCookFood
} }
//无烹饪数据打开新增采样页面 //无烹饪数据打开新增采样页面
startActivity<DishSamplingActivity> { startActivity<PrepareCookActivity> {
putExtra(DishSamplingActivity.PAGE_FROM, DishSamplingActivity.HOME) putExtra(PrepareCookActivity.PAGE_FROM, PrepareCookActivity.HOME)
} }
finish() finish()
} }
@@ -113,8 +113,8 @@ class InitActivity : BaseActivity() {
return@countCookFood return@countCookFood
} }
//无烹饪数据打开新增采样页面 //无烹饪数据打开新增采样页面
startActivity<DishSamplingActivity> { startActivity<PrepareCookActivity> {
putExtra(DishSamplingActivity.PAGE_FROM, DishSamplingActivity.HOME) putExtra(PrepareCookActivity.PAGE_FROM, PrepareCookActivity.HOME)
} }
} }
} }
@@ -1,31 +1,35 @@
package com.shuwei.dish.match.ui package com.shuwei.dish.match.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.Typeface import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.DishPartAdapter import com.shuwei.dish.match.adapter.DishPartAdapter
import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityPrepareCookBinding import com.shuwei.dish.match.databinding.ActivityPrepareCookBinding
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.dialog.CommonDialog import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.FoodSearchDialog import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.entity.CookFoodEntity import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.FoodRecord import com.shuwei.dish.match.entity.FoodRecord
import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.lifecycle.lifecycleScope
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.objbox.FoodModule import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.utils.AddressUtil import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.CameraUtils import com.shuwei.dish.match.utils.CameraUtils
import com.shuwei.dish.match.utils.ImageUtil import com.shuwei.dish.match.utils.ImageUtil
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.SwipeCallback import com.shuwei.dish.match.utils.SwipeCallback
import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import com.shuwei.dish.match.utils.ext.clickWithDebounce import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.roundedDecimalPlace import com.shuwei.dish.match.utils.ext.roundedDecimalPlace
@@ -46,6 +50,9 @@ class PrepareCookActivity : BaseActivity() {
companion object { companion object {
const val TAG = "CookActivity" const val TAG = "CookActivity"
const val FOOD_ITEM = "foodItem" const val FOOD_ITEM = "foodItem"
const val FOOD_NAME = "foodName"
const val PAGE_FROM = "pageFrom"
const val HOME = "home"
const val WEIGHT_CHANGE_VALUE = 5 const val WEIGHT_CHANGE_VALUE = 5
const val WEIGHT_RECOGNIZE_VALUE = 10 const val WEIGHT_RECOGNIZE_VALUE = 10
} }
@@ -56,6 +63,12 @@ class PrepareCookActivity : BaseActivity() {
private var food: FoodRecord? = null private var food: FoodRecord? = null
/** 采集模式下通过搜索页回填的 foodId */
private var foodId: String? = null
/** 来源页面标识,HOME 时返回跳转 SamplingListActivity */
private var pageFrom: String? = null
private var goodsList: MutableList<CookFoodGoodsEntity>? = null private var goodsList: MutableList<CookFoodGoodsEntity>? = null
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@@ -64,50 +77,91 @@ class PrepareCookActivity : BaseActivity() {
binding = ActivityPrepareCookBinding.inflate(layoutInflater) binding = ActivityPrepareCookBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setHeaderBackground() setHeaderBackground()
pageFrom = intent.getStringExtra(PAGE_FROM)
food = intent.extras?.getSerializable(FOOD_ITEM) as FoodRecord?
val isSamplingMode = SpTool.cookMode == 1
val titleText = if (isSamplingMode) "菜品采集" else "菜品制作"
setTitleBar(titleBarAction = { setTitleBar(titleBarAction = {
it.visible() it.visible()
}, titleAction = { }, titleAction = {
it.text = "菜品制作" it.text = titleText
}, rightIconActon = { }, rightIconActon = {
it.gone() it.gone()
it.setImageResource(R.drawable.ic_setting)
it.setOnClickListener {
//startActivity<DeviceSettingActivity>()
}
}, backAction = { }, backAction = {
it.setOnClickListener { it.setOnClickListener {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
}) })
food = intent.extras?.getSerializable(FOOD_ITEM) as FoodRecord?
binding.tvDishName.text = food?.foodName ?: "" // 根据 cookMode 设置搜索栏可编辑性
if (isSamplingMode) {
// 采集模式:可编辑,支持搜索
val foodName = intent.getStringExtra(FOOD_NAME)
binding.etInputDish.setText(foodName ?: food?.foodName ?: "")
binding.ivDishSearch.visible()
} else {
// 制作模式:不可编辑,仅展示菜名
binding.etInputDish.setText(food?.foodName ?: "")
binding.etInputDish.isFocusable = false
binding.etInputDish.isFocusableInTouchMode = false
binding.ivDishSearch.gone()
}
addViewClickListener() addViewClickListener()
addBackKeyListener() addBackKeyListener()
initCamera(binding.flCameraContainer) initCamera(binding.flCameraContainer)
initRecyclerView() initRecyclerView()
if (!isSamplingMode) {
getDishDetail() getDishDetail()
} }
}
private fun addViewClickListener() { private fun addViewClickListener() {
WeightUtil.addWeightListener( WeightUtil.addWeightListener(
weightKey = TAG, weightKey = TAG,
getWeight = { address, state, weight -> getWeight = { address, state, weight ->
//Log.d(TAG, "addViewClickListener: address=$address,state=$state,weight=$weight") if (address == AddressUtil.ONE && state == WeightUtil.STATE_STABLE) {
if (address == AddressUtil.ONE) { runOnUiThread {
binding.tvDishPartWeight.text = "${weight}"
if (state == WeightUtil.STATE_STABLE) {
recognizeFood(weight) recognizeFood(weight)
} }
} }
}) })
binding.ivWeightClear.setOnClickListener {
WeightUtil.tareTwo(AddressUtil.ONE) if (SpTool.cookMode == 1) {
// 采集模式:绑定搜索栏交互
binding.ivDishSearch.setOnClickListener { v -> jumpSearch(v) }
binding.etInputDish.addOnActionSearchListener { jumpSearch(binding.etInputDish) }
} }
binding.ivWeightAdd.setOnClickListener { addWeight() }
binding.tvAddFood.clickWithDebounce { addFood() }
binding.btnCook.clickWithDebounce { binding.btnCook.clickWithDebounce {
if (SpTool.cookMode == 1) {
// 采集模式:从输入框创建 FoodRecord
val showFoodName = binding.etInputDish.text.toString().trim()
if (showFoodName.isBlank()) {
toast("菜品名称为空")
return@clickWithDebounce
}
if (list.isEmpty()) {
toast("菜品构成信息未设置")
return@clickWithDebounce
}
val foodRecord = FoodRecord()
binding.etInputDish.let {
if (it.tag != null && it.tag.toString() == it.text.toString()) {
foodRecord.foodId = foodId
}
}
foodRecord.foodName = showFoodName
foodRecord.cookMode = 1
Log.d(TAG, "goToSubmit: goodsList:${list.toJsonString()}")
startActivity<SubmitFoodActivity> {
putExtra(SubmitFoodActivity.GOODS_LIST, list as Serializable)
putExtra(SubmitFoodActivity.FOOD_ITEM, foodRecord as Serializable)
}
} else {
// 制作模式:检查食材是否全部设置完成
val count = list.count { it.isSetFinished.not() } val count = list.count { it.isSetFinished.not() }
if (count > 0) { if (count > 0) {
showRemindDialog() showRemindDialog()
@@ -115,49 +169,12 @@ class PrepareCookActivity : BaseActivity() {
} }
openSubmitPage() openSubmitPage()
} }
binding.rgCook.setOnCheckedChangeListener { group, checkedId ->
if (clickIndex < 0 || clickIndex >= list.size) return@setOnCheckedChangeListener
val tempType =
if (checkedId == R.id.rbDishTypeFirst) 1 else if (checkedId == R.id.rbDishTypeSecond) 2 else -1
list[clickIndex].materialType = tempType
dishPartAdapter.notifyItemChanged(clickIndex)
binding.tvDishType.text =
if (tempType == 1) "主材" else if (tempType == 2) "辅材" else ""
}
}
/**
* 添加重量信息到食材数据
*/
private fun addWeight() {
if (clickIndex == -1) {
toast("请选择菜品构成")
return
}
val clickItem = list[clickIndex]
clickItem.run {
if (isNewDishType && materialType != 1 && materialType != 2) {
toast("请选择主辅材类型")
return
}
}
val weight = binding.tvDishPartWeight.text.toString().toDouble()
if (weight <= 0.toDouble()) {
toast("食材用量需要大于0")
return
}
Log.d(TAG, "addViewClickListener->useRealWeight=$weight")
clickItem.run {
useWeight = weight
isOriginalData = false
isSetFinished = true
dishPartAdapter.notifyItemChanged(clickIndex)
} }
} }
private val isTakingPhoto = AtomicBoolean(false) private val isTakingPhoto = AtomicBoolean(false)
private var isRecognizeOpen = false private var isRecognizeOpen = false
// 页面可见时允许识别,onPause 后禁止,onResume 后恢复 // 页面可见时允许识别,onPause 后禁止,onResume 后恢复
private var isRecognizeEnabled = true private var isRecognizeEnabled = true
private var lastPhotoUri: Uri? = null private var lastPhotoUri: Uri? = null
@@ -185,33 +202,24 @@ class PrepareCookActivity : BaseActivity() {
/** /**
* 菜品选择回调 * 菜品选择回调
* - list 中不含该 goodsId:直接新增,isNewDishType = true
* - list 中已含该 goodsId:累加 useWeight
*/ */
private val foodSelectCallback: (CookFoodGoodsEntity) -> Unit = foodSelectCallback@{ item -> private val foodSelectCallback: (CookFoodGoodsEntity) -> Unit = { item ->
val filterResult = list.firstOrNull { it.goodsId == item.goodsId } val existing = list.firstOrNull { it.goodsId == item.goodsId }
if (filterResult != null) { if (existing != null) {
toast("不允许重复添加同一食材") existing.useWeight = (existing.useWeight ?: 0.0) + (item.useWeight ?: 0.0)
return@foodSelectCallback existing.isSetFinished = true
} dishPartAdapter.notifyItemChanged(list.indexOf(existing))
list.add(CookFoodGoodsEntity().apply { } else {
goodsId = item.goodsId list.add(item.also {
goodsName = item.goodsName it.isNewDishType = true
// materialType = it.isSetFinished = true
isNewDishType = true
isOriginalData = false
useWeight = lastWeight
isSetFinished = true
}) })
val positon = list.size - 1 val position = list.size - 1
dishPartAdapter.notifyItemInserted(positon) dishPartAdapter.notifyItemInserted(position)
onFoodItemClick(positon) binding.rvDishPartList.smoothScrollToPosition(position)
binding.rvDishPartList.smoothScrollToPosition(positon)
} }
/**
* 添加食材
*/
private fun addFood() {
FoodSearchDialog(activity = this, onItemSelected = foodSelectCallback).show()
} }
private fun openSubmitPage() { private fun openSubmitPage() {
@@ -307,9 +315,12 @@ class PrepareCookActivity : BaseActivity() {
private var firstReqSize = 0 private var firstReqSize = 0
private val list = mutableListOf<CookFoodGoodsEntity>() private val list = mutableListOf<CookFoodGoodsEntity>()
private val dishPartAdapter by lazy { private val dishPartAdapter by lazy {
DishPartAdapter(list).apply { DishPartAdapter(list = list).apply {
setOnItemClickListener { _, _, positon -> setOnItemClickListener { _, _, positon ->
onFoodItemClick(positon) list.forEachIndexed { index, entity ->
entity.isItemClicked = index == positon
}
notifyDataSetChanged()
} }
addOnItemChildClickListener(R.id.ivClearIcon) { _, _, positon -> addOnItemChildClickListener(R.id.ivClearIcon) { _, _, positon ->
Log.d(TAG, "onFoodItemClick: ${list[positon].toJsonString()}") Log.d(TAG, "onFoodItemClick: ${list[positon].toJsonString()}")
@@ -327,52 +338,6 @@ class PrepareCookActivity : BaseActivity() {
} }
} }
/**
* 菜品列表item点击
*/
private fun onFoodItemClick(positon: Int) {
this@PrepareCookActivity.clickIndex = positon
list[clickIndex].let { it ->
binding.tvDishShowName.run {
text = it.goodsName
setTypeface(typeface, Typeface.BOLD)
}
var materialType: String
when (it.materialType) {
1 -> {
materialType = "主材"
binding.rgCook.check(R.id.rbDishTypeFirst)
}
2 -> {
materialType = "辅材"
binding.rgCook.check(R.id.rbDishTypeSecond)
}
else -> {
materialType = ""
binding.rgCook.clearCheck()
}
}
binding.tvDishType.text = materialType
list.forEach { bean -> bean.isItemClicked = false }
it.isItemClicked = true
dishPartAdapter.notifyDataSetChanged()
if (it.isNewDishType) {
binding.dividerDishType.visible()
binding.llDishType.visible()
} else {
binding.dividerDishType.gone()
binding.llDishType.gone()
}
}
}
private var clickIndex = -1
/** /**
* RecyclerView初始化 * RecyclerView初始化
*/ */
@@ -387,14 +352,10 @@ class PrepareCookActivity : BaseActivity() {
dishPartAdapter.notifyItemChanged(position) dishPartAdapter.notifyItemChanged(position)
return@SwipeCallback return@SwipeCallback
} }
// dishPartAdapter.removeAt(position)
list.removeAt(position) list.removeAt(position)
dishPartAdapter.notifyItemRemoved(position) dishPartAdapter.notifyItemRemoved(position)
dishPartAdapter.notifyItemRangeChanged(position, list.size - position) dishPartAdapter.notifyItemRangeChanged(position, list.size - position)
toast("已删除") toast("已删除")
if (list.isEmpty()) {
binding.tvDishShowName.text = ""
}
}) })
itemTouchHelper.attachToRecyclerView(this) itemTouchHelper.attachToRecyclerView(this)
} }
@@ -428,11 +389,23 @@ class PrepareCookActivity : BaseActivity() {
*/ */
private fun addBackKeyListener() { private fun addBackKeyListener() {
onBackPressedDispatcher.addCallback(this) { onBackPressedDispatcher.addCallback(this) {
if (SpTool.cookMode == 1) {
// 采集模式:有新增食材时提示保存
if (list.isEmpty().not()) {
saveDataRemindDialog()
return@addCallback
}
if (pageFrom == HOME) {
startActivity<SamplingListActivity>()
}
} else {
// 制作模式:有未保存数据时提示
val count = list.count { !it.isOriginalData } val count = list.count { !it.isOriginalData }
if (count > 0) { if (count > 0) {
saveDataRemindDialog() saveDataRemindDialog()
return@addCallback return@addCallback
} }
}
finish() finish()
} }
} }
@@ -443,9 +416,14 @@ class PrepareCookActivity : BaseActivity() {
private fun saveDataRemindDialog() { private fun saveDataRemindDialog() {
CommonDialog(this) CommonDialog(this)
.setTitle("温馨提示") .setTitle("温馨提示")
.setContent("您好,当前页面存在未保存的数据,确认返回吗?") .setContent("您好,当前页面存在未保存的数据,确认返回吗?")
.setNegativeButton("取消") .setNegativeButton("取消")
.setPositiveButton("确认") { finish() } .setPositiveButton("确认") {
if (SpTool.cookMode == 1 && pageFrom == HOME) {
startActivity<SamplingListActivity>()
}
finish()
}
.setOnDismissCallback { hideStatusBar() } .setOnDismissCallback { hideStatusBar() }
.show() .show()
} }
@@ -496,12 +474,16 @@ class PrepareCookActivity : BaseActivity() {
val foodList = mutableListOf<CookFoodGoodsEntity>() val foodList = mutableListOf<CookFoodGoodsEntity>()
list.forEachIndexed { index, food -> list.forEachIndexed { index, food ->
val foodName = food.name.split("WP").first() val foodName = food.name.split("WP").first()
val foodScore = ((1-food.score) * 100).roundedDecimalPlace(2) foodList.add(
foodList.add(CookFoodGoodsEntity( CookFoodGoodsEntity(
goodsId = index.toString(), goodsId = "${food.id}",
goodsName = "${foodName}-${foodScore}%", goodsName = foodName,
foodId = index.toString() foodId = index.toString()
)) ).also {
it.foodScore = ((1 - food.score) * 10000).toInt()
it.isOriginalData = false
}
)
} }
FoodRecognizeActivity.start( FoodRecognizeActivity.start(
activity = this, activity = this,
@@ -511,7 +493,26 @@ class PrepareCookActivity : BaseActivity() {
) )
isRecognizeOpen = true isRecognizeOpen = true
isTakingPhoto.set(false) isTakingPhoto.set(false)
lastWeight = currentWeight }
/**
* 跳转食物搜索页(采集模式专用)
* @param v 触发搜索的 View,用于收起键盘
*/
private fun jumpSearch(v: View) {
val intent = Intent(this, FoodSearchActivity::class.java).apply {
putExtra(FoodSearchActivity.FOOD_NAME, binding.etInputDish.text.toString().trim())
}
startActivity(intent) {
foodId = it?.getStringExtra(FoodSearchActivity.FOOD_ID)
val name = it?.getStringExtra(FoodSearchActivity.FOOD_NAME)
binding.etInputDish.run {
setText(name)
tag = name
setSelection(text.length)
}
}
KeyboardUtil.hideKeyboard(v.context, v)
} }
} }
@@ -120,8 +120,8 @@ class SamplingListActivity : BaseActivity() {
} }
return return
} }
startActivity<DishSamplingActivity> { startActivity<PrepareCookActivity> {
putExtra(DishSamplingActivity.FOOD_NAME, item.foodName) putExtra(PrepareCookActivity.FOOD_NAME, item.foodName)
} }
} }
@@ -154,7 +154,7 @@ class SamplingListActivity : BaseActivity() {
private fun addViewListener() { private fun addViewListener() {
binding.btnAddSampling.setOnClickListener { binding.btnAddSampling.setOnClickListener {
judgeDeviceConfig { judgeDeviceConfig {
startActivity<DishSamplingActivity>() startActivity<PrepareCookActivity>()
} }
} }
binding.refreshLayout.setOnRefreshListener { binding.refreshLayout.setOnRefreshListener {
@@ -2,6 +2,8 @@ package com.shuwei.dish.match.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -45,6 +47,13 @@ class SlaveActivity : BaseActivity() {
/** 已连接的主设备数量(有连接即代表主设备在线) */ /** 已连接的主设备数量(有连接即代表主设备在线) */
private var masterConnected = false private var masterConnected = false
/** 各秤地址的上一次重量快照,用于计算 delta */
private val weightSnapshot = mutableMapOf<Int, Double>()
/** 主线程 Handler,用于延迟清除高亮 */
private val mainHandler = Handler(Looper.getMainLooper())
/** 当前高亮清除任务,新高亮触发时取消旧任务 */
private var clearHighlightRunnable: Runnable? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivitySlaveBinding.inflate(layoutInflater) binding = ActivitySlaveBinding.inflate(layoutInflater)
@@ -134,9 +143,19 @@ class SlaveActivity : BaseActivity() {
/** /**
* 监听本机 WeightUtil 回调,实时刷新秤列表 * 监听本机 WeightUtil 回调,实时刷新秤列表
* 22/18个秤走网格 adapter,其余走线性列表 * 22/18个秤走网格 adapter,其余走线性列表
* 秤稳定且重量增加超过阈值时,高亮对应格子,15秒后自动恢复
*/ */
private fun listenLocalScales() { private fun listenLocalScales() {
WeightUtil.addWeightListener(TAG) { address, state, weight -> WeightUtil.addWeightListener(TAG) { address, state, weight ->
// 重量快照与高亮判断(在回调线程计算,不涉及 UI)
val baseline = weightSnapshot[address]
val shouldHighlight = baseline != null
&& state == WeightUtil.STATE_STABLE
&& (weight - baseline) > WEIGHT_DELTA_THRESHOLD
if (baseline == null || state == WeightUtil.STATE_STABLE) {
weightSnapshot[address] = weight
}
runOnUiThread { runOnUiThread {
when { when {
scale22Adapter != null -> { scale22Adapter != null -> {
@@ -167,6 +186,19 @@ class SlaveActivity : BaseActivity() {
linearAdapter.submitList(list) linearAdapter.submitList(list)
} }
} }
// 触发格子高亮
if (shouldHighlight) {
clearHighlightRunnable?.let { mainHandler.removeCallbacks(it) }
val pos22 = ScaleDeviceConfig.SCALE_ORDER_22.indexOf(address)
val pos18 = ScaleDeviceConfig.SCALE_ORDER_18.indexOf(address)
scale22Adapter?.highlightPosition(pos22)
scale18Adapter?.highlightPosition(pos18)
clearHighlightRunnable = Runnable {
scale22Adapter?.clearHighlight()
scale18Adapter?.clearHighlight()
}.also { mainHandler.postDelayed(it, HIGHLIGHT_DURATION_MS) }
}
} }
} }
} }
@@ -186,11 +218,16 @@ class SlaveActivity : BaseActivity() {
override fun onDestroy() { override fun onDestroy() {
ScaleServiceManager.onMasterConnectionChanged = null ScaleServiceManager.onMasterConnectionChanged = null
WeightUtil.removeWeightListener(TAG) WeightUtil.removeWeightListener(TAG)
clearHighlightRunnable?.let { mainHandler.removeCallbacks(it) }
super.onDestroy() super.onDestroy()
} }
companion object { companion object {
const val TAG = "SlaveActivity" const val TAG = "SlaveActivity"
/** 重量增加阈值(克),超过此值才触发高亮 */
private const val WEIGHT_DELTA_THRESHOLD = 10.0
/** 高亮自动恢复时间(毫秒) */
private const val HIGHLIGHT_DURATION_MS = 15_000L
} }
/** /**
@@ -47,8 +47,8 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
private val adapter22 by lazy { buildAdapter22() } private val adapter22 by lazy { buildAdapter22() }
private val adapter18 by lazy { Seasoning18GridAdapter() } private val adapter18 by lazy { Seasoning18GridAdapter() }
/** 各秤地址的上一次重量快照,用于计算 delta */ /** 各秤的上一次重量快照,key 格式 "{deviceId}#{address}"用于计算 delta */
private val weightSnapshot = mutableMapOf<Int, Double>() private val weightSnapshot = mutableMapOf<String, Double>()
/** 主线程 Handler,用于延迟清除高亮 */ /** 主线程 Handler,用于延迟清除高亮 */
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
/** 当前高亮清除任务,新高亮触发时取消旧任务 */ /** 当前高亮清除任务,新高亮触发时取消旧任务 */
@@ -161,7 +161,7 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
allScalesFlow.collect { scaleMap -> allScalesFlow.collect { scaleMap ->
scaleMap.forEach { (key, data) -> scaleMap.forEach { (key, data) ->
val snapshotKey = data.address + data.deviceId.hashCode() * 1000 val snapshotKey = "${data.deviceId}#${data.address}"
val baseline = weightSnapshot[snapshotKey] val baseline = weightSnapshot[snapshotKey]
if (baseline == null) { if (baseline == null) {
// 首次收到:记录基准 // 首次收到:记录基准
@@ -187,11 +187,12 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
} else { } else {
// 子设备:注册本机 WeightUtil 监听,触发后通过 broadcastEvent 通知主设备 // 子设备:注册本机 WeightUtil 监听,触发后通过 broadcastEvent 通知主设备
WeightUtil.addWeightListener(TAG) { address, state, weight -> WeightUtil.addWeightListener(TAG) { address, state, weight ->
val baseline = weightSnapshot[address] val snapshotKey = "${GlobalData.deviceId}#$address"
val baseline = weightSnapshot[snapshotKey]
Log.d(TAG, "onWeight: address=$address state=$state weight=$weight baseline=$baseline") Log.d(TAG, "onWeight: address=$address state=$state weight=$weight baseline=$baseline")
if (baseline == null) { if (baseline == null) {
weightSnapshot[address] = weight weightSnapshot[snapshotKey] = weight
return@addWeightListener return@addWeightListener
} }
@@ -201,12 +202,12 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
when { when {
// 仅在秤稳定后判断增量,避免取走物品时手部冲力造成误触发 // 仅在秤稳定后判断增量,避免取走物品时手部冲力造成误触发
state == WeightUtil.STATE_STABLE && delta > WEIGHT_DELTA_THRESHOLD -> { state == WeightUtil.STATE_STABLE && delta > WEIGHT_DELTA_THRESHOLD -> {
weightSnapshot[address] = weight weightSnapshot[snapshotKey] = weight
Log.d(TAG, "onWeight: 触发高亮 address=$address delta=$delta") Log.d(TAG, "onWeight: 触发高亮 address=$address delta=$delta")
triggerHighlight(GlobalData.deviceId, address, delta) triggerHighlight(GlobalData.deviceId, address, delta)
} }
state == WeightUtil.STATE_STABLE -> { state == WeightUtil.STATE_STABLE -> {
weightSnapshot[address] = weight weightSnapshot[snapshotKey] = weight
} }
} }
} }
@@ -0,0 +1,91 @@
package com.shuwei.dish.match.utils;
import android.util.Log;
import com.wabon.wbintelligenthardwaresdk.api.SensorScale;
import java.lang.reflect.Method;
/**
* 修复 SensorScale 地址编码 bug 的子类。
*
* 问题根因:SensorScale.zeroTwo / tareTwo 内部使用
* String command = address + "630603";
* 将 int 地址直接拼接为十进制字符串,再整体按十六进制解析成字节。
* 对于地址 1-9,十进制 == 十六进制,偶然正确;
* 对于地址 10+,十进制 "10" 被解析为 0x10=16),命中错误的物理秤。
*
* 修复方式:重写这两个方法,将地址先格式化为两位十六进制字符串(如 10 → "0A"),
* 再拼接命令码,通过反射调用父类私有方法 suspendSendThread 和 sumHex 发送。
*/
public class FixedSensorScale extends SensorScale {
private static final String TAG = "FixedSensorScale";
public FixedSensorScale(OnScaleResult result) {
super(result);
}
/**
* 重写清零命令,修复地址编码:十进制地址 → 两位十六进制字符串。
* 例:address=10 → "0A630603",发送字节 0x0A(正确),而非原来的 0x10(错误)。
*
* @param listener 操作成功回调
* @param address 秤地址(来自 onGetWeight 回调的十进制值)
*/
@Override
public void zeroTwo(OperateSuccessListener listener, int address) {
sendFixedCommand(listener, address, "630603", "zeroTwo");
}
/**
* 重写去皮命令,修复地址编码,逻辑同 zeroTwo。
*
* @param listener 操作成功回调
* @param address 秤地址
*/
@Override
public void tareTwo(OperateSuccessListener listener, int address) {
sendFixedCommand(listener, address, "630601", "tareTwo");
}
/**
* 通用发送逻辑:将地址格式化为两位十六进制后拼接命令码,
* 通过反射调用父类私有方法 sumHex 计算校验和,再调用 suspendSendThread 发送。
*
* @param listener 操作成功回调
* @param address 秤地址(十进制)
* @param commandCode 命令码(十六进制字符串,如 "630603"
* @param methodName 调用方名称,仅用于日志
*/
private void sendFixedCommand(OperateSuccessListener listener, int address,
String commandCode, String methodName) {
try {
// 将十进制地址转为两位十六进制字符串:10 → "0A"1 → "01"
String hexAddress = String.format("%02X", address);
String command = hexAddress + commandCode;
Log.d(TAG, methodName + ", address=" + address
+ ", hexAddress=" + hexAddress + ", command=" + command);
// 反射调用 sumHex(String) 计算校验和
Method sumHexMethod = SensorScale.class.getDeclaredMethod("sumHex", String.class);
sumHexMethod.setAccessible(true);
String checksum = (String) sumHexMethod.invoke(this, command);
// 反射调用 suspendSendThread(String) 发送完整命令帧
Method suspendMethod = SensorScale.class.getDeclaredMethod(
"suspendSendThread", String.class);
suspendMethod.setAccessible(true);
suspendMethod.invoke(this, command + checksum);
// 保存回调(通过反射写入父类私有字段 mOperateSuccessListener
java.lang.reflect.Field listenerField = SensorScale.class
.getDeclaredField("mOperateSuccessListener");
listenerField.setAccessible(true);
listenerField.set(this, listener);
} catch (Exception e) {
Log.e(TAG, methodName + " 反射调用失败: address=" + address, e);
}
}
}
@@ -23,7 +23,7 @@ public class Weigher2 {
public static final int ERR_003 = 1003; public static final int ERR_003 = 1003;
public static final int ERR_004 = 1004; public static final int ERR_004 = 1004;
public static final int ERR_PCB_NOT_SUPPORT = 2000; public static final int ERR_PCB_NOT_SUPPORT = 2000;
private static SensorScale mSensorScale; private static FixedSensorScale mSensorScale;
// private static String mDevicePort = "/dev/ttyS4"; // private static String mDevicePort = "/dev/ttyS4";
private static String mDevicePort = "/dev/ttyS7"; private static String mDevicePort = "/dev/ttyS7";
private static boolean mConnect = false; private static boolean mConnect = false;
@@ -40,8 +40,8 @@ public class Weigher2 {
} }
public static void init() { public static void init() {
String var0; String var0 = Build.MODEL;
if ((var0 = Build.MODEL) == "pcb_941") { if ("pcb_941".equals(var0)) {
Log.w(TAG, "pcb not support, " + var0); Log.w(TAG, "pcb not support, " + var0);
Listener var1; Listener var1;
if ((var1 = mListener) != null) { if ((var1 = mListener) != null) {
@@ -51,7 +51,7 @@ public class Weigher2 {
} else { } else {
System.init(); System.init();
initScale(); initScale();
if (var0 == "pcb_908") { if ("pcb_908".equals(var0)) {
// mDevicePort = "/dev/ttyS4"; // mDevicePort = "/dev/ttyS4";
mDevicePort = "/dev/ttyS7"; mDevicePort = "/dev/ttyS7";
} }
@@ -76,8 +76,8 @@ public class Weigher2 {
} }
public static void config() { public static void config() {
String var0; String var0 = Build.MODEL;
if ((var0 = Build.MODEL) == "pcb_941") { if ("pcb_941".equals(var0)) {
Log.w(TAG, "pcb not support, " + var0); Log.w(TAG, "pcb not support, " + var0);
Listener var2; Listener var2;
if ((var2 = mListener) != null) { if ((var2 = mListener) != null) {
@@ -402,7 +402,8 @@ public class Weigher2 {
} }
}; };
//.<init>(); //.<init>();
mSensorScale = new SensorScale(var0); // 使用修复版子类,解决地址 ≥ 10 时 zeroTwo/tareTwo 编码错误的问题
mSensorScale = new FixedSensorScale(var0);
} }
public static void setNoPull485Pin(boolean noPull) { public static void setNoPull485Pin(boolean noPull) {
@@ -1,8 +1,6 @@
package com.shuwei.dish.match.utils package com.shuwei.dish.match.utils
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace
//import com.aithings.Weigher //import com.aithings.Weigher
@@ -21,6 +19,7 @@ object WeightUtil {
val weightFuncMap: ConcurrentHashMap<String, WeightCallback?> = ConcurrentHashMap() val weightFuncMap: ConcurrentHashMap<String, WeightCallback?> = ConcurrentHashMap()
fun init() { fun init() {
SensorScale.isLog = true
Weigher2.init() Weigher2.init()
Weigher2.setListener(object : WeightListenerImpl() { Weigher2.setListener(object : WeightListenerImpl() {
override fun onInit(connect: Boolean) { override fun onInit(connect: Boolean) {
@@ -28,6 +27,16 @@ object WeightUtil {
isConnected = connect isConnected = connect
} }
override fun onTare() {
super.onTare()
Log.d(TAG, "tareTwo,onTare 回调触发")
}
override fun onZero() {
super.onZero()
Log.d(TAG, "tareTwo,onZero 回调触发")
}
override fun onGetWeight(address: Int, state: Int, weight: Double) { override fun onGetWeight(address: Int, state: Int, weight: Double) {
super.onGetWeight(address, state, weight) super.onGetWeight(address, state, weight)
val stateStr = when (state) { val stateStr = when (state) {
@@ -69,16 +78,12 @@ object WeightUtil {
fun tareTwo(address: Int) { fun tareTwo(address: Int) {
try { try {
//Weigher2.tareTwo(address) Log.d(TAG, "tareTwo,执行清零操作: address=$address")
// SDK 内部自动管理总线,直接发送清零命令
Weigher2.zeroTwo(address) Weigher2.zeroTwo(address)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} Log.e(TAG, "tareTwo,执行清零异常: address=$address, error=${e.message}")
}
private fun runOnUiThread(action: () -> Unit) {
Handler(Looper.getMainLooper()).post {
action()
} }
} }
@@ -100,7 +105,7 @@ open class WeightListenerImpl : Weigher2.Listener {
} }
override fun onZero() { override fun onZero() {
Log.d("WeightUtil", "zero ok") Log.d("WeightUtil", "tareTwo,zero ok")
} }
override fun onTare() { override fun onTare() {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

@@ -58,11 +58,12 @@
android:textSize="32sp" /> android:textSize="32sp" />
<!-- 更换食材 --> <!-- 更换食材 -->
<androidx.appcompat.widget.AppCompatButton <TextView
android:id="@+id/btnChangeFood" android:id="@+id/btnChangeFood"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="90dp" android:layout_height="90dp"
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
android:gravity="center"
android:background="@drawable/shape_green_bg" android:background="@drawable/shape_green_bg"
android:text="更换食材" android:text="更换食材"
android:textColor="@color/white" android:textColor="@color/white"
@@ -70,12 +71,13 @@
android:textStyle="bold" /> android:textStyle="bold" />
<!-- 取消 --> <!-- 取消 -->
<androidx.appcompat.widget.AppCompatButton <TextView
android:id="@+id/btnCancel" android:id="@+id/btnCancel"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="90dp" android:layout_height="90dp"
android:layout_marginTop="30dp" android:layout_marginTop="30dp"
android:layout_marginBottom="30dp" android:layout_marginBottom="30dp"
android:gravity="center"
android:background="@drawable/shape_white_12_corners" android:background="@drawable/shape_white_12_corners"
android:text="取消" android:text="取消"
android:textColor="@color/black333" android:textColor="@color/black333"
+27 -200
View File
@@ -7,214 +7,52 @@
android:orientation="vertical" android:orientation="vertical"
tools:background="@drawable/bg_other_page"> tools:background="@drawable/bg_other_page">
<TextView
android:id="@+id/tvDishName"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_marginStart="30dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="30dp"
android:background="@drawable/shape_white_30_corners"
android:gravity="center"
android:textColor="@color/black"
android:textSize="40sp"
android:textStyle="bold"
tools:text="雪菜烧豆腐" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="30dp"
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="109dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<!-- <TextView-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_weight="1"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginStart="30dp"-->
<!-- android:text="食材名称"-->
<!-- android:textColor="@color/black666"-->
<!-- android:textSize="28sp" />-->
<TextView
android:id="@+id/tvDishShowName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:textColor="@color/black"
android:textSize="30sp"
android:textColorHint="@color/black666"
android:hint="食材名称"
tools:text="" />
<TextView
android:id="@+id/tvDishType"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:textColor="@color/black999"
android:textSize="26sp"
android:layout_marginEnd="30dp"
tools:text="" />
</LinearLayout>
<com.google.android.material.divider.MaterialDivider
android:id="@+id/dividerDishType"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
app:dividerColor="@color/gray_eb"
android:visibility="gone"/>
<LinearLayout
android:id="@+id/llDishType"
android:layout_width="match_parent"
android:layout_height="113dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:text="@string/dish_from_type"
android:textColor="@color/black666"
android:textSize="28sp" />
<RadioGroup
android:id="@+id/rgCook"
android:layout_width="match_parent"
android:layout_height="80dp"
android:gravity="center_vertical|end"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rbDishTypeFirst"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="30dp"
android:background="@color/white"
android:button="@null"
android:drawableStart="@drawable/selector_dish_type"
android:drawablePadding="30dp"
android:gravity="center"
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:text="@string/dish_type_first"
android:textColor="@color/dish_type_font"
android:textSize="30sp"
android:textStyle="bold" />
<RadioButton
android:id="@+id/rbDishTypeSecond"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="30dp"
android:background="@color/white"
android:button="@null"
android:checked="false"
android:drawableStart="@drawable/selector_dish_type"
android:drawablePadding="30dp"
android:gravity="center"
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:text="@string/dish_type_second"
android:textColor="@color/dish_type_font"
android:textSize="30sp"
android:textStyle="bold" />
</RadioGroup>
</LinearLayout>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
app:dividerColor="@color/gray_eb" />
<LinearLayout <LinearLayout
android:id="@+id/llSearchBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="100dp" android:layout_height="100dp"
android:layout_marginTop="40dp" android:layout_margin="30dp"
android:background="@drawable/shape_white_30_corners"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <EditText
android:layout_width="wrap_content" android:id="@+id/etInputDish"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:text="@string/dish_part_weight_remind"
android:textColor="@color/black666"
android:textSize="26sp" />
<TextView
android:id="@+id/tvDishPartWeight"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="80dp"
android:layout_marginStart="30dp" android:layout_marginHorizontal="28dp"
android:layout_weight="1" android:layout_weight="1"
android:autofillHints="" android:autofillHints=""
android:background="@drawable/shape_white_f6_10_corners" android:background="@color/white"
android:gravity="center" android:gravity="center"
android:hint="-" android:hint="@string/dish_search_hint"
android:maxLines="1" android:inputType="text"
android:paddingStart="20dp" android:imeOptions="actionSearch"
android:paddingEnd="20dp" android:paddingStart="3dp"
android:text="" android:paddingEnd="3dp"
android:textColor="@color/dish_green" android:textColor="@color/black333"
android:textColorHint="@color/gray_d6" android:textColorHint="@color/gray_c8"
android:textSize="60sp" android:textSize="40sp"
tools:ignore="HardcodedText" /> tools:ignore="TextFields" />
<ImageView <ImageView
android:id="@+id/ivWeightClear" android:id="@+id/ivDishSearch"
android:layout_width="90dp" android:layout_width="60dp"
android:layout_height="90dp" android:layout_height="60dp"
android:layout_marginStart="30dp" android:layout_marginStart="20dp"
android:src="@drawable/ic_weight_clear" android:layout_marginEnd="20dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:visibility="gone"
android:src="@drawable/ic_search_gray"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/ivWeightAdd"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:src="@drawable/ic_weight_add"
tools:ignore="ContentDescription" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="39dp"
android:layout_marginBottom="39dp"
android:text="@string/dish_weight_set_remind"
android:textColor="@color/black999"
android:textSize="26sp" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginStart="30dp" android:layout_marginHorizontal="30dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="30dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/shape_white_30_corners" android:background="@drawable/shape_white_30_corners"
android:orientation="vertical"> android:orientation="vertical">
@@ -237,17 +75,6 @@
android:textColor="@color/black666" android:textColor="@color/black666"
android:textSize="28sp" /> android:textSize="28sp" />
<TextView
android:id="@+id/tvAddFood"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:drawablePadding="16dp"
android:padding="10dp"
android:text="@string/add_food"
android:textColor="@color/dish_green"
android:textSize="26sp"
app:drawableEndCompat="@drawable/ic_food_add" />
</FrameLayout> </FrameLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
@@ -59,7 +59,7 @@
android:id="@+id/ivClearIcon" android:id="@+id/ivClearIcon"
android:layout_width="80dp" android:layout_width="80dp"
android:layout_height="60dp" android:layout_height="60dp"
android:src="@drawable/ic_dish_clear" android:src="@drawable/ic_delete"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@@ -18,4 +18,18 @@
android:ellipsize="end" android:ellipsize="end"
android:background="@drawable/shape_white_12_corners"/> android:background="@drawable/shape_white_12_corners"/>
</LinearLayout> <TextView
android:id="@+id/tvFoodScore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black666"
android:textSize="16sp"
android:layout_marginBottom="20dp"
android:layout_gravity="bottom|center_horizontal"
tools:text="100%"
android:maxLines="1"
android:ellipsize="end"
android:visibility="gone"/>
</FrameLayout>