12 Commits
Author SHA1 Message Date
马增飞 9818dfc438 结算 2025-11-19 12:05:12 +08:00
mazengfei cb9d68d2e3 功能优化 2025-11-17 16:09:01 +08:00
mazengfei b4087e435e 修改模式切换流程 2025-11-10 18:55:21 +08:00
lvmeng 4abc855b36 功能优化 2025-11-07 14:23:53 +08:00
mazengfei c898cbb3e2 修改模式切换流程 2025-11-06 19:19:45 +08:00
mazengfei 0a09814a61 修改菜品识别逻辑 2025-11-06 17:49:47 +08:00
mazengfei cc2fd2d743 修改菜品识别逻辑 2025-11-06 14:18:48 +08:00
mazengfei cd883af92f 修改菜品识别逻辑 2025-11-04 15:46:40 +08:00
mazengfei 9c60049843 修改菜品识别逻辑 2025-10-31 23:19:42 +08:00
mazengfei e47b362883 添加菜品采集 2025-10-31 09:53:51 +08:00
马增飞 ed6c5e83be 新增菜品采集 2025-10-18 15:06:07 +08:00
lvmeng a27cc5f7ee 识别图片处理、查询菜品数据 2025-10-17 14:47:28 +08:00
84 changed files with 2453 additions and 94 deletions
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>
+2 -2
View File
@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-08-05T03:50:06.075639Z">
<DropdownSelection timestamp="2025-11-07T10:55:54.677927600Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=2FD2511004054749" />
<DeviceId pluginId="Default" identifier="serial=192.168.1.56:5555;connection=2687c52a" />
</handle>
</Target>
</DropdownSelection>
+1 -1
View File
@@ -6,7 +6,7 @@
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="zulu-17" />
<option name="gradleJvm" value="azul-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
+1 -1
View File
@@ -1,6 +1,6 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="zulu-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
+4 -8
View File
@@ -57,14 +57,7 @@ android {
}
dependencies {
// implementation(
// fileTree(
// mapOf(
// "dir" to "libs",
// "include" to listOf("*.aar", "*.jar")
// )
// )
// )
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
@@ -118,6 +111,9 @@ dependencies {
val objectboxVersion = "5.0.1"
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
// releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
implementation(libs.androidx.recyclerview)
}
apply(plugin = "io.objectbox")
Binary file not shown.
+49
View File
@@ -0,0 +1,49 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:594331511073099531",
"lastPropertyId": "4:1528837175750321569",
"name": "Food",
"properties": [
{
"id": "1:2317727855243736226",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:340951913928211738",
"name": "name",
"type": 9
},
{
"id": "3:5484846939472684412",
"name": "foodIdx",
"type": 5
},
{
"id": "4:1528837175750321569",
"name": "foodVector",
"indexId": "1:6010838397086628487",
"type": 28,
"flags": 8
}
],
"relations": []
}
],
"lastEntityId": "1:594331511073099531",
"lastIndexId": "1:6010838397086628487",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [],
"retiredRelationUids": [],
"version": 1
}
+16
View File
@@ -51,6 +51,22 @@
android:name=".activity.MainActivity"
android:exported="true">
</activity>
<activity
android:name=".activity.FoodCollectionActivity"
android:exported="true">
</activity>
<activity android:name="com.sw.dualscreen.activity.CollectedDataActivity" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
Binary file not shown.
@@ -11,6 +11,7 @@ object GlobalData {
* 设备id
*/
var deviceId: String = ""
/**
* app版本号
*/
@@ -38,7 +39,7 @@ object GlobalData {
var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
var activeKey = "085F-118G-Q3AB-1WVJ"
var activeKey = "085F-118G-Q3LH-HPGZ"//2号楼"085F-118G-Q3AB-1WVJ" 5号楼"085F-118G-Q3LH-HPGZ"
}
/**
@@ -59,4 +60,5 @@ object GlobalKey {
const val KEY_TOKEN = "tokenKey"
const val KEY_FIRST_RUN = "firstRun"
const val KEY_USER_INFO = "userInfoKey"
const val KEY_PICKUP_MODE = "pickupMode"
}
@@ -9,10 +9,12 @@ import timber.log.Timber
class MyApp : App() {
companion object {
const val DEBUG: Boolean = true
var instance: MyApp?=null
}
override fun onCreate() {
super.onCreate()
instance = this
Timber.plant(Timber.DebugTree())
var deviceId = AppUtil.getUDID(this)
Timber.d("UDID = ${AppUtil.getUDID(this)}")
@@ -8,14 +8,14 @@ import android.view.View
import android.view.WindowManager
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.sw.dualscreen.R
import com.sw.dualscreen.model.IActivityResult
import com.sw.dualscreen.view.CustomDialog
import com.sw.dualscreen.viewmodel.BaseViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -81,4 +81,20 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
private var launchPermissionCallback: IActivityResult.RequestPermissionCallback? = null
fun requestSinglePermissionResult(
permission: String,
permissionCallback: IActivityResult.RequestPermissionCallback?
) {
this.launchPermissionCallback = permissionCallback
permissionLauncher.launch(permission)
}
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { result: Boolean ->
launchPermissionCallback?.callback(result)
}
}
@@ -0,0 +1,134 @@
package com.sw.dualscreen.activity
import android.annotation.SuppressLint
import androidx.activity.viewModels
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.CollectedFoodAdapter
import com.sw.dualscreen.databinding.ActivityCollectedDataBinding
import com.sw.dualscreen.databinding.LayoutEmptySearchBinding
import com.sw.dualscreen.dialog.WarnDialog
import com.sw.dualscreen.ext.addOnActionSearchListener
import com.sw.dualscreen.ext.hideKeyboard
import com.sw.dualscreen.objbox.CollectedFoodBean
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import kotlin.getValue
class CollectedDataActivity : BaseActivity<ActivityCollectedDataBinding>() {
private val viewModel by viewModels<UserViewModel>()
override fun getViewModel(): BaseViewModel {
return viewModel
}
override fun inflateViewBinding(): ActivityCollectedDataBinding {
return ActivityCollectedDataBinding.inflate(layoutInflater)
}
private val list: MutableList<CollectedFoodBean> = mutableListOf()
private val adapter by lazy {
CollectedFoodAdapter(list).apply {
isStateViewEnable = true
addOnItemChildClickListener(R.id.ivDeleteFood) { _, _, position ->
deleteGoods(position)
}
}
}
override fun initialize() {
super.initialize()
binding.rvFoodList.let {
it.layoutManager = LinearLayoutManager(this)
it.adapter = adapter
}
binding.ivBack.setOnClickListener { finish() }
binding.ivGoodsSearch.setOnClickListener {
val searchName = binding.etInputGoods.text.toString().trim()
if (searchName.isBlank()) {
ToastUtils.showToast("请输入物品名称")
return@setOnClickListener
}
getCollectGoods(searchName)
}
binding.etInputGoods.let { v ->
v.addOnActionSearchListener {
val searchName = binding.etInputGoods.text.toString().trim()
if (searchName.isBlank()) {
ToastUtils.showToast("请输入物品名称")
return@addOnActionSearchListener
}
getCollectGoods(searchName)
}
v.addTextChangedListener {
if (it.isNullOrBlank()) {
getCollectGoods()
}
}
}
getCollectGoods()
}
private var box: Box<Food>? = null
@SuppressLint("NotifyDataSetChanged")
private fun getCollectGoods(searchName:String?=null) {
if (box == null) {
box = ObjectBox.boxStore.boxFor(Food::class)
}
list.clear()
var queryList = box?.all?.distinctBy { it.name }
if (searchName.isNullOrBlank().not()) {
queryList = queryList?.filter { it.name?.contains(searchName) == true }
}
queryList?.forEach {
list.add(CollectedFoodBean(foodName = it.name))
}
adapter.notifyDataSetChanged()
if (list.isEmpty()) {
loadEmptyView()
}
binding.root.hideKeyboard()
}
private var emptyBinding: LayoutEmptySearchBinding? = null
private fun loadEmptyView() {
if (emptyBinding == null) {
emptyBinding = LayoutEmptySearchBinding.inflate(layoutInflater, binding.rvFoodList, false)
}
emptyBinding?.root?.let { layout ->
layout.setOnClickListener { layout.hideKeyboard() }
adapter.stateView = layout
}
}
private fun deleteGoods(position: Int) {
WarnDialog(
context = context,
content = "请确认是否删除菜品:${list[position].foodName}",
confirmBlock = {
Thread {
runOnUiThread {
showWaitingDialog("加载中……")
}
val name = list[position].foodName
val filterIdList = box?.all?.filter { it.name == name }?.map { it.id }
box?.removeByIds(filterIdList)
runOnUiThread {
hideWaitingDialog()
adapter.removeAt(position)
ToastUtils.showToast("删除成功")
if (list.isEmpty()) {
loadEmptyView()
}
}
}.start()
}).show()
}
}
@@ -0,0 +1,370 @@
package com.sw.dualscreen.activity
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Typeface
import android.net.Uri
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.viewModels
import androidx.camera.view.PreviewView
import androidx.core.net.toUri
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.example.utils.FloatBase64Utils
import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.FoodCollectionAdapter
import com.sw.dualscreen.adapter.GenericItemAdapter
import com.sw.dualscreen.adapter.GridSpacingItemDecoration
import com.sw.dualscreen.adapter.dpToPx
import com.sw.dualscreen.databinding.ActivityFoodCollectionBinding
import com.sw.dualscreen.databinding.ItemSearchFoodInfoBinding
import com.sw.dualscreen.databinding.LayoutCameraPreviewBinding
import com.sw.dualscreen.ext.clickWithDebounce
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.FoodCollectionBean
import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.utils.BitmapCropper
import com.sw.dualscreen.utils.BitmapSaver
import com.sw.dualscreen.utils.CameraHelper
import com.sw.dualscreen.utils.CameraUtils
import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.ImageUtil
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.launch
import org.pytorch.IValue
import org.pytorch.Module
import org.pytorch.torchvision.TensorImageUtils
import timber.log.Timber
import kotlin.math.max
class FoodCollectionActivity : BaseActivity<ActivityFoodCollectionBinding>() {
companion object {
val MAX_COUNT = 100
}
private val viewModel by viewModels<UserViewModel>()
private var selectedFoodId: String? = ""
private var selectedFoodName: String? = ""
private var box: Box<Food>? = null
private val foodCollectionList: MutableList<FoodCollectionBean> = mutableListOf()
// private lateinit var cameraHelper: CameraHelper
private lateinit var foodAdapter: GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding>
private val foodList = mutableListOf<FoodInfo>() // 适配器内部维护的数据列表
private val debouncer = Debouncer(2000)
private lateinit var previewView: PreviewView
private val cameraUtils: CameraUtils by lazy {
CameraUtils(this)
}
private val collectionAdapter: FoodCollectionAdapter by lazy {
FoodCollectionAdapter(foodCollectionList).apply {
// setOnItemClickListener { _, _, position ->
// if (list[position].isShowCamera) {
//// takePhoto()
// cameraHelper.openCamera()
// }
// }
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
// list.removeAt(position)
// collectionAdapter.notifyItemRemoved(position)
// collectionAdapter.notifyItemRangeChanged(position, list.size)
foodCollectionList[position].let {
it.bitmap = null
it.isShowCamera = true
it.isFinish = false
}
notifyItemChanged(position)
}
}
}
override fun getViewModel(): BaseViewModel {
return viewModel
}
override fun inflateViewBinding(): ActivityFoodCollectionBinding {
return ActivityFoodCollectionBinding.inflate(layoutInflater)
}
private val cameraCallback: (Uri) -> Unit = { uri ->
val index = foodCollectionList.indexOfFirst { it.bitmap == null }
if (index == -1) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
rerurn@ cameraCallback
}
ImageUtil.uriToBitmap(this, uri)?.let { bitmap ->
// val cropBitmap = BitmapCropper.cropCenter(
// original = bitmap,
// targetWidth = 1300, targetHeight = 900,
// //offsetX = 30, offsetY = 100
// )
val file = BitmapSaver.saveToAppFilesDir(
bitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
Timber.d("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
foodCollectionList[index].let {
it.bitmap = bitmap
it.isShowCamera = false
it.imageUri = file?.toUri()
}
collectionAdapter.notifyItemChanged(index)
}
}
@SuppressLint("NotifyDataSetChanged")
private fun takePhoto() {
val count = foodCollectionList.count { it.bitmap != null }
if (count >= MAX_COUNT) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
return
}
cameraUtils.takePhoto(cameraCallback)
}
override fun initialize() {
cameraUtils.initCamera()
val previewBinding =
LayoutCameraPreviewBinding.inflate(layoutInflater, binding.flCameraPreview)
previewView = previewBinding.previewView.also {
it.updateLayoutParams {
width = 180.dp
height = 180.dp
}
}
cameraUtils.setPreviewController(previewView)
// 初始化 CameraHelper
// cameraHelper = CameraHelper(
// context = this,
// caller = this,
// authority = "${packageName}.fileprovider"
// ) { uri, path ->
// //if (foodCollectionList.size < 6) {
// // val insertIndex = if (foodCollectionList.isEmpty()) 0 else foodCollectionList.size - 1
// // foodCollectionList.add(insertIndex, FoodCollectionBean(imageUri = uri))
// //} else {
// // foodCollectionList[foodCollectionList.size - 1] = FoodCollectionBean(imageUri = uri)
// //}
// //collectionAdapter.notifyDataSetChanged()
// cameraCallback(uri)
// }
binding.ivBack.setOnClickListener {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
repeat(MAX_COUNT) {
foodCollectionList.add(FoodCollectionBean(isShowCamera = true))
}
binding.rvFoodCollection.let {
it.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false)
it.adapter = collectionAdapter
}
binding.btnFoodSearch.setOnClickListener {
searchInfo()
}
binding.btnSave.setOnClickListener {
if (selectedFoodName.isNullOrBlank()) {
Toast.makeText(this, "请选择菜品名称", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
if (foodCollectionList.size <= 1) {
Toast.makeText(this, "请拍摄菜品照片", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
vectorThread()
}
binding.editFoodName.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
searchInfo()
val imm =
v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(v.windowToken, 0)
true
} else {
false
}
}
binding.btnCollectedGoods.setOnClickListener {
startActivity(Intent(this, CollectedDataActivity::class.java))
}
binding.btnTakePhoto.clickWithDebounce {
binding.btnTakePhoto.text = "拍照"
val count = foodCollectionList.count { it.bitmap != null }
if (count >= MAX_COUNT) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
return@clickWithDebounce
}
// cameraHelper.openCamera()
takePhoto()
}
binding.btnClearData.setOnClickListener { clearData() }
foodAdapter = createAdapter()
binding.recyclerview.layoutManager = GridLayoutManager(context, 2)
// 添加间距装饰(12dp)
binding.recyclerview.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = dpToPx(30),
includeEdge = false // 包含边缘间距
)
)
binding.recyclerview.adapter = foodAdapter
}
private fun vectorThread() {
//Loading.show(this)
showWaitingDialog("加载中……")
Thread {
foodCollectionList
.filter { it.bitmap != null }
.forEachIndexed { index, it ->
image2VectorTask(it, index)
}
runOnUiThread {
window.decorView.postDelayed({
//Loading.dismiss()
hideWaitingDialog()
}, 1000)
}
}.start()
}
private fun initBox() {
if (box == null) {
box = ObjectBox.boxStore.boxFor(Food::class)
}
}
private fun image2VectorTask(item: FoodCollectionBean, position: Int) {
initBox()
val imageVector = FoodModule.bitmap2FloatArray(item.bitmap!!)
val base64Str = FloatBase64Utils.floatArrayToBase64(imageVector)
Timber.tag("mzf1").e(base64Str)
// viewModel.postImageData(
// context,
// foodId = selectedFoodId.toString(),
// foodName = selectedFoodName.toString(),
// foodVector = base64Str,
// uri = item.imageUri!!
// )
box?.put(Food(name = checkedItem!!.foodName, foodIdx = 0, foodVector = imageVector))
foodCollectionList[position].let {
it.imageVector = imageVector
it.isFinish = true
}
runOnUiThread {
collectionAdapter.notifyItemChanged(position)
}
}
private fun searchInfo() {
debouncer.debounce {
viewModel.searchByFoodName(binding.editFoodName.text.toString())
}
}
override fun registerDataChange() {
super.registerDataChange()
lifecycleScope.launch {
viewModel.searchFoodInfoList.collect {
// foodList.clear()
// foodList.addAll(it)
foodAdapter.updateData(it)
}
}
}
private var checkedItem: FoodInfo? = null
private fun createAdapter(): GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding> {
return GenericItemAdapter(
items = emptyList(),
bindingInflater = ItemSearchFoodInfoBinding::inflate,
bindCallback = { item, position ->
this.tvName.text = item.foodName
if (item.id != checkedItem?.id) {
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.NORMAL)
this.tvName.setTextColor(resources.getColor(R.color.search_normal))
this.llRoot.setBackgroundResource(R.drawable.grid_search_item_normal)
} else {
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.BOLD)
this.tvName.setTextColor(resources.getColor(R.color.search_checked))
this.llRoot.setBackgroundResource(R.drawable.grid_search_item_checked)
}
this.llRoot.setOnClickListener {
Timber.d("itemClick ${item.foodName}, position = $position")
checkedItem = item
// viewModel.updateCurrentItem(item)
// itemClickCallback(item)
selectedFoodName = item.foodName
selectedFoodId = item.id
foodAdapter.notifyDataSetChanged()
}
}
)
}
private var clickIndex = -1
@SuppressLint("NotifyDataSetChanged")
private fun clearData() {
foodCollectionList.forEach {
it.bitmap = null
it.isShowCamera = true
it.isFinish = false
}
collectionAdapter.notifyDataSetChanged()
clickIndex = -1
binding.editFoodName.setText("")
// foodList.clear()
// foodAdapter.updateData(mutableListOf())
// foodAdapter.notifyDataSetChanged()
//loadEmptyView()
}
override fun onResume() {
super.onResume()
cameraUtils.bind()
binding.llCameraFlag.run {
visibility = View.VISIBLE
postDelayed({
visibility = View.GONE
}, 3000)
}
}
override fun onPause() {
super.onPause()
cameraUtils.unbind()
binding.llCameraFlag.visibility = View.VISIBLE
}
}
@@ -1,6 +1,7 @@
package com.sw.dualscreen.activity
import android.content.Context
import android.content.Intent
import android.graphics.Outline
import android.graphics.Typeface
import android.hardware.display.DisplayManager
@@ -22,6 +23,8 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.google.common.util.concurrent.ListenableFuture
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.GenericItemAdapter
import com.sw.dualscreen.adapter.GridSpacingItemDecoration
@@ -29,23 +32,31 @@ import com.sw.dualscreen.adapter.dpToPx
import com.sw.dualscreen.databinding.ActivityMainBinding
import com.sw.dualscreen.databinding.ItemFoodInfoBinding
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.ext.format2String
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.FoodModule.IdNameScore
import com.sw.dualscreen.presentation.SecondaryScreenPresentation
import com.sw.dualscreen.sdk.SensorScaleUtils
import com.sw.dualscreen.utils.AssetsTool
import com.sw.dualscreen.utils.BitmapCropper
import com.sw.dualscreen.utils.BitmapSaver
import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.GlideUtils
import com.sw.dualscreen.utils.GsonUtils
import com.sw.dualscreen.utils.ImageUtil
import com.sw.dualscreen.utils.SPUtil
import com.sw.dualscreen.view.CustomBottomSheetDialog
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.LightManager
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
import java.util.concurrent.Executors
import kotlin.math.roundToInt
/**
* 服务员显示界面
@@ -65,6 +76,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
private var lastWeight: Double = 0.0
private var lastPhotoUri: Uri? = null // 最后拍照的图片
private var debouncer = Debouncer(2000)
private var isRecognitionFood = true
override fun getViewModel(): BaseViewModel {
return viewModel
@@ -90,6 +102,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
private fun initView() {
viewModel.activeEngine()
binding.foodDisplayLayout.setOnClickListener {
val intent = Intent(this, FoodCollectionActivity::class.java)
startActivity(intent)
finish()
}
binding.previewView.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
@@ -110,7 +127,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
arrayOf("即放即取", "余量取餐")
)
binding.spinner.adapter = spinnerAdapter
binding.spinner.setSelection(0)
val mode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0)
binding.spinner.setSelection(mode ?: 0)
presentation?.updateMealPickupMode(mode ?: 0)
binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
@@ -118,7 +137,12 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
position: Int,
id: Long
) {
SPUtil.getInstance().put(GlobalKey.KEY_PICKUP_MODE, position)
updateCurrentFood(null)
isRecognitionFood = true
presentation?.updateMealPickupMode(position)
SensorScaleUtils.zero()
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
@@ -137,6 +161,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
binding.tvFoodName.setOnClickListener {
SensorScaleUtils.zero()
}
binding.tvRescan.setOnClickListener {
debouncer.debounce { recognizeFood() }
}
}
fun updateCurrentFood(foodInfo: FoodInfo?) {
@@ -170,6 +197,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun registerDataChange() {
super.registerDataChange()
// lifecycleScope.launch {
// viewModel.identifiedFoodInfoList2.collect { list ->
// updateFoodInfo(list)
// }
// }
lifecycleScope.launch {
viewModel.identifiedFoodInfoList.collect { list ->
adapter.updateData(list)
@@ -185,31 +217,84 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
Timber.d("registerDataChange weight = $weight")
presentation?.updateWeight(weight)
if (weight - lastWeight > 0.1 && presentation?.mealPickupMode == 0) { // 大于100g
if (weight <= 0.005) {//余量取餐,检测到秤上没有东西,重新启动识别菜品 && presentation?.mealPickupMode == 1
isRecognitionFood = true
}
if (weight - lastWeight > 0.05 && isRecognitionFood) { // 大于50g
if (presentation?.mealPickupMode == 1) {
isRecognitionFood = false
}
debouncer.debounce {
recognizeFood()
}
}
lastWeight = weight
}
}
private fun recognizeFood() {
LightManager.openRedLight()
LightManager.openGreenLight()
takePhoto { photoUri ->
lastPhotoUri = photoUri
Timber.d("registerDataChange photoUri = ${photoUri.path}")
// viewModel.getIdentifiedFoodList()
AssetsTool.loadImageBitmapFromAssets(this, "data/20251016-091510.png") { bitmap ->
val resultList = FoodModule.queryFood(bitmap)
val name = resultList.joinToString(separator = ",") { it.name.toString() }
Timber.d("registerDataChange 固定图片识别数据名称:$name")
}
ImageUtil.uriToBitmap(this, photoUri)?.let { bitmap ->
Timber.d("registerDataChange photoUri 拿到bitmap")
val resultList = FoodModule.queryFood(bitmap)
Timber.d("registerDataChange photoUri 拿到识别数据")
val name = resultList.joinToString(separator = ",") { it.name.toString() }
Timber.d("registerDataChange photoUri 识别数据名称:$name")
// Toast.makeText(this, "${res.name}|${res.foodIdx}|${res.score}", Toast.LENGTH_LONG).show()
// val bmp = BitmapCropper.cropCenter(bitmap, 1300, 900)
Timber.d("registerDataChange photoUri bitmap裁剪完成")
val file = BitmapSaver.saveToAppFilesDir(
bitmap,
this,
"IMG_CROP_${System.currentTimeMillis()}.jpg"
)
Timber.d("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}")
//val nameList = FoodModule.queryFood(bitmap)
val scoreList = FoodModule.getFoodScoreList(bitmap)
Timber.d(
"registerDataChange photoUri 拿到识别数据:${
GsonUtils.toJson(
scoreList
)
}"
)
val nameList = scoreList.map { it.name }
val foodName = nameList.joinToString(separator = ",")
Timber.d("registerDataChange photoUri 识别数据名称:$foodName")
if (TextUtils.isEmpty(foodName)) {
LightManager.closeGreenLight()
LightManager.closeRedLight()
binding.layoutRescan.visibility = View.VISIBLE
} else {
binding.layoutRescan.visibility = View.GONE
viewModel.getFoodInfo(foodName) { list ->
runOnUiThread {
updateFoodInfo(list, scoreList)
}
}
}
}
lastWeight = weight
}
}
private fun updateFoodInfo(list: List<FoodInfo>, scoreList: List<IdNameScore>) {
Timber.d("registerDataChange识别后查询接口数据:${GsonUtils.toJson(list)}")
list.forEach { foodInfo ->
val scoreItem = scoreList.firstOrNull { it.name == foodInfo.foodName }
val score = scoreItem?.score ?: 0.0
foodInfo.score = ((1 - score) * 10000).roundToInt()
}
adapter.updateData(list)
if (list.isNotEmpty()) {
checkedItem = list[0]
checkedItem!!.photoUri = lastPhotoUri
updateCurrentFood(checkedItem)
lastPhotoUri = null
LightManager.closeRedLight()
}
}
@@ -218,7 +303,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
items = emptyList(),
bindingInflater = ItemFoodInfoBinding::inflate,
bindCallback = { item, position ->
this.tvName.text = item.foodName
val score = (item.score / 100.0).format2String(2)
this.tvName.text = "${item.foodName}\n${score}%"
if (item.id != checkedItem?.id) {
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.NORMAL)
this.tvName.setTextColor(resources.getColor(R.color.identify_normal))
@@ -399,6 +485,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
)
presentation!!.setStepChangeCallback { step ->
if (step == 1) {
Timber.d("setStepChangeCallback${step}")
updateCurrentFood(null)
bottomSheetDialog?.dismiss()
}
@@ -409,6 +496,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
}
}
fun clearFoodList() {
adapter.updateData(listOf())
adapter.notifyDataSetChanged()
}
override fun onDestroy() {
SensorScaleUtils.closeScale()
presentation?.dismiss()
@@ -0,0 +1,34 @@
package com.sw.dualscreen.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.ListItemCollectedDataBinding
import com.sw.dualscreen.databinding.ListItemFoodCollectionBinding
import com.sw.dualscreen.objbox.CollectedFoodBean
import com.sw.dualscreen.objbox.FoodCollectionBean
class CollectedFoodAdapter (var list: MutableList<CollectedFoodBean>) :
BaseQuickAdapter<CollectedFoodBean, CollectedFoodAdapter.VH>(list) {
inner class VH(var binding: ListItemCollectedDataBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemCollectedDataBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodBean?) {
holder.binding.tvFoodName.text = item?.foodName
holder.binding.divider.run {
visibility = if (position == list.size - 1) View.GONE else View.VISIBLE
}
}
}
@@ -0,0 +1,43 @@
package com.sw.dualscreen.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.R
import com.sw.dualscreen.databinding.ListItemFoodCollectionBinding
import com.sw.dualscreen.objbox.FoodCollectionBean
class FoodCollectionAdapter (var list: MutableList<FoodCollectionBean>) :
BaseQuickAdapter<FoodCollectionBean, FoodCollectionAdapter.VH>(list) {
inner class VH(var binding: ListItemFoodCollectionBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemFoodCollectionBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: FoodCollectionBean?) {
val binding = holder.binding
item?.let {
binding.ivFinish.visibility = if (it.isFinish) View.VISIBLE else View.GONE
binding.ivDelete.visibility = if (it.isShowCamera) View.GONE else View.VISIBLE
binding.imageView.run {
if (it.isShowCamera) {
scaleType = ImageView.ScaleType.CENTER
setImageResource(R.drawable.ic_camera256)
} else {
scaleType = ImageView.ScaleType.FIT_CENTER
//setImageURI(it.imageUri)
setImageBitmap(it.bitmap)
}
}
}
}
}
@@ -0,0 +1,48 @@
package com.sw.dualscreen.dialog
import android.app.Dialog
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Gravity
import android.view.View
import com.sw.dualscreen.R
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.ext.hideKeyboard
abstract class BaseDialog(
context: Context,
var defWidth: Int = 400.dp,
var defHeight: Int = 260.dp
) : Dialog(context, R.style.DialogTheme) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val rootView = getRootView()
setContentView(rootView)
setCancelable(false)
setCanceledOnTouchOutside(false)
window?.apply {
setLayout(defWidth, defHeight)
setBackgroundDrawable(ColorDrawable())
setGravity(Gravity.CENTER)
}
rootView.setOnClickListener { it.hideKeyboard() }
initView()
}
abstract fun getRootView(): View
abstract fun initView()
// fun getReceiptActivity(): GoodsListActivity? {
// var ctx = context
// while (ctx is ContextWrapper) {
// ctx = ctx.baseContext
// if (ctx is GoodsListActivity) {
// return ctx
// }
// }
// return null
// }
}
@@ -0,0 +1,35 @@
package com.sw.dualscreen.dialog
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import com.sw.dualscreen.databinding.DialogWarnBinding
class WarnDialog(
context: Context,
var content:String,
var cancelBlock: () -> Unit = {},
var confirmBlock: () -> Unit = {},
) : BaseDialog(context = context) {
private lateinit var binding: DialogWarnBinding
override fun getRootView(): View {
val inflater = LayoutInflater.from(context)
binding = DialogWarnBinding.inflate(inflater)
return binding.root
}
override fun initView() {
binding.tvWarnContent.text = content
binding.btnCancel.setOnClickListener {
cancelBlock()
dismiss()
}
binding.btnConfirm.setOnClickListener {
confirmBlock()
dismiss()
}
}
}
@@ -1,5 +1,6 @@
package com.sw.dualscreen.ext
import android.content.Context
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
@@ -7,8 +8,17 @@ import android.graphics.ImageFormat
import android.graphics.Rect
import android.graphics.YuvImage
import android.util.TypedValue
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.annotation.Dimension
import androidx.camera.core.ImageProxy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
/**
@@ -56,6 +66,7 @@ val Float.sp: Float
)
fun Double.format2String(): String = "%.1f".format(this)
fun Double.format2String(num:Int): String = "%.${num}f".format(this)
// 添加扩展函数
fun ImageProxy.toSafeBitmap(): Bitmap {
@@ -82,3 +93,30 @@ fun ImageProxy.toSafeBitmap(): Bitmap {
val imageBytes = out.toByteArray()
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
fun View.hideKeyboard() {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(this.windowToken, 0)
this.clearFocus() // 清除焦点避免键盘再次弹出
}
fun EditText.addOnActionSearchListener(searchCallback: () -> Unit) {
setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
// 处理搜索逻辑
searchCallback()
return@setOnEditorActionListener true // 阻止事件继续传递
}
return@setOnEditorActionListener false
}
}
fun View.clickWithDebounce(delay: Long = 500, action: () -> Unit) {
var job: Job? = null
setOnClickListener {
job?.cancel()
job = CoroutineScope(Dispatchers.Main).launch {
delay(delay)
action()
}
}
}
@@ -0,0 +1,24 @@
package com.sw.dualscreen.model;
import androidx.activity.result.ActivityResult;
import java.util.Map;
public interface IActivityResult {
interface LaunchActivityCallback {
void callback(ActivityResult result);
}
interface LaunchTackPictureCallback {
void callback(boolean result);
}
interface RequestPermissionCallback {
void callback(boolean result);
}
interface RequestMultiplePermissionsCallback {
void callback(Map<String,Boolean> result);
}
}
@@ -38,7 +38,8 @@ data class FoodInfo(
@SerializedName("stFoodInfoSetting")
val stFoodInfoSetting: StFoodInfoSetting? = StFoodInfoSetting(),
@SerializedName("stFoodInfoSpecificationList")
val stFoodInfoSpecificationList: List<StFoodInfoSpecification?>? = listOf()
val stFoodInfoSpecificationList: List<StFoodInfoSpecification?>? = listOf(),
var score: Int = 0
) : Parcelable {
@Parcelize
data class FoodTypeAndRealIntakeVo(
@@ -8,10 +8,15 @@ import com.sw.dualscreen.model.response.EquipmentInfo
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.UserFaceInfo
import com.sw.dualscreen.model.response.UserNutritionData
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Query
import retrofit2.http.Url
@@ -99,4 +104,27 @@ interface ApiService {
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/userEatFood/doubleBalance/face/stall",
@Body param: List<UserNutritionParam>
): ApiResponse<String>
/**
* 获取菜品信息
*/
@GET
suspend fun getFoodInfo(
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getRestInfoFoodsByType/stall/v2",
@Query("restId") restId: String,
@Query("foodNames") foodName: String,
): ApiResponse<List<FoodInfo>>
/**
* 提交采集图片数据
*/
@Multipart
@POST
suspend fun postImageData(
@Url url: String = "http://192.168.1.201:14801/terminal/neglect/dishCollectionVectorData/add",
@PartMap params: Map<String, @JvmSuppressWildcards RequestBody>,
@Part image: MultipartBody.Part?
): ApiResponse<String>
}
@@ -1,14 +1,29 @@
package com.sw.dualscreen.objbox
import android.graphics.Bitmap
import android.net.Uri
data class FoodClassInfo(
var class_names: List<String>,
var class_to_idx: Map<String, Int>,
var idx_to_class: Map<String, String>
)
data class FoodQueryResult(
var id: Long,
var name: String?,
var foodIdx: Int,
var score: Double
//data class FoodQueryResult(
// var id: Long,
// var name: String?,
// var foodIdx: Int,
// var score: Double
//)
data class FoodCollectionBean(
var imageUri: Uri? = null,
var bitmap: Bitmap? = null,
var imageVector: FloatArray? = null,
var isShowCamera: Boolean = false,
var isFinish:Boolean = false
)
data class CollectedFoodBean(
var foodName: String?,
)
@@ -2,15 +2,21 @@ package com.sw.dualscreen.objbox
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.util.SparseLongArray
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.sw.dualscreen.MyApp
import com.sw.dualscreen.utils.AssetsTool
import com.sw.dualscreen.utils.GsonUtils
import com.sw.dualscreen.utils.ImageUtil
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import io.objectbox.query.Query
import org.pytorch.IValue
import org.pytorch.Module
import org.pytorch.torchvision.TensorImageUtils
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@@ -18,58 +24,146 @@ import java.io.InputStream
object FoodModule {
private const val THRESHOLD = 0.8
private lateinit var module_mobile: Module
private lateinit var box: Box<Food>
private lateinit var embeddingsList: List<List<Float>>
private lateinit var labelsList: IntArray
private lateinit var classInfo: FoodClassInfo
private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
val DEFAULT_FOOD_INDEX = -1
fun init(context: Context) {
Thread {
module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
box = ObjectBox.boxStore.boxFor(Food::class)
if (box.all.isNotEmpty()) {
box.removeAll()
//if (box.all.isNotEmpty()) {
// box.removeAll()
//}
//if (box.all.isEmpty()) {
// initDefFoodData(context)
//}
}.start()
}
fun uri2FloatArray(uri: Uri): FloatArray? {
return MyApp.instance?.let { context ->
ImageUtil.uriToBitmap(context, uri)?.let {
bitmap2FloatArray(it)
}
if (box.all.isEmpty()) {
initFoodData(context)
}
}
fun queryFood(bitmap: Bitmap, queryCount:Int = 30): MutableList<FoodQueryResult> {
fun bitmap2FloatArray(bitmap: Bitmap): FloatArray {
val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
bitmap,
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()
val floatArray = outputTensor.dataAsFloatArray
return outputTensor.dataAsFloatArray
}
fun queryFood(uri: Uri, queryCount: Int = 15): List<String>? {
return uri2FloatArray(uri)?.let {
queryFood(it, queryCount)
}
}
/**
* 返回识别物品名称列表
*/
fun queryFood(bitmap: Bitmap, queryCount: Int = 15): List<String> {
val floatArray = bitmap2FloatArray(bitmap)
return queryFood(floatArray, queryCount)
}
fun queryFood(floatArray: FloatArray, queryCount:Int = 30): MutableList<FoodQueryResult> {
val query: Query<Food> = box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
/**
* 返回识别物品IdNameScore对象列表
*/
fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
val floatArray = bitmap2FloatArray(bitmap)
return queryFoodNameScore(floatArray, queryCount)
}
fun queryFoodNameScore(floatArray: FloatArray, queryCount: Int = 15): List<IdNameScore> {
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}" }
val resultList = mutableListOf<FoodQueryResult>()
val idScoreList = query.findIdsWithScores()
val nameScoreList = mutableListOf<IdNameScore>()
idScoreList.forEach {
nameScoreList.add(IdNameScore(id = it.id, name = box.get(it.id).name?:"", score = it.score))
}
Timber.tag("FoodModule").d("registerDataChange,queryFood数据:${GsonUtils.toJson(nameScoreList)}")
return nameScoreList
}
fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
val floatArray = bitmap2FloatArray(bitmap)
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
val maxScoreList = nameScoreList
.filter { it.score < 1 - THRESHOLD }
.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)
})
//Timber.tag("FoodModule").d("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
return sortedScoreList
}
fun queryFood(floatArray: FloatArray, queryCount: Int = 15): List<String> {
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}" }
val map = mutableMapOf<String, Int>()
query.findIdsWithScores().forEach {
val food = box.get(it.id)
resultList.add(
FoodQueryResult(
id = it.id,
name = food.name,
foodIdx = food.foodIdx,
score = it.score
)
)
Timber.d("${food.name}|${food.foodIdx}|${it.score}")
if (1 - it.score >= THRESHOLD) {
//FoodQueryResult(id = it.id, name = food.name, foodIdx = food.foodIdx, score = it.score)
food.name?.let { key ->
val count = map[key] ?: 0
map.put(key, count + 1)
}
return resultList
}
}
val list = map.entries.sortedByDescending { it.value }.map { it.key }
return list
// 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
)
private fun initFoodData(context: Context) {
if (box.all.isNotEmpty()) {
fun initDefFoodData(context: Context, action:()-> Unit={}) {
val count = box.all.count { it.foodIdx == DEFAULT_FOOD_INDEX }
if (count > 0) {
return
}
val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json")
@@ -87,8 +181,9 @@ object FoodModule {
val classIdx = labelsList[index]
val foodName = foodMap["$classIdx"]
val array = floatList.toFloatArray()
box.put(Food(name = foodName, foodVector = array, foodIdx = index))
box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
}
action()
}
@@ -42,6 +42,11 @@ import kotlin.jvm.javaClass
*/
object ObjectBox {
// 确保所有设备/版本使用相同的objectbox-models/default.json
// 避免直接修改自动生成的MyObjectBox类
// 跨版本升级时使用boxStore.runInTx执行数据迁移
private const val TAG = "ObjectBox"
lateinit var boxStore: BoxStore
@@ -74,8 +74,9 @@ class SecondaryScreenPresentation(
private lateinit var itemUserNutritionBinding: ItemUserNutritionBinding
private lateinit var stepChangeCallback: (Int) -> Unit
// 上一次的计算热量结果
private var lastWeight = 0.0
private var detectWeight = 0.0 //识别菜品时的重量
private var lastWeight = 0.0 // 上一次的计算热量结果
private val debouncer = Debouncer(500)
private var recognitionTime: Long = 0L // 人脸识别时的时间
private var recognitionWeight: Double = 0.0 // 人脸识别时的重量
@@ -85,6 +86,8 @@ class SecondaryScreenPresentation(
// 当前步骤
var currentStep: Int = 0
private var isGoStep1 = false;//防止持续调用
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -162,7 +165,7 @@ class SecondaryScreenPresentation(
Timber.d("step1")
currentStep = 1
LightManager.closeGreenLight()
LightManager.openRedLight()
LightManager.closeRedLight()
//回复上一次的设置
// lastWeight = 0.0
dinnerTypeInfo = null
@@ -180,16 +183,19 @@ class SecondaryScreenPresentation(
pauseCamera()
binding.tvBottomTip.visibility = View.VISIBLE
itemUserNutritionBinding.clNutritionData.visibility = View.GONE
activity.clearFoodList()
}
/**
* 显示人脸识别
*/
fun step2(foodInfo: FoodInfo) {
isGoStep1 = true
Timber.d("step2")
currentStep = 2
LightManager.openRedLight()
LightManager.openGreenLight()
LightManager.closeRedLight()
// LightManager.openGreenLight()
dinnerTypeInfo = null
userNutritionData = null
recognitionWeight = 0.0
@@ -231,6 +237,7 @@ class SecondaryScreenPresentation(
fun step3() {
Timber.d("step3")
LightManager.closeRedLight()
LightManager.openGreenLight()
currentStep = 3
stepChangeCallback(currentStep)
if (currentFood == null) {
@@ -270,10 +277,24 @@ class SecondaryScreenPresentation(
fun updateWeight(weight: Double) {
lastWeight = weight * 1000 // 将千克转成克
Timber.d("updateWeight lastWeight = $weight")
if (currentStep == 2 &&
lastWeight <= 5 &&
isGoStep1
) {//人脸识别过程中,秤上物品拿走
isGoStep1 = false
activity.runOnUiThread {
Timber.e("切回step1")
step1()
}
}
if (currentFood == null) {
Timber.e("updateWeight currentFood is null")
return
}
if (userNutritionData == null) {
Timber.e("updateWeight userNutritionData is null")
return
@@ -285,12 +306,17 @@ class SecondaryScreenPresentation(
calculateNutrition(recognitionWeight - lastWeight)
}
fun updateDetectWeight(weight: Double) {
detectWeight = weight * 1000 // 将千克转成克
}
/**
* 余量取餐
*/
fun updateMealPickupMode(mode: Int) {
Timber.d("updateMealPickupMode mode = $mode")
mealPickupMode = mode
step1()
}
private fun calculateNutrition(weight1: Double) {
@@ -758,7 +784,7 @@ class SecondaryScreenPresentation(
* @param facePreviewInfoList RGB画面的实时人脸信息
*/
private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo?>) {
Timber.d("drawPreviewInfo facePreviewInfoList = ${facePreviewInfoList.size}, rgbFaceRectTransformer = ${rgbFaceRectTransformer != null}")
// Timber.d("drawPreviewInfo facePreviewInfoList = ${facePreviewInfoList.size}, rgbFaceRectTransformer = ${rgbFaceRectTransformer != null}")
if (rgbFaceRectTransformer != null) {
val rgbDrawInfoList: MutableList<DrawInfo?>? = recognizeViewModel.getDrawInfo(
facePreviewInfoList,
@@ -789,12 +815,18 @@ class SecondaryScreenPresentation(
Timber.d("postUserData userNutritionData = ${userNutritionData == null}, currentFood = ${currentFood == null}")
return
}
var eatWeight = 0.0
if (mealPickupMode == 0) {//即放即取
eatWeight = recognitionWeight - 0
} else {
eatWeight = recognitionWeight - lastWeight
}
val userNutritionParam = UserNutritionParam(
userId = userNutritionData?.userId!!,
foodId = currentFood?.id!!,
faceTime = recognitionTime,
faceEndTime = System.currentTimeMillis(),
eatWeight = recognitionWeight - lastWeight,
eatWeight = eatWeight,//lastWeight
foodWeight = lastWeight
)
viewModel.postUserNutritionData(listOf<UserNutritionParam>(userNutritionParam))
@@ -1,5 +1,7 @@
package com.sw.dualscreen.repository
import android.content.Context
import android.net.Uri
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.model.request.UserNutritionParam
import com.sw.dualscreen.model.response.ApiResponse
@@ -9,6 +11,9 @@ import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.UserFaceInfo
import com.sw.dualscreen.model.response.UserNutritionData
import com.sw.dualscreen.network.api.ApiService
import com.sw.dualscreen.utils.ImageUtil
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
/**
* 远程数据处理
@@ -103,4 +108,39 @@ class RemoteRepository constructor(
): ApiResponse<String> {
return safeApiCall { apiService.postUserNutritionData(param = param) }
}
/**
* 获取菜品信息
* @param restId 从device服务获取的canteenId字段
* @param foodName 菜品名称,多个使用逗号拼接
*/
suspend fun getFoodInfo(
restId: String = GlobalData.restId,
foodName: String,
): ApiResponse<List<FoodInfo>> {
return safeApiCall { apiService.getFoodInfo(restId = restId, foodName = foodName) }
}
/**
* 获取菜品信息
* @param restId 从device服务获取的canteenId字段
* @param foodName 菜品名称,多个使用逗号拼接
*/
suspend fun postImageData(
context: Context,
restId: String = GlobalData.restId,
foodId: String,
foodName: String,
foodVector: String,
uri: Uri,
): ApiResponse<String> {
val params = HashMap<String, RequestBody>()
params["placeId"] = restId.toRequestBody()
params["foodId"] = foodId.toRequestBody()
params["foodName"] = foodName.toRequestBody()
params["foodVector"] = foodVector.toRequestBody()
val imagePart = ImageUtil.uriToMultipart(context, uri, "foodPic")
return safeApiCall { apiService.postImageData(params = params, image = imagePart) }
}
}
@@ -19,7 +19,7 @@ object SensorScaleUtils {
private var mSensorScale: SensorScale? = null
private var isOpened: Boolean = false
private var isRead: Boolean = false
private var isZero = false
var isZero = false
private var callback: Callback? = {}
private var lastWeight: Double? = null
@@ -118,8 +118,8 @@ object SensorScaleUtils {
fun zero() {
Timber.d("zero isOpened = $isOpened, mSensorScale = $mSensorScale")
if (!isOpened) return
// isZero = true
mSensorScale?.zero {
isZero = true
Timber.d("zero 零位标定操作成功")
ToastUtils.showToast("零位标定操作成功")
}
@@ -0,0 +1,38 @@
package com.sw.dualscreen.utils
import android.graphics.Bitmap
object BitmapCropper {
/**
* 裁剪Bitmap中心区域为指定尺寸
* @param original 原始Bitmap
* @param targetWidth 目标宽度
* @param targetHeight 目标高度
* @return 裁剪后的Bitmap
*/
fun cropCenter(original: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap {
val originalWidth = original.width
val originalHeight = original.height
// 计算中心点坐标
var startX = (originalWidth - targetWidth) / 2
var startY = (originalHeight - targetHeight) / 2
// 边界检查
startX = startX.coerceAtLeast(0)
startY = startY.coerceAtLeast(0)
val actualWidth = minOf(targetWidth, originalWidth - startX)
val actualHeight = minOf(targetHeight, originalHeight - startY)
return Bitmap.createBitmap(original, startX, startY, actualWidth, actualHeight)
}
}
// 使用示例
fun main() {
// 假设这是从资源加载的Bitmap
val originalBitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888)
// 裁剪中心500x500区域
val croppedBitmap = BitmapCropper.cropCenter(originalBitmap, 500, 500)
println("裁剪后尺寸:${croppedBitmap.width}x${croppedBitmap.height}")
}
@@ -0,0 +1,57 @@
package com.sw.dualscreen.utils
import android.content.Context
import android.graphics.Bitmap
import android.os.Environment
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
object BitmapSaver {
// 保存到公共目录(需WRITE_EXTERNAL_STORAGE权限)
fun saveToPublicDirectory(
bitmap: Bitmap,
folderName: String = Environment.DIRECTORY_PICTURES,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
quality: Int = 100
): File? {
val dir = Environment.getExternalStoragePublicDirectory(folderName)
if (!dir.exists()) dir.mkdirs()
return saveBitmap(bitmap, File(dir, fileName), format, quality)
}
// 保存到应用私有目录(无需权限)
fun saveToAppFilesDir(
bitmap: Bitmap,
context: Context,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
quality: Int = 100
): File? {
//val dir = context.getExternalFilesDir(null)
val dir = File(context.cacheDir, "crop")
if (dir.exists().not()) {
dir.mkdirs()
}
return saveBitmap(bitmap, File(dir, fileName), format, quality)
}
private fun saveBitmap(
bitmap: Bitmap,
outputFile: File,
format: Bitmap.CompressFormat,
quality: Int
): File? {
return try {
FileOutputStream(outputFile).use { fos ->
bitmap.compress(format, quality, fos)
fos.flush()
}
outputFile
} catch (e: IOException) {
e.printStackTrace()
null
}
}
}
@@ -0,0 +1,68 @@
package com.sw.dualscreen.utils
import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import android.widget.Toast
import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import java.io.File
import java.io.FileOutputStream
/**
* 简洁相机工具类
* 调用系统相机拍照并返回照片路径
*/
class CameraHelper(
private val context: Context,
caller: ActivityResultCaller,
private val authority: String,
private val onResult: (uri: Uri, filePath: String?) -> Unit
) {
private lateinit var imageUri: Uri
private val takePictureLauncher: ActivityResultLauncher<Uri>
init {
takePictureLauncher =
caller.registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
if (success) {
val path = getRealPathFromUri(context, imageUri)
onResult(imageUri, path)
} else {
Toast.makeText(context, "拍照取消", Toast.LENGTH_SHORT).show()
}
}
}
/** 打开系统相机 */
fun openCamera() {
// 1. 创建存储图片的文件
val photoFile = File(context.externalCacheDir, "IMG_${System.currentTimeMillis()}.jpg")
photoFile.parentFile?.mkdirs() // 确保目录存在
// 2. 生成 content:// Uri
imageUri = FileProvider.getUriForFile(context, authority, photoFile)
// 3. 启动相机
takePictureLauncher.launch(imageUri)
}
/** 可选:将 content:// 转为实际路径 */
private fun getRealPathFromUri(context: Context, uri: Uri): String? {
var filePath: String? = null
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
val fileName = cursor.getString(nameIndex)
val file = File(context.cacheDir, fileName)
context.contentResolver.openInputStream(uri)?.use { input ->
FileOutputStream(file).use { output -> input.copyTo(output) }
}
filePath = file.absolutePath
}
return filePath
}
}
@@ -0,0 +1,81 @@
package com.sw.dualscreen.utils
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.camera.core.CameraSelector
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
class CameraUtils(private var activity: ComponentActivity) {
private var cameraController: LifecycleCameraController? = null
private var photoCaptureHelper: PhotoCaptureHelper? = null
// private var isCameraReady = false
fun takePhoto(callback: (Uri) -> Unit) {
cameraController?.let {
if (photoCaptureHelper == null) {
initCaptureHelper()
}
}
photoCaptureHelper?.let {
it.addSuccessCallback(callback)
it.bindCameraCallback {
bind()
}
it.takePhoto()
}
}
private fun initCaptureHelper() {
photoCaptureHelper = PhotoCaptureHelper(
context = activity,
cameraController = cameraController!!,
onSuccess = {},
onError = { msg ->
//toast(msg)
}
)
}
fun setPreviewController(previewView: PreviewView?) {
if (previewView?.controller == null) {
previewView?.controller = cameraController
}
}
fun initCamera() {
if (cameraController == null) {
cameraController = LifecycleCameraController(activity).apply {
// 必须设置有效的用例
setEnabledUseCases(
CameraController.IMAGE_CAPTURE
// or CameraController.VIDEO_CAPTURE
)
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
}
bind()
}
// if (isCameraReady.not()) {
// try {
// cameraController!!.initializationFuture.addListener({
// isCameraReady = true
// Timber.d("Camera initialized successfully")
// }, ContextCompat.getMainExecutor(this))
// } catch (e: Exception) {
// Timber.d("Camera initialized error = ${e.message}")
// }
// }
}
fun bind() {
cameraController?.bindToLifecycle(activity)
}
fun unbind() {
cameraController?.unbind()
}
}
@@ -0,0 +1,47 @@
package com.example.utils
import android.util.Base64
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* 工具类:float[] 与 Base64 字符串互转
* 无需外部依赖,适用于 Android 原生项目。
*/
object FloatBase64Utils {
/**
* float数组 → Base64字符串
* @param values FloatArray
* @return Base64字符串(无换行)
*/
fun floatArrayToBase64(values: FloatArray): String {
if (values.isEmpty()) return ""
val buffer = ByteBuffer.allocate(values.size * 4)
buffer.order(ByteOrder.BIG_ENDIAN) // 默认大端序,可改 LITTLE_ENDIAN
values.forEach { buffer.putFloat(it) }
val bytes = buffer.array()
return Base64.encodeToString(bytes, Base64.NO_WRAP)
}
/**
* Base64字符串 → float数组
* @param base64 Base64字符串
* @return FloatArray
*/
fun base64ToFloatArray(base64: String?): FloatArray {
if (base64.isNullOrEmpty()) return FloatArray(0)
val bytes = Base64.decode(base64, Base64.NO_WRAP)
val buffer = ByteBuffer.wrap(bytes)
buffer.order(ByteOrder.BIG_ENDIAN)
val result = FloatArray(bytes.size / 4)
for (i in result.indices) {
result[i] = buffer.getFloat()
}
return result
}
}
@@ -4,6 +4,12 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
object ImageUtil {
@@ -18,4 +24,24 @@ object ImageUtil {
}
}
fun uriToMultipart(context: Context, uri: Uri, partName: String): MultipartBody.Part? {
try {
val contentResolver = context.contentResolver
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
// 临时文件(避免直接访问SAF文件)
val tempFile = File.createTempFile("upload_", ".jpg", context.cacheDir)
val outputStream = FileOutputStream(tempFile)
inputStream.copyTo(outputStream)
inputStream.close()
outputStream.close()
val requestFile = RequestBody.create("image/*".toMediaTypeOrNull(), tempFile)
return MultipartBody.Part.createFormData(partName, tempFile.name, requestFile)
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
}
@@ -0,0 +1,92 @@
package com.sw.dualscreen.utils
import android.content.Context
import android.net.Uri
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.view.CameraController
import androidx.core.content.ContextCompat
import timber.log.Timber
import java.io.File
/**
* 拍照工具类
* @param context Context 上下文
* @param cameraController CameraController 相机控制器
* @param onSuccess (Uri) -> Unit 拍照成功回调
* @param onError (String) -> Unit 拍照失败回调
*/
class PhotoCaptureHelper(
private val context: Context,
private val cameraController: CameraController,
private val onSuccess: (Uri) -> Unit = {},
private val onError: (String) -> Unit = {}
) {
private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf()
fun addSuccessCallback(callback:(Uri) -> Unit) {
if (callbackList.contains(callback).not()) {
callbackList.add(callback)
}
}
private var bindCamera:(()->Unit)?=null
fun bindCameraCallback(callback:()->Unit) {
this.bindCamera = callback
}
/**
* 拍照方法
* @param fileNamePrefix 文件名前缀,默认为"IMG_"
* @param fileExtension 文件扩展名,默认为".jpg"
*/
fun takePhoto(
fileNamePrefix: String = "IMG_",
fileExtension: String = ".jpg"
) {
Timber.d("开始拍照采集")
try {
val executor = ContextCompat.getMainExecutor(context)
val cacheDir = context.cacheDir
val photoFile = File.createTempFile(
"${fileNamePrefix}${System.currentTimeMillis()}",
fileExtension,
cacheDir
)
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
cameraController.takePicture(
outputOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
Timber.d("照片保存成功: $photoUri")
onSuccess(photoUri)
callbackList.forEach {
it(photoUri)
}
}
override fun onError(exception: ImageCaptureException) {
val errorMsg = "拍照失败: ${exception.message}"
if (errorMsg.contains("Not bound to a valid Camera")) {
if (bindCamera != null) {
bindCamera?.invoke()
//takePhoto()
}
}
Timber.e(exception, errorMsg)
onError(errorMsg)
}
}
)
} catch (e: Exception) {
val errorMsg = "创建临时文件失败: ${e.message}"
Timber.e(e, errorMsg)
onError(errorMsg)
}
}
}
@@ -0,0 +1,34 @@
package com.sw.dualscreen.utils
import android.Manifest
import com.sw.dualscreen.activity.BaseActivity
object StorageUtils {
fun requestPermission(activity: BaseActivity<*>, callback: () -> Unit) {
activity.requestSinglePermissionResult(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
callback()
}
// activity.requestSinglePermissionResult(Manifest.permission.CAMERA) {
//// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
//// if (Environment.isExternalStorageManager()) {
//// callback()
//// } else {
//// val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
////// activity.startActivityForResult(intent, MANAGE_REQUEST_CODE)
//// activity.launchActivityResult(intent) {
//// callback()
//// }
//// }
//// } else {
//
//// }
// }
}
fun requestCameraPermission(activity: BaseActivity<*>, callback: () -> Unit) {
activity.requestSinglePermissionResult(Manifest.permission.CAMERA) {
callback()
}
}
}
@@ -52,7 +52,10 @@ abstract class BaseViewModel() : ViewModel() {
}
protected open fun parseResponse(response: ApiResponse<*>): Boolean {
if (response.isSuccess()) {
if (response.isSuccess()
|| response.code == 200
|| response.code == 0
) {
return true
}
Timber.d("msg = ${response.message}, code = ${response.code}")
@@ -62,6 +65,7 @@ abstract class BaseViewModel() : ViewModel() {
fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) {
GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!!
GlobalData.appBaseUrl="http://192.168.1.210/gateway/local"
GlobalData.sdkKey = equipmentInfo.arcsoftSdkKey!!
GlobalData.appId = equipmentInfo.arcsoftAppId!!
// GlobalData.activeKey = equipmentInfo.arcsoftActiveKey!!
@@ -1,5 +1,7 @@
package com.sw.dualscreen.viewmodel
import android.content.Context
import android.net.Uri
import androidx.lifecycle.viewModelScope
import com.arcsoft.face.ErrorInfo
import com.sw.dualscreen.GlobalData
@@ -21,6 +23,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import kotlin.math.atan
class UserViewModel : BaseViewModel() {
@@ -49,6 +52,8 @@ class UserViewModel : BaseViewModel() {
*/
private val _loadFaceResult = MutableStateFlow<Boolean>(false)
val loadFaceResult: StateFlow<Boolean> = _loadFaceResult
private val _identifiedFoodInfoList2 = MutableStateFlow<List<FoodInfo>>(emptyList())
val identifiedFoodInfoList2: StateFlow<List<FoodInfo>> = _identifiedFoodInfoList2
/**
* 获取token
@@ -205,4 +210,35 @@ class UserViewModel : BaseViewModel() {
}
}
}
fun getFoodInfo(foodName: String, action:(List<FoodInfo>)-> Unit = {}) {
Timber.d("getFoodInfo")
launchWithLoading {
val response = repository.getFoodInfo(foodName = foodName)
if (parseResponse(response)) {
val list = response.result ?: emptyList()
_identifiedFoodInfoList2.value = list
action(list)
}
}
}
fun postImageData(
context: Context,
foodId: String,
foodName: String,
foodVector: String,
uri: Uri
) {
Timber.d("postImageData")
launchWithLoading {
val response = repository.postImageData(
context, foodId = foodId, foodName = foodName, foodVector = foodVector,
uri = uri
)
if (parseResponse(response)) {
Timber.tag("mzf").d("code=${response.code}")
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#FF3232" android:state_checked="true"/>
<item android:color="#5E7585" android:state_checked="false"/>
</selector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#30000000">
<item>
<shape android:shape="rectangle">
<solid android:color="#FF0032C8" />
<corners android:radius="10dp" />
</shape>
</item>
</ripple>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#ffffff"/>
<stroke android:color="#cdcdcd" android:width="1dp"/>
<corners android:radius="5dp"/>
</shape>
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="false">
<shape android:shape="rectangle">
<corners android:radius="8dp"/>
<solid android:color="#F4FAFF"/>
<stroke android:color="@color/white" android:width="2dp"/>
</shape>
</item>
<item android:state_checked="true">
<layer-list>
<item>
<shape android:shape="rectangle">
<corners android:radius="8dp"/>
<solid android:color="#FFF0F0"/>
<stroke android:color="#FF3232" android:width="2dp"/>
</shape>
</item>
<item android:width="32dp" android:height="32dp" android:gravity="bottom|end">
<bitmap android:src="@drawable/check_red"/>
</item>
</layer-list>
</item>
</selector>
+11
View File
@@ -0,0 +1,11 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:bottomLeftRadius="12dp"
android:bottomRightRadius="12dp"
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
<stroke
android:color="#ffff9900"
android:width="2dp" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white"/>
<corners android:radius="12dp"/>
</shape>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#30000000">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/white"/>
<stroke android:color="#FF0032C8" android:width="1dp"/>
<corners android:radius="10dp"/>
</shape>
</item>
</ripple>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="false" >
<layer-list>
<item android:width="60dp" android:height="60dp">
<bitmap android:src="@drawable/pay_cash_normal" />
</item>
</layer-list>
</item>
<item android:state_checked="true">
<layer-list>
<item android:width="60dp" android:height="60dp">
<bitmap android:src="@drawable/pay_cash_checked" />
</item>
</layer-list>
</item>
</selector>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="false" >
<layer-list>
<item android:width="60dp" android:height="60dp">
<bitmap android:src="@drawable/pay_face_normal" />
</item>
</layer-list>
</item>
<item android:state_checked="true">
<layer-list>
<item android:width="60dp" android:height="60dp">
<bitmap android:src="@drawable/pay_face_checked" />
</item>
</layer-list>
</item>
</selector>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="false" >
<layer-list>
<item android:width="60dp" android:height="60dp">
<bitmap android:src="@drawable/pay_number_normal" />
</item>
</layer-list>
</item>
<item android:state_checked="true">
<layer-list>
<item android:width="60dp" android:height="60dp">
<bitmap android:src="@drawable/pay_number_checked" />
</item>
</layer-list>
</item>
</selector>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="false" >
<layer-list>
<item android:width="60dp" android:height="60dp">
<bitmap android:src="@drawable/pay_qrcode_normal" />
</item>
</layer-list>
</item>
<item android:state_checked="true">
<layer-list>
<item android:width="60dp" android:height="60dp">
<bitmap android:src="@drawable/pay_qrcode_checked" />
</item>
</layer-list>
</item>
</selector>
@@ -0,0 +1,92 @@
<?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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="88dp"
android:layout_marginTop="24dp">
<ImageView
android:id="@+id/ivBack"
android:layout_width="68dp"
android:layout_height="68dp"
android:padding="10dp"
android:src="@drawable/ic_back" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="已采集菜品"
android:textColor="@color/black"
android:textSize="26sp" />
</FrameLayout>
<FrameLayout
android:id="@+id/flSearchBlock"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginHorizontal="20dp"
android:background="@drawable/bg_gray">
<EditText
android:id="@+id/etInputGoods"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00000000"
android:gravity="center"
android:hint="输入菜品名称"
android:maxLines="1"
android:paddingStart="10dp"
android:paddingEnd="80dp"
android:textColor="#FF666666"
android:textColorHint="#FFC7C8DC"
android:textSize="30sp"
android:inputType="text"
android:imeOptions="actionSearch" />
<ImageView
android:id="@+id/ivGoodsSearch"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="16dp"
android:padding="14dp"
android:src="@drawable/ic_goods_search" />
</FrameLayout>
<!-- <com.scwang.smart.refresh.layout.SmartRefreshLayout-->
<!-- android:id="@+id/refreshLayout"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="0dp"-->
<!-- android:layout_marginHorizontal="130dp"-->
<!-- android:layout_marginTop="40dp"-->
<!-- android:layout_marginBottom="58dp"-->
<!-- app:layout_constraintBottom_toTopOf="@id/dividerLine"-->
<!-- app:layout_constraintTop_toBottomOf="@id/flSearchBlock">-->
<!-- <com.scwang.smart.refresh.header.ClassicsHeader-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content" />-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFoodList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
android:layout_margin="10dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="8"
tools:listitem="@layout/list_item_collected_data" />
<!-- <com.scwang.smart.refresh.footer.ClassicsFooter-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content" />-->
<!-- </com.scwang.smart.refresh.layout.SmartRefreshLayout>-->
</LinearLayout>
@@ -0,0 +1,182 @@
<?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">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="88dp"
android:layout_marginTop="24dp">
<ImageView
android:id="@+id/ivBack"
android:layout_width="68dp"
android:layout_height="68dp"
android:padding="15dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_back" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="菜品采集"
android:textColor="@color/black"
android:textSize="26sp" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="24dp">
<FrameLayout
android:id="@+id/flPreview"
android:layout_width="180dp"
android:layout_height="180dp"
android:layout_marginStart="24dp">
<androidx.cardview.widget.CardView
android:id="@+id/flCameraPreview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_margin="2dp"
app:cardCornerRadius="10dp"
app:cardElevation="0dp"/>
<LinearLayout
android:id="@+id/llCameraFlag"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:background="@color/white"
android:orientation="vertical"
android:visibility="visible">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:scaleType="fitCenter"
android:src="@drawable/ic_camera256"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="图片采集"
android:textColor="#ffb4b4c8"
android:textSize="16sp" />
</LinearLayout>
</FrameLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="20dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<Button
android:id="@+id/btnTakePhoto"
android:layout_width="140dp"
android:layout_height="80dp"
android:gravity="center"
android:text="开始拍照"
android:textColor="#ffffff"
android:textSize="18sp" />
<Button
android:id="@+id/btnClearData"
android:layout_width="120dp"
android:layout_height="80dp"
android:gravity="center"
android:text="清除"
android:layout_marginHorizontal="10dp"
android:textColor="#ffffff"
android:textSize="18sp" />
<Button
android:id="@+id/btnCollectedGoods"
android:layout_width="220dp"
android:layout_height="80dp"
android:gravity="center"
android:text="已采集菜品"
android:textColor="#ffffff"
android:textSize="18sp" />
</LinearLayout>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFoodCollection"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginHorizontal="3dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="3"
tools:itemCount="3"
tools:listitem="@layout/list_item_food_collection" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="66dp"
android:layout_marginLeft="24dp"
android:layout_marginTop="24dp"
android:layout_marginRight="24dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<EditText
android:id="@+id/editFoodName"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/bg_gray"
android:gravity="center_vertical"
android:hint="输入菜品名称"
android:imeOptions="actionSearch"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:singleLine="true"
android:textColor="@color/black"
android:textSize="26sp" />
<Button
android:id="@+id/btnFoodSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:text="菜品检索"
android:textColor="#ffffff"
android:textSize="26sp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="24dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="24dp"
android:layout_weight="1" />
<Button
android:id="@+id/btnSave"
android:layout_width="260dp"
android:layout_height="60dp"
android:layout_gravity="center"
android:layout_marginVertical="24dp"
android:text="保存"
android:textColor="#ffffff"
android:textSize="26sp" />
</LinearLayout>
+31 -1
View File
@@ -24,9 +24,9 @@
<Spinner
android:id="@+id/spinner"
style="@style/customSpinnerStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/customSpinnerStyle"
android:layout_gravity="end"
android:padding="0dp"
android:popupBackground="@drawable/shape_for_custom_spinner" />
@@ -43,6 +43,7 @@
android:text="羊肉臊子荞面饸饹" />
<FrameLayout
android:id="@+id/foodDisplayLayout"
android:layout_width="match_parent"
android:layout_height="531dp"
android:layout_marginHorizontal="60dp"
@@ -77,6 +78,35 @@
android:layout_marginHorizontal="60dp"
android:layout_weight="1" />
<LinearLayout
android:id="@+id/layout_rescan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="140dp"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="未识别出菜品,请重试或手动搜索"
android:textColor="@color/white"
android:textSize="24sp" />
<TextView
android:id="@+id/tv_rescan"
android:layout_width="180dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="@drawable/bg_rescan"
android:gravity="center"
android:padding="10dp"
android:text="重试"
android:textColor="#ffff9900"
android:textSize="32sp" />
</LinearLayout>
<TextView
android:id="@+id/tv_to_search"
android:layout_width="wrap_content"
+95
View File
@@ -0,0 +1,95 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#E7EFF8"
android:orientation="vertical">
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="167dp"
android:layout_gravity="bottom"
android:layout_marginHorizontal="32dp"
android:layout_marginBottom="64dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatCheckedTextView
android:id="@+id/btnPayQrCode"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/bg_pay_icon"
android:checked="true"
android:drawablePadding="10dp"
android:gravity="center_horizontal"
android:paddingTop="30dp"
android:text="在线支付"
android:textAlignment="center"
android:textColor="@color/color_pay"
android:textSize="30sp"
android:textStyle="bold"
app:drawableTopCompat="@drawable/pay_qrcode" />
<androidx.appcompat.widget.AppCompatCheckedTextView
android:id="@+id/btnPayCash"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="32dp"
android:layout_weight="1"
android:background="@drawable/bg_pay_icon"
android:checked="false"
android:drawablePadding="10dp"
android:gravity="center_horizontal"
android:paddingTop="30dp"
android:text="现金"
android:textAlignment="center"
android:textColor="@color/color_pay"
android:textSize="30sp"
android:textStyle="bold"
app:drawableTopCompat="@drawable/pay_cash" />
<androidx.appcompat.widget.AppCompatCheckedTextView
android:id="@+id/btnPayNumber"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="32dp"
android:layout_weight="1"
android:background="@drawable/bg_pay_icon"
android:checked="false"
android:drawablePadding="11dp"
android:gravity="center_horizontal"
android:paddingTop="30dp"
android:text="消费码"
android:textAlignment="center"
android:textColor="@color/color_pay"
android:textSize="30sp"
android:textStyle="bold"
app:drawableTopCompat="@drawable/pay_number" />
<androidx.appcompat.widget.AppCompatCheckedTextView
android:id="@+id/btnPayFace"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="32dp"
android:layout_weight="1"
android:background="@drawable/bg_pay_icon"
android:checked="false"
android:drawablePadding="11dp"
android:gravity="center_horizontal"
android:paddingTop="30dp"
android:text="刷脸"
android:textAlignment="center"
android:textColor="@color/color_pay"
android:textSize="30sp"
android:textStyle="bold"
app:drawableTopCompat="@drawable/pay_face" />
</LinearLayout>
</LinearLayout>
+56
View File
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="400dp"
android:layout_height="260dp"
android:gravity="center"
android:orientation="vertical"
android:background="@drawable/bg_white_radius12"
tools:ignore="HardcodedText">
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/ic_tip_warn"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/tvWarnContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="30dp"
android:layout_marginVertical="30dp"
android:gravity="center"
tools:text="存在未收货的物品,请确认是否放弃收货,若放弃则数据不会保存?"
android:textColor="#ff141428"
android:textSize="20sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/btnCancel"
android:layout_width="120dp"
android:layout_height="50dp"
android:background="@drawable/bg_white_stroke_blue_ripple"
android:text="取消"
android:gravity="center"
android:textColor="#FF0033CC"
android:textSize="20sp" />
<TextView
android:id="@+id/btnConfirm"
android:layout_width="120dp"
android:layout_height="50dp"
android:layout_marginStart="30dp"
android:background="@drawable/bg_blue_ripple"
android:text="确认"
android:gravity="center"
android:textColor="@color/white"
android:textSize="20sp" />
</LinearLayout>
</LinearLayout>
+3 -2
View File
@@ -12,9 +12,10 @@
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="肉沫干拌面"
android:maxLines="1"
android:ellipsize="end"
android:gravity="center"
android:maxLines="2"
android:text="肉沫干拌面"
android:textColor="#ff031127"
android:textSize="36sp" />
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="180dp"
android:layout_height="180dp"
android:layout_gravity="center"/>
</merge>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:ignore="HardcodedText">
<ImageView
android:layout_width="wrap_content"
android:layout_height="80dp"
android:src="@drawable/ic_empty_view"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginBottom="39dp"
android:text="暂无该物品"
android:textColor="#ff000000"
android:textSize="30sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="请检查物品名称,或稍后重新搜索"
android:textColor="#ff999999"
android:gravity="center"
android:textSize="26sp" />
</LinearLayout>
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/tvFoodName"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_gravity="center"
android:gravity="center_vertical"
android:layout_marginStart="30dp"
android:ellipsize="end"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/ivDeleteFood"
android:maxLines="1"
android:textColor="#ff666666"
android:textSize="26sp"
tools:text="高杆白菜" />
<ImageView
android:id="@+id/ivDeleteFood"
android:layout_width="80dp"
android:layout_height="80dp"
android:paddingHorizontal="25dp"
android:layout_gravity="end"
android:layout_marginHorizontal="5dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvFoodName"
app:layout_constraintStart_toEndOf="@id/tvFoodName"
android:src="@drawable/ic_close" />
<com.google.android.material.divider.MaterialDivider
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginHorizontal="30dp"
app:layout_constraintTop_toBottomOf="@id/tvFoodName"
app:dividerColor="#FFE6E6E6"/>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_margin="3dp"
android:background="@drawable/bg_gray">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="center"
tools:src="@drawable/ic_camera256" />
<ImageView
android:id="@+id/ivDelete"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="end|top"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:src="@drawable/ic_delete_red" />
<ImageView
android:id="@+id/ivFinish"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="end|bottom"
android:padding="5dp"
android:src="@drawable/ic_finish" />
</FrameLayout>
+19
View File
@@ -130,4 +130,23 @@
<item name="windowNoTitle">true</item>
<item name="windowActionBar">false</item>
</style>
<style name="DialogTheme" parent="@android:style/Theme.Dialog">
<!-- 边框 -->
<item name="android:windowFrame">@null</item>
<!-- 是否浮现在activity之上 -->
<item name="android:windowIsFloating">true</item>
<!-- 半透明 -->
<item name="android:windowIsTranslucent">true</item>
<!-- 无标题 -->
<item name="android:windowNoTitle">true</item>
<item name="android:background">@android:color/transparent</item>
<!-- 背景透明 -->
<item name="android:windowBackground">@android:color/transparent</item>
<!-- 模糊 -->
<item name="android:backgroundDimEnabled">true</item>
<!-- 遮罩层 -->
<item name="android:backgroundDimAmount">0.5</item>
</style>
</resources>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-cache-path
name="images"
path="pictures/" />
<external-cache-path
name="cache"
path="." />
</paths>
+8
View File
@@ -28,6 +28,10 @@ lifecycleRuntimeKtx = "2.8.3"
pytorch = "1.13.0"
recyclerview = "1.3.2"
#baseQuickAdapter = "4.1.2"
[libraries]
android-core = { module = "com.google.zxing:android-core", version.ref = "androidCore" }
core = { module = "com.google.zxing:core", version.ref = "core" }
@@ -67,6 +71,10 @@ androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-view
pytorch-android = { group = "org.pytorch", name = "pytorch_android", version.ref = "pytorch" }
pytorch-android-torchvision = { group = "org.pytorch", name = "pytorch_android_torchvision", version.ref = "pytorch" }
#base-quick-adapter-ktx = { module = "io.github.cymchad:BaseRecyclerViewAdapterHelper4", version.ref = "baseQuickAdapter" }
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
@@ -77,7 +77,7 @@ public class FaceRectTransformer {
rect.bottom *= verticalRatio;
Rect newRect = new Rect();
L.e("cameraDisplayOrientation " + cameraDisplayOrientation + " === " + cameraId);
// L.e("cameraDisplayOrientation " + cameraDisplayOrientation + " === " + cameraId);
switch (cameraDisplayOrientation) {
case 0:
if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {