feat(layout): 更新主秤监控界面布局并启用PyTorch模型功能

- 将主秤监控界面从LinearLayout改为ConstraintLayout布局
- 添加返回按钮并实现点击关闭页面功能
- 在BaseApp中初始化ObjectBox数据库
- 启用PyTorch模型依赖库并移除注释代码
- 在CollectFragment中激活食物图像向量化功能
- 初始化FoodModule中的PyTorch模型
- 临时忽略assets下的.pt模型文件到.gitignore
This commit is contained in:
2026-04-13 15:11:47 +08:00
parent 0d0fcd24b7
commit 41036cdbf9
8 changed files with 363 additions and 337 deletions
+2
View File
@@ -11,3 +11,5 @@ local.properties
.claude/ .claude/
app/debug/ app/debug/
app/release/ app/release/
# 临时忽略 assets 下的 .pt 模型文件,正式文件就绪后取消此行
app/src/main/assets/*.pt
+2 -2
View File
@@ -137,8 +137,8 @@ dependencies {
implementation(libs.androidx.camera.extensions) implementation(libs.androidx.camera.extensions)
//pytorch //pytorch
// implementation (libs.pytorch.android) implementation (libs.pytorch.android)
// implementation (libs.pytorch.android.torchvision) implementation (libs.pytorch.android.torchvision)
// objectbox // objectbox
if (isDebug) { if (isDebug) {
@@ -8,6 +8,7 @@ import android.content.SharedPreferences
import android.util.Log import android.util.Log
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
import com.shuwei.dish.match.db.DatabaseProvider import com.shuwei.dish.match.db.DatabaseProvider
import com.shuwei.dish.match.objbox.ObjectBox
import com.shuwei.dish.match.scale.ScaleServiceManager import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.ui.InitActivity.Companion.TAG import com.shuwei.dish.match.ui.InitActivity.Companion.TAG
import com.shuwei.dish.match.utils.AppUtil import com.shuwei.dish.match.utils.AppUtil
@@ -44,7 +45,7 @@ class BaseApp : Application() {
// GlobalData.deviceId = "39a7abdd06b3c7ab" // GlobalData.deviceId = "39a7abdd06b3c7ab"
val filter = IntentFilter(Intent.ACTION_BOOT_COMPLETED) val filter = IntentFilter(Intent.ACTION_BOOT_COMPLETED)
registerReceiver(BootReceiver(), filter) registerReceiver(BootReceiver(), filter)
// ObjectBox.init(this) ObjectBox.init(this)
} }
override fun onTerminate() { override fun onTerminate() {
@@ -1,308 +1,308 @@
//package com.shuwei.dish.match.objbox package com.shuwei.dish.match.objbox
//
//import android.content.Context import android.content.Context
//import android.graphics.Bitmap import android.graphics.Bitmap
//import android.util.Log import android.util.Log
//import androidx.core.graphics.scale import androidx.core.graphics.scale
//import com.google.gson.Gson import com.google.gson.Gson
//import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
//import com.shuwei.dish.match.utils.AssetsTool import com.shuwei.dish.match.utils.AssetsTool
//import com.shuwei.dish.match.utils.LogSaveUtil import com.shuwei.dish.match.utils.LogSaveUtil
//import com.shuwei.dish.match.utils.ext.toJsonString import com.shuwei.dish.match.utils.ext.toJsonString
//import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
//import org.pytorch.IValue import org.pytorch.IValue
//import org.pytorch.Module import org.pytorch.Module
//import org.pytorch.torchvision.TensorImageUtils import org.pytorch.torchvision.TensorImageUtils
//import java.io.File import java.io.File
//import java.io.FileOutputStream import java.io.FileOutputStream
//import java.io.IOException import java.io.IOException
//import java.io.InputStream import java.io.InputStream
//
//
//object FoodModule { object FoodModule {
//
// private lateinit var module_mobile: Module private lateinit var module_mobile: Module
//
// private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f) private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
// private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f) private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
//
// // 1. 定义你的模型固定输入尺寸 (根据你的tflite模型修改,比如224x224) // 1. 定义你的模型固定输入尺寸 (根据你的tflite模型修改,比如224x224)
// private const val MODEL_INPUT_WIDTH = 300 private const val MODEL_INPUT_WIDTH = 300
// private const val MODEL_INPUT_HEIGHT = 300 private const val MODEL_INPUT_HEIGHT = 300
// const val DEFAULT_FOOD_INDEX = -1 const val DEFAULT_FOOD_INDEX = -1
// const val DEFAULT_QUERY_COUNT = 50 const val DEFAULT_QUERY_COUNT = 50
//
// const val BAG_RATE = 0.05 const val BAG_RATE = 0.05
//
// suspend fun init(context: Context) { suspend fun init(context: Context) {
// withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt")) module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
// initDefFoodData(context) initDefFoodData(context)
// } }
// } }
//
//// fun uri2FloatArray(uri: Uri): FloatArray? { // fun uri2FloatArray(uri: Uri): FloatArray? {
//// return MyApp.instance?.let { context -> // return MyApp.instance?.let { context ->
//// ImageUtil.uriToBitmap(context, uri)?.let { // ImageUtil.uriToBitmap(context, uri)?.let {
//// bitmap2FloatArray(it) // bitmap2FloatArray(it)
//// }
//// }
//// }
//
// fun bitmap2FloatArray(originBitmap: Bitmap, isRecycle: Boolean): FloatArray? {
// var rgb565Bitmap: Bitmap? = null
// try {
// val scaledBitmap = originBitmap.scale(MODEL_INPUT_WIDTH, MODEL_INPUT_HEIGHT)
// if (isRecycle) {
// originBitmap.recycle()
// } // }
// rgb565Bitmap = scaledBitmap.copy(Bitmap.Config.RGB_565, false)
// scaledBitmap.recycle()
// val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
// rgb565Bitmap,
// NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB
// NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB
// )
// val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor()
// return outputTensor.dataAsFloatArray
// } catch (e: OutOfMemoryError) {
// e.printStackTrace()
// logInfo("bitmap2FloatArray异常:${e.message}")
// } finally {
// if (rgb565Bitmap != null && rgb565Bitmap.isRecycled.not()) {
// rgb565Bitmap.recycle()
// }
// //System.gc()
// //System.runFinalization()
// }
// return null
// }
//
//// fun queryFood(uri: Uri, queryCount: Int = DEFAULT_QUERY_COUNT): List<String>? {
//// return uri2FloatArray(uri)?.let {
//// queryFood(it, queryCount)
//// }
//// }
//
//// /**
//// * 返回识别物品名称列表
//// */
//// fun queryFood(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
//// val floatArray = bitmap2FloatArray(bitmap)
//// return queryFood(floatArray, queryCount)
//// }
//
//// /**
//// * 返回识别物品IdNameScore对象列表
//// */
//// fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
//// val floatArray = bitmap2FloatArray(bitmap)
//// return queryFoodNameScore(floatArray, queryCount)
//// }
//
// suspend fun queryFoodNameScore(
// floatArray: FloatArray?,
// queryCount: Int = DEFAULT_QUERY_COUNT
// ): List<IdNameScore> {
// if (floatArray == null) return emptyList()
//// val query: Query<Food> = box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
//// //查询比较分数
////// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
//// var idScoreList: List<IdWithScore>
//// try {
//// idScoreList = query.findIdsWithScores();
//// } finally {
//// // 先关闭Query,释放Cursor
//// query.close()
//// }
//
// logInfo("queryFood向量:${floatArray.slice(0 until 50).toJsonString()}")
// val idScoreList = ObjectBox.query(floatArray, queryCount)
// // 批量加载所有实体,一次DB操作替代循环单条查询
// val foodMap = ObjectBox.getByIds(idScoreList.map { it.id }).associateBy { it.id }
// val nameScoreList = mutableListOf<IdNameScore>()
// idScoreList.forEach {
// val name = foodMap[it.id]?.name ?: ""
// nameScoreList.add(IdNameScore(id = it.id, name = name, score = it.score))
// }
// logInfo("queryFood数据:${nameScoreList.toJsonString()}")
// return nameScoreList
// }
//
// suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
// val floatArray = bitmap2FloatArray(bitmap, false) ?: return emptyList()
// logInfo("getFoodScoreList向量:${floatArray.toJsonString()}")
// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
// if (nameScoreList.isEmpty()) {
// return emptyList()
// }
// //暂时不过滤
// val maxScoreList = nameScoreList
// //.filter { it.score < 0.15 }
// .groupBy { it.name }
// .map { (_, value) -> value.minByOrNull { it.score }!! }
// .toMutableList()
//// val map = mutableMapOf<String, Int>()
//// nameScoreList.forEach {
//// val key = it.name
//// val count = map[key] ?: 0
//// map[key] = count + 1
//// }
//// val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList()
//// val firstFood = nameScoreList[0].name
//// orderList.remove(firstFood)
//// orderList.add(0, firstFood)
////
//// val sortedScoreList = maxScoreList.sortedWith(compareBy {
//// orderList.indexOf(it.name)
//// })
// val sortedScoreList = maxScoreList.sortedBy { it.score }
// logInfo("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
// return sortedScoreList
// }
//
//// suspend fun queryFood(floatArray: FloatArray, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
//// val map = mutableMapOf<String, Int>()
//// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
//// nameScoreList.filter { it.score < 0.05 }.forEach {
//// val count = map[it.name] ?: 0
//// map[it.name] = count + 1
//// }
//// val list = map.entries.sortedByDescending { it.value }.map { it.key }
//// return list
//// }
//
// data class IdNameScore(
// val id: Long,
// var name: String,
// val score: Double
// )
//
// suspend fun initDefFoodData(context: Context, action: () -> Unit = {}) {
// try {
// val count = ObjectBox.countIndexField(DEFAULT_FOOD_INDEX)
// if (count > 0) {
// return
// }
// val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json")
// val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json")
// val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json")
//
// val embeddingsList: List<List<Float>> =
// Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
// val labelsList: IntArray = Gson().fromJson(labelsJson, IntArray::class.java)
// val classInfo: FoodClassInfo =
// Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
//
// val foodMap = classInfo.idx_to_class
// val list = mutableListOf<Food>()
// val size = embeddingsList.size
// val size2 = labelsList.size
// val size3 = foodMap.size
// logInfo("initDefFoodData: $size,$size2,$size3")
// embeddingsList.forEachIndexed { index, floatList ->
// val classIdx = labelsList[index]
// val foodName = foodMap["$classIdx"]
// val array = floatList.toFloatArray()
// // ObjectBox.boxStore.runInTx {
// // box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
// // }
// val food = Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX)
// list.add(food)
// }
//// val tempList = list.chunked(10)
// ObjectBox.putAll(list)
// action()
// } catch (e: Exception) {
// e.printStackTrace()
// logInfo("initDefFoodData: ---${e.message}--")
// } // }
// } // }
//
// fun bitmap2FloatArray(originBitmap: Bitmap, isRecycle: Boolean): FloatArray? {
var rgb565Bitmap: Bitmap? = null
try {
val scaledBitmap = originBitmap.scale(MODEL_INPUT_WIDTH, MODEL_INPUT_HEIGHT)
if (isRecycle) {
originBitmap.recycle()
}
rgb565Bitmap = scaledBitmap.copy(Bitmap.Config.RGB_565, false)
scaledBitmap.recycle()
val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
rgb565Bitmap,
NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB
NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB
)
val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor()
return outputTensor.dataAsFloatArray
} catch (e: OutOfMemoryError) {
e.printStackTrace()
logInfo("bitmap2FloatArray异常:${e.message}")
} finally {
if (rgb565Bitmap != null && rgb565Bitmap.isRecycled.not()) {
rgb565Bitmap.recycle()
}
//System.gc()
//System.runFinalization()
}
return null
}
// fun queryFood(uri: Uri, queryCount: Int = DEFAULT_QUERY_COUNT): List<String>? {
// return uri2FloatArray(uri)?.let {
// queryFood(it, queryCount)
// }
// }
// /** // /**
// * ,此方法的主要目的是:从assets 拷贝到 app的cache目录 // * 返回识别物品名称列表
// * @param context
// * @param fileName
// * @return 例如是这样:/data/user/0/com.frizzle.pluginhookandroid9/cache/plugin-debug.apk
// *
// * 不可能反正SD
// */ // */
//// fun copyAssetToCache(context: Context, fileName: String): String? { // fun queryFood(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
//// // 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦 // val floatArray = bitmap2FloatArray(bitmap)
//// val cacheDir = context.getCacheDir() // return queryFood(floatArray, queryCount)
//// if (!cacheDir.exists()) { // }
//// cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建
//// } // /**
//// val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置 // * 返回识别物品IdNameScore对象列表
//// if (outPath.exists()) { // */
//// outPath.delete() // TODO 如果该文件已经存在,就删掉 // fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
//// } // val floatArray = bitmap2FloatArray(bitmap)
//// var `is`: InputStream? = null // 读取 // return queryFoodNameScore(floatArray, queryCount)
//// var fos: FileOutputStream? = null // 写入 // }
//// try {
//// // 创建文件,如果创建成功,就返回true suspend fun queryFoodNameScore(
//// val res = outPath.createNewFile() floatArray: FloatArray?,
//// if (res) { queryCount: Int = DEFAULT_QUERY_COUNT
//// `is` = context.getAssets().open(fileName) // 拿到main/assets目录的输入流,用于读取字节 ): List<IdNameScore> {
//// fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath if (floatArray == null) return emptyList()
//// val buf = ByteArray(`is`.available()) // 缓存区 // val query: Query<Food> = box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
//// var byteCount: Int // //查询比较分数
//// //// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
//// // 开始循环读取 // var idScoreList: List<IdWithScore>
//// while ((`is`.read(buf).also { byteCount = it }) != -1) {
//// fos.write(buf, 0, byteCount)
//// }
//// return outPath.getAbsolutePath()
//// }
//// } catch (e: IOException) {
//// e.printStackTrace()
//// } finally {
//// try {
//// // TODO 一定要记得关闭资源,为了不去性能的磨损
//// fos?.flush()
//// `is`?.close()
//// fos?.close()
//// } catch (e: IOException) {
//// e.printStackTrace()
//// }
//// }
//// return null
//// }
//
// fun copyAssetToCache(context: Context, fileName: String): String? {
// val cacheFile = File(context.cacheDir, fileName)
// val buffer = ByteArray(8 * 1024)
// var inputStream: InputStream? = null
// var outputStream: FileOutputStream? = null
//
// try { // try {
// inputStream = context.assets.open(fileName) // idScoreList = query.findIdsWithScores();
// outputStream = FileOutputStream(cacheFile) // } finally {
// var byteCount: Int // // 先关闭Query,释放Cursor
// while (inputStream.read(buffer).also { byteCount = it } != -1) { // query.close()
// outputStream.write(buffer, 0, byteCount) // }
logInfo("queryFood向量:${floatArray.slice(0 until 50).toJsonString()}")
val idScoreList = ObjectBox.query(floatArray, queryCount)
// 批量加载所有实体,一次DB操作替代循环单条查询
val foodMap = ObjectBox.getByIds(idScoreList.map { it.id }).associateBy { it.id }
val nameScoreList = mutableListOf<IdNameScore>()
idScoreList.forEach {
val name = foodMap[it.id]?.name ?: ""
nameScoreList.add(IdNameScore(id = it.id, name = name, score = it.score))
}
logInfo("queryFood数据:${nameScoreList.toJsonString()}")
return nameScoreList
}
suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
val floatArray = bitmap2FloatArray(bitmap, false) ?: return emptyList()
logInfo("getFoodScoreList向量:${floatArray.toJsonString()}")
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
if (nameScoreList.isEmpty()) {
return emptyList()
}
//暂时不过滤
val maxScoreList = nameScoreList
//.filter { it.score < 0.15 }
.groupBy { it.name }
.map { (_, value) -> value.minByOrNull { it.score }!! }
.toMutableList()
// val map = mutableMapOf<String, Int>()
// nameScoreList.forEach {
// val key = it.name
// val count = map[key] ?: 0
// map[key] = count + 1
// }
// val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList()
// val firstFood = nameScoreList[0].name
// orderList.remove(firstFood)
// orderList.add(0, firstFood)
//
// val sortedScoreList = maxScoreList.sortedWith(compareBy {
// orderList.indexOf(it.name)
// })
val sortedScoreList = maxScoreList.sortedBy { it.score }
logInfo("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
return sortedScoreList
}
// suspend fun queryFood(floatArray: FloatArray, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
// val map = mutableMapOf<String, Int>()
// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
// nameScoreList.filter { it.score < 0.05 }.forEach {
// val count = map[it.name] ?: 0
// map[it.name] = count + 1
// }
// val list = map.entries.sortedByDescending { it.value }.map { it.key }
// return list
// }
data class IdNameScore(
val id: Long,
var name: String,
val score: Double
)
suspend fun initDefFoodData(context: Context, action: () -> Unit = {}) {
try {
val count = ObjectBox.countIndexField(DEFAULT_FOOD_INDEX)
if (count > 0) {
return
}
val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json")
val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json")
val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json")
val embeddingsList: List<List<Float>> =
Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
val labelsList: IntArray = Gson().fromJson(labelsJson, IntArray::class.java)
val classInfo: FoodClassInfo =
Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
val foodMap = classInfo.idx_to_class
val list = mutableListOf<Food>()
val size = embeddingsList.size
val size2 = labelsList.size
val size3 = foodMap.size
logInfo("initDefFoodData: $size,$size2,$size3")
embeddingsList.forEachIndexed { index, floatList ->
val classIdx = labelsList[index]
val foodName = foodMap["$classIdx"]
val array = floatList.toFloatArray()
// ObjectBox.boxStore.runInTx {
// box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
// }
val food = Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX)
list.add(food)
}
// val tempList = list.chunked(10)
ObjectBox.putAll(list)
action()
} catch (e: Exception) {
e.printStackTrace()
logInfo("initDefFoodData: ---${e.message}--")
}
}
/**
* ,此方法的主要目的是:从assets 拷贝到 app的cache目录
* @param context
* @param fileName
* @return 例如是这样:/data/user/0/com.frizzle.pluginhookandroid9/cache/plugin-debug.apk
*
* 不可能反正SD
*/
// fun copyAssetToCache(context: Context, fileName: String): String? {
// // 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦
// val cacheDir = context.getCacheDir()
// if (!cacheDir.exists()) {
// cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建
// }
// val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置
// if (outPath.exists()) {
// outPath.delete() // TODO 如果该文件已经存在,就删掉
// }
// var `is`: InputStream? = null // 读取
// var fos: FileOutputStream? = null // 写入
// try {
// // 创建文件,如果创建成功,就返回true
// val res = outPath.createNewFile()
// if (res) {
// `is` = context.getAssets().open(fileName) // 拿到main/assets目录的输入流,用于读取字节
// fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath
// val buf = ByteArray(`is`.available()) // 缓存区
// var byteCount: Int
//
// // 开始循环读取
// while ((`is`.read(buf).also { byteCount = it }) != -1) {
// fos.write(buf, 0, byteCount)
// }
// return outPath.getAbsolutePath()
// } // }
// outputStream.channel.force(true) // 强制物理落盘,比flush更彻底
// return cacheFile.absolutePath
// } catch (e: IOException) { // } catch (e: IOException) {
// e.printStackTrace() // e.printStackTrace()
// logInfo("文件拷贝失败 fileName=$fileName, error=${e.message}")
// // 拷贝失败时删除残缺文件,避免下次读取到损坏文件
// if (cacheFile.exists()) {
// cacheFile.delete()
// }
// } finally { // } finally {
// outputStream?.close() // try {
// inputStream?.close() // // TODO 一定要记得关闭资源,为了不去性能的磨损
// fos?.flush()
// `is`?.close()
// fos?.close()
// } catch (e: IOException) {
// e.printStackTrace()
// }
// } // }
// return null // return null
// } // }
//
// private fun logInfo(msg:String) { fun copyAssetToCache(context: Context, fileName: String): String? {
// Log.d("FoodModule", msg) val cacheFile = File(context.cacheDir, fileName)
// LogSaveUtil.saveLogFile(msg) val buffer = ByteArray(8 * 1024)
// } var inputStream: InputStream? = null
// var outputStream: FileOutputStream? = null
//}
try {
inputStream = context.assets.open(fileName)
outputStream = FileOutputStream(cacheFile)
var byteCount: Int
while (inputStream.read(buffer).also { byteCount = it } != -1) {
outputStream.write(buffer, 0, byteCount)
}
outputStream.channel.force(true) // 强制物理落盘,比flush更彻底
return cacheFile.absolutePath
} catch (e: IOException) {
e.printStackTrace()
logInfo("文件拷贝失败 fileName=$fileName, error=${e.message}")
// 拷贝失败时删除残缺文件,避免下次读取到损坏文件
if (cacheFile.exists()) {
cacheFile.delete()
}
} finally {
outputStream?.close()
inputStream?.close()
}
return null
}
private fun logInfo(msg:String) {
Log.d("FoodModule", msg)
LogSaveUtil.saveLogFile(msg)
}
}
@@ -18,6 +18,7 @@ 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.databinding.ActivityInitBinding import com.shuwei.dish.match.databinding.ActivityInitBinding
import com.shuwei.dish.match.db.AppRepository import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.utils.AppUtil import com.shuwei.dish.match.utils.AppUtil
import com.shuwei.dish.match.utils.NetworkUtils import com.shuwei.dish.match.utils.NetworkUtils
import com.shuwei.dish.match.utils.SpTool import com.shuwei.dish.match.utils.SpTool
@@ -47,9 +48,9 @@ class InitActivity : BaseActivity() {
// useBackground(false) // useBackground(false)
binding = ActivityInitBinding.inflate(layoutInflater) binding = ActivityInitBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
// lifecycleScope.launch { lifecycleScope.launch {
// FoodModule.init(this@InitActivity) FoodModule.init(this@InitActivity)
// } }
// setHeaderBackground(isHomePage = true) // setHeaderBackground(isHomePage = true)
setHeaderBgVisible(false) setHeaderBgVisible(false)
BaseApp.appVersion = AppUtil.getAppVersionCode(this).toString() BaseApp.appVersion = AppUtil.getAppVersionCode(this).toString()
@@ -52,6 +52,7 @@ class MasterScaleActivity : BaseActivity() {
setContentView(binding.root) setContentView(binding.root)
setHeaderBackground() setHeaderBackground()
binding.ivMasterBack.setOnClickListener { finish() }
binding.rvScaleList.layoutManager = LinearLayoutManager(this) binding.rvScaleList.layoutManager = LinearLayoutManager(this)
binding.rvScaleList.itemAnimator = null // 关闭默认动画,避免数据频繁更新时出现闪烁错乱 binding.rvScaleList.itemAnimator = null // 关闭默认动画,避免数据频繁更新时出现闪烁错乱
binding.rvScaleList.adapter = adapter binding.rvScaleList.adapter = adapter
@@ -17,6 +17,7 @@ import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.databinding.FragmentCollectBinding import com.shuwei.dish.match.databinding.FragmentCollectBinding
import com.shuwei.dish.match.dialog.Loading import com.shuwei.dish.match.dialog.Loading
import com.shuwei.dish.match.objbox.FoodCollectionBean import com.shuwei.dish.match.objbox.FoodCollectionBean
import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.ui.SettingActivity import com.shuwei.dish.match.ui.SettingActivity
import com.shuwei.dish.match.utils.BitmapSaver import com.shuwei.dish.match.utils.BitmapSaver
import com.shuwei.dish.match.utils.Debouncer import com.shuwei.dish.match.utils.Debouncer
@@ -119,38 +120,38 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
} }
private fun getImageVector(index: Int, bitmap: Bitmap) { private fun getImageVector(index: Int, bitmap: Bitmap) {
//// val bitmap = BitmapCropper.cropCenter( // val bitmap = BitmapCropper.cropCenter(
//// original = srcBmp, // original = srcBmp,
//// targetWidth = 900, targetHeight = 900, // targetWidth = 900, targetHeight = 900,
////// offsetX = 30, offsetY = 100 //// offsetX = 30, offsetY = 100
//// )
// val imageVector = try {
// FoodModule.bitmap2FloatArray(bitmap, false)
// } catch (e: Exception) {
// e.printStackTrace()
// toast("操作失败")
// log("操作失败:${e.message}")
// hideWaitingDialog()
// return
// }
// val file = BitmapSaver.saveToAppFilesDir(
// bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg"
// ) // )
// log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}") val imageVector = try {
// FoodModule.bitmap2FloatArray(bitmap, false)
// activity?.runOnUiThread { } catch (e: Exception) {
// foodCollectionList[index].let { e.printStackTrace()
// it.imageVector = imageVector toast("操作失败")
// it.bitmap = null log("操作失败:${e.message}")
// it.isShowCamera = false hideWaitingDialog()
// it.imageFile = file return
// } }
// collectionAdapter.notifyItemChanged(index) val file = BitmapSaver.saveToAppFilesDir(
// } bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg"
// if (bitmap.isRecycled.not()) { )
// bitmap.recycle() log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
// }
// hideWaitingDialog() activity?.runOnUiThread {
foodCollectionList[index].let {
it.imageVector = imageVector
it.bitmap = null
it.isShowCamera = false
it.imageFile = file
}
collectionAdapter.notifyItemChanged(index)
}
if (bitmap.isRecycled.not()) {
bitmap.recycle()
}
hideWaitingDialog()
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
@@ -1,19 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout 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="match_parent" android:layout_height="match_parent"
android:orientation="vertical" xmlns:app="http://schemas.android.com/apk/res-auto"
tools:background="@color/bg_color"> tools:background="@color/bg_color">
<!-- 顶部标题 --> <!-- 顶部标题 -->
<TextView <TextView
android:id="@+id/tvMasterTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:text="全设备秤数据监控" android:text="全设备秤数据监控"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:textColor="@color/home_title" android:textColor="@color/home_title"
android:textSize="36sp" android:textSize="36sp"
android:textStyle="bold" /> android:textStyle="bold" />
@@ -23,20 +27,36 @@
android:id="@+id/tvConnectionStatus" android:id="@+id/tvConnectionStatus"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" app:layout_constraintTop_toBottomOf="@id/tvMasterTitle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:textColor="@color/home_sub_title" android:textColor="@color/home_sub_title"
android:textSize="24sp" android:textSize="24sp"
tools:text="已发现设备:3 台" /> tools:text="已发现设备:3 台" />
<ImageView
android:id="@+id/ivMasterBack"
android:layout_width="70dp"
android:layout_height="60dp"
android:layout_gravity="start"
android:paddingStart="30dp"
android:paddingEnd="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/tvMasterTitle"
app:layout_constraintBottom_toBottomOf="@id/tvMasterTitle"
android:src="@drawable/ic_back"
tools:ignore="ContentDescription" />
<!-- 秤数据列表 --> <!-- 秤数据列表 -->
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvScaleList" android:id="@+id/rvScaleList"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" app:layout_constraintTop_toBottomOf="@id/tvConnectionStatus"
app:layout_constraintBottom_toBottomOf="parent"
android:overScrollMode="never" android:overScrollMode="never"
android:padding="16dp" android:padding="16dp"
tools:listitem="@layout/list_item_scale_data"/> tools:listitem="@layout/list_item_scale_data"/>
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>