添加菜品采集

This commit is contained in:
mazengfei
2025-10-31 09:53:51 +08:00
parent ed6c5e83be
commit e47b362883
16 changed files with 426 additions and 68 deletions
+5 -2
View File
@@ -105,12 +105,15 @@ dependencies {
implementation(libs.core) implementation(libs.core)
implementation(libs.android.core) implementation(libs.android.core)
implementation (libs.pytorch.android) implementation(libs.pytorch.android)
implementation (libs.pytorch.android.torchvision) implementation(libs.pytorch.android.torchvision)
val objectboxVersion = "5.0.1" val objectboxVersion = "5.0.1"
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion") debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
// releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion") // releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
implementation(libs.androidx.recyclerview)
} }
apply(plugin = "io.objectbox") apply(plugin = "io.objectbox")
+15
View File
@@ -51,6 +51,21 @@
android:name=".activity.MainActivity" android:name=".activity.MainActivity"
android:exported="true"> android:exported="true">
</activity> </activity>
<activity
android:name=".activity.FoodCollectionActivity"
android:exported="true">
</activity>
<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> </application>
</manifest> </manifest>
@@ -1,7 +1,14 @@
package com.sw.dualscreen.activity package com.sw.dualscreen.activity
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.Typeface
import android.net.Uri import android.net.Uri
import android.util.Log
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
@@ -13,17 +20,28 @@ import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.example.utils.FloatBase64Utils
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.sw.dualscreen.R import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.FoodCollectionAdapter 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.ActivityFoodCollectionBinding
import com.sw.dualscreen.databinding.ItemSearchFoodInfoBinding
import com.sw.dualscreen.databinding.ItemSearchFoodInfoBinding.inflate
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.objbox.Food import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.FoodCollectionBean import com.sw.dualscreen.objbox.FoodCollectionBean
import com.sw.dualscreen.objbox.FoodModule import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.ObjectBox import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.presentation.SecondaryScreenPresentation import com.sw.dualscreen.presentation.SecondaryScreenPresentation
import com.sw.dualscreen.sdk.SensorScaleUtils import com.sw.dualscreen.sdk.SensorScaleUtils
import com.sw.dualscreen.utils.CameraHelper
import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.GlideUtils
import com.sw.dualscreen.utils.ImageUtil import com.sw.dualscreen.utils.ImageUtil
import com.sw.dualscreen.utils.StorageUtils import com.sw.dualscreen.utils.StorageUtils
import com.sw.dualscreen.view.CustomBottomSheetDialog import com.sw.dualscreen.view.CustomBottomSheetDialog
@@ -32,6 +50,7 @@ import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import io.objectbox.Box import io.objectbox.Box
import io.objectbox.kotlin.boxFor import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.launch
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
@@ -50,6 +69,7 @@ class FoodCollectionActivity : BaseActivity<ActivityFoodCollectionBinding>() {
private val executor = Executors.newSingleThreadExecutor() private val executor = Executors.newSingleThreadExecutor()
private val viewModel by viewModels<UserViewModel>() private val viewModel by viewModels<UserViewModel>()
private var selectedFoodId: String? = ""
private var selectedFoodName: String? = "" private var selectedFoodName: String? = ""
private var bottomSheetDialog: CustomBottomSheetDialog? = null private var bottomSheetDialog: CustomBottomSheetDialog? = null
@@ -60,11 +80,17 @@ class FoodCollectionActivity : BaseActivity<ActivityFoodCollectionBinding>() {
private var box: Box<Food>? = null private var box: Box<Food>? = null
private val list: MutableList<FoodCollectionBean> = mutableListOf() private val list: MutableList<FoodCollectionBean> = mutableListOf()
private lateinit var cameraHelper: CameraHelper
private lateinit var foodAdapter: GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding>
private val debouncer = Debouncer(2000)
private val adapter: FoodCollectionAdapter by lazy { private val adapter: FoodCollectionAdapter by lazy {
FoodCollectionAdapter(list).apply { FoodCollectionAdapter(list).apply {
setOnItemClickListener { _, _, position -> setOnItemClickListener { _, _, position ->
if (list[position].isShowCamera) { if (list[position].isShowCamera) {
takePhoto() // takePhoto()
cameraHelper.openCamera()
} }
} }
addOnItemChildClickListener(R.id.ivDelete) { _, _, position -> addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
@@ -84,7 +110,28 @@ class FoodCollectionActivity : BaseActivity<ActivityFoodCollectionBinding>() {
} }
override fun initialize() { override fun initialize() {
binding.ivBack.setOnClickListener { finish() }
// 初始化 CameraHelper
cameraHelper = CameraHelper(
context = this,
caller = this,
authority = "${packageName}.fileprovider"
) { uri, path ->
if (list.size < 6) {
val insertIndex = if (list.isEmpty()) 0 else list.size - 1
list.add(insertIndex, FoodCollectionBean(imageUri = uri))
} else {
list[list.size - 1] = FoodCollectionBean(imageUri = uri)
}
adapter.notifyDataSetChanged()
}
binding.ivBack.setOnClickListener {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
list.add(FoodCollectionBean(isShowCamera = true)) list.add(FoodCollectionBean(isShowCamera = true))
binding.rvFoodCollection.let { binding.rvFoodCollection.let {
it.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false) it.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false)
@@ -92,11 +139,7 @@ class FoodCollectionActivity : BaseActivity<ActivityFoodCollectionBinding>() {
} }
binding.btnFoodSearch.setOnClickListener { binding.btnFoodSearch.setOnClickListener {
bottomSheetDialog = CustomBottomSheetDialog.newInstance(viewModel) { searchInfo()
selectedFoodName = it.foodName
binding.tvSelectedFood.text = selectedFoodName
}
bottomSheetDialog!!.show(supportFragmentManager, "CustomBottomSheetDialog")
} }
binding.btnSave.setOnClickListener { binding.btnSave.setOnClickListener {
@@ -110,39 +153,41 @@ class FoodCollectionActivity : BaseActivity<ActivityFoodCollectionBinding>() {
} }
vectorThread() vectorThread()
} }
}
private fun takePhoto() { binding.editFoodName.setOnEditorActionListener { v, actionId, event ->
StorageUtils.requestCameraPermission(this) { if (actionId == EditorInfo.IME_ACTION_SEARCH) {
val outputFile = File(this.filesDir, "IMG_${System.currentTimeMillis()}.jpg") searchInfo()
outputFileUri = val imm =
FileProvider.getUriForFile(this, "${this.packageName}.fileprovider", outputFile) v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
takePictureLauncher.launch(outputFileUri!!) imm.hideSoftInputFromWindow(v.windowToken, 0)
} true
} } else {
false
private var outputFileUri: Uri? = null
@SuppressLint("NotifyDataSetChanged")
private val takePictureLauncher =
registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
if (success) {
//Loading.show(this)
if (list.size < 6) {
list.add(list.size - 1, FoodCollectionBean(imageUri = outputFileUri))
} else {
list[list.size - 1] = FoodCollectionBean(imageUri = outputFileUri)
}
adapter.notifyDataSetChanged()
} }
} }
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() { private fun vectorThread() {
//Loading.show(this) //Loading.show(this)
showWaitingDialog("加载中……") showWaitingDialog("加载中……")
Thread { Thread {
list.forEachIndexed { index, it -> list.forEachIndexed { index, it ->
image2VectorTask(it.imageUri!!, index)
if (!it.isShowCamera)
image2VectorTask(it.imageUri!!, index)
} }
runOnUiThread { runOnUiThread {
window.decorView.postDelayed({ window.decorView.postDelayed({
@@ -169,17 +214,76 @@ class FoodCollectionActivity : BaseActivity<ActivityFoodCollectionBinding>() {
NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB
) )
val outputTensor = module!!.forward(IValue.from(inputTensor)).toTensor() val outputTensor = module!!.forward(IValue.from(inputTensor)).toTensor()
val base64Str = FloatBase64Utils.floatArrayToBase64(outputTensor.dataAsFloatArray)
Timber.tag("mzf1").e(base64Str)
val imageVector = outputTensor.dataAsFloatArray val imageVector = outputTensor.dataAsFloatArray
box?.put(Food(name = selectedFoodName, foodIdx = 0, foodVector = imageVector)) box?.put(Food(name = selectedFoodName, foodIdx = 0, foodVector = imageVector))
list[position].let { list[position].let {
it.imageVector = imageVector it.imageVector = imageVector
it.isFinish = true it.isFinish = true
Timber
viewModel.postImageData(
context,
foodId = selectedFoodId.toString(),
foodName = selectedFoodName.toString(),
foodVector = base64Str,
uri = imageUri
)
} }
runOnUiThread { runOnUiThread {
adapter.notifyItemChanged(position) adapter.notifyItemChanged(position)
} }
Timber.tag("registerDataChange").e("box.all.size=${box?.all?.size}")
}
}
private fun searchInfo() {
debouncer.debounce {
viewModel.searchByFoodName(binding.editFoodName.text.toString())
}
}
override fun registerDataChange() {
super.registerDataChange()
lifecycleScope.launch {
viewModel.searchFoodInfoList.collect {
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()
}
}
)
}
} }
@@ -1,6 +1,7 @@
package com.sw.dualscreen.activity package com.sw.dualscreen.activity
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.Outline import android.graphics.Outline
import android.graphics.Typeface import android.graphics.Typeface
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
@@ -43,6 +44,7 @@ import com.sw.dualscreen.utils.ImageUtil
import com.sw.dualscreen.view.CustomBottomSheetDialog import com.sw.dualscreen.view.CustomBottomSheetDialog
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.LightManager
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -93,6 +95,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
private fun initView() { private fun initView() {
viewModel.activeEngine() viewModel.activeEngine()
binding.foodDisplayLayout.setOnClickListener {
val intent = Intent(this, FoodCollectionActivity::class.java)
startActivity(intent)
finish()
}
binding.previewView.outlineProvider = object : ViewOutlineProvider() { binding.previewView.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) { override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(0, 0, view.width, view.height, 12f.dp) outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
@@ -202,6 +209,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
if (weight - lastWeight > 0.1 && presentation?.mealPickupMode == 0) { // 大于100g if (weight - lastWeight > 0.1 && presentation?.mealPickupMode == 0) { // 大于100g
debouncer.debounce { debouncer.debounce {
LightManager.openRedLight()
LightManager.openGreenLight()
takePhoto { photoUri -> takePhoto { photoUri ->
lastPhotoUri = photoUri lastPhotoUri = photoUri
Timber.d("registerDataChange photoUri = ${photoUri.path}") Timber.d("registerDataChange photoUri = ${photoUri.path}")
@@ -211,7 +220,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
Timber.d("registerDataChange photoUri 拿到bitmap") Timber.d("registerDataChange photoUri 拿到bitmap")
val bmp = BitmapCropper.cropCenter(bitmap, 1300, 900) val bmp = BitmapCropper.cropCenter(bitmap, 1300, 900)
Timber.d("registerDataChange photoUri bitmap裁剪完成") Timber.d("registerDataChange photoUri bitmap裁剪完成")
val file = BitmapSaver.saveToAppFilesDir(bmp, this, "IMG_CROP_${System.currentTimeMillis()}.jpg") val file = BitmapSaver.saveToAppFilesDir(
bmp,
this,
"IMG_CROP_${System.currentTimeMillis()}.jpg"
)
Timber.d("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}") Timber.d("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}")
val resultList = FoodModule.queryFood(bmp) val resultList = FoodModule.queryFood(bmp)
Timber.d("registerDataChange photoUri 拿到识别数据") Timber.d("registerDataChange photoUri 拿到识别数据")
@@ -8,10 +8,15 @@ import com.sw.dualscreen.model.response.EquipmentInfo
import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.UserFaceInfo import com.sw.dualscreen.model.response.UserFaceInfo
import com.sw.dualscreen.model.response.UserNutritionData import com.sw.dualscreen.model.response.UserNutritionData
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Query import retrofit2.http.Query
import retrofit2.http.Url import retrofit2.http.Url
@@ -109,4 +114,17 @@ interface ApiService {
@Query("restId") restId: String, @Query("restId") restId: String,
@Query("foodNames") foodName: String, @Query("foodNames") foodName: String,
): ApiResponse<List<FoodInfo>> ): 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>
} }
@@ -32,9 +32,9 @@ object FoodModule {
// if (box.all.isNotEmpty()) { // if (box.all.isNotEmpty()) {
// box.removeAll() // box.removeAll()
// } // }
if (box.all.isEmpty()) { // if (box.all.isEmpty()) {
initFoodData(context) // initFoodData(context)
} // }
} }
fun queryFood(bitmap: Bitmap, queryCount:Int = 15): List<String> { fun queryFood(bitmap: Bitmap, queryCount:Int = 15): List<String> {
@@ -162,7 +162,7 @@ class SecondaryScreenPresentation(
Timber.d("step1") Timber.d("step1")
currentStep = 1 currentStep = 1
LightManager.closeGreenLight() LightManager.closeGreenLight()
LightManager.openRedLight() LightManager.closeRedLight()
//回复上一次的设置 //回复上一次的设置
// lastWeight = 0.0 // lastWeight = 0.0
dinnerTypeInfo = null dinnerTypeInfo = null
@@ -188,7 +188,7 @@ class SecondaryScreenPresentation(
fun step2(foodInfo: FoodInfo) { fun step2(foodInfo: FoodInfo) {
Timber.d("step2") Timber.d("step2")
currentStep = 2 currentStep = 2
LightManager.openRedLight() LightManager.closeRedLight()
LightManager.openGreenLight() LightManager.openGreenLight()
dinnerTypeInfo = null dinnerTypeInfo = null
userNutritionData = null userNutritionData = null
@@ -231,6 +231,7 @@ class SecondaryScreenPresentation(
fun step3() { fun step3() {
Timber.d("step3") Timber.d("step3")
LightManager.closeRedLight() LightManager.closeRedLight()
LightManager.openGreenLight()
currentStep = 3 currentStep = 3
stepChangeCallback(currentStep) stepChangeCallback(currentStep)
if (currentFood == null) { if (currentFood == null) {
@@ -1,5 +1,7 @@
package com.sw.dualscreen.repository package com.sw.dualscreen.repository
import android.content.Context
import android.net.Uri
import com.sw.dualscreen.GlobalData import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.model.request.UserNutritionParam import com.sw.dualscreen.model.request.UserNutritionParam
import com.sw.dualscreen.model.response.ApiResponse 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.UserFaceInfo
import com.sw.dualscreen.model.response.UserNutritionData import com.sw.dualscreen.model.response.UserNutritionData
import com.sw.dualscreen.network.api.ApiService import com.sw.dualscreen.network.api.ApiService
import com.sw.dualscreen.utils.ImageUtil
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
/** /**
* 远程数据处理 * 远程数据处理
@@ -115,4 +120,27 @@ class RemoteRepository constructor(
): ApiResponse<List<FoodInfo>> { ): ApiResponse<List<FoodInfo>> {
return safeApiCall { apiService.getFoodInfo(restId = restId, foodName = foodName) } 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) }
}
} }
@@ -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,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.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri 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 { 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
}
}
} }
@@ -52,7 +52,10 @@ abstract class BaseViewModel() : ViewModel() {
} }
protected open fun parseResponse(response: ApiResponse<*>): Boolean { protected open fun parseResponse(response: ApiResponse<*>): Boolean {
if (response.isSuccess()) { if (response.isSuccess()
|| response.code == 200
|| response.code == 0
) {
return true return true
} }
Timber.d("msg = ${response.message}, code = ${response.code}") Timber.d("msg = ${response.message}, code = ${response.code}")
@@ -1,5 +1,7 @@
package com.sw.dualscreen.viewmodel package com.sw.dualscreen.viewmodel
import android.content.Context
import android.net.Uri
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.arcsoft.face.ErrorInfo import com.arcsoft.face.ErrorInfo
import com.sw.dualscreen.GlobalData import com.sw.dualscreen.GlobalData
@@ -48,7 +50,7 @@ class UserViewModel : BaseViewModel() {
* 人脸加载完成 * 人脸加载完成
*/ */
private val _loadFaceResult = MutableStateFlow<Boolean>(false) private val _loadFaceResult = MutableStateFlow<Boolean>(false)
val loadFaceResult : StateFlow<Boolean> = _loadFaceResult val loadFaceResult: StateFlow<Boolean> = _loadFaceResult
private val _identifiedFoodInfoList2 = MutableStateFlow<List<FoodInfo>>(emptyList()) private val _identifiedFoodInfoList2 = MutableStateFlow<List<FoodInfo>>(emptyList())
val identifiedFoodInfoList2: StateFlow<List<FoodInfo>> = _identifiedFoodInfoList2 val identifiedFoodInfoList2: StateFlow<List<FoodInfo>> = _identifiedFoodInfoList2
@@ -91,7 +93,7 @@ class UserViewModel : BaseViewModel() {
val nextPageIndex = response.result?.nextPageIndex ?: -1 val nextPageIndex = response.result?.nextPageIndex ?: -1
if (nextPageIndex > 0) { if (nextPageIndex > 0) {
getUserFaceCache(nextPageIndex) getUserFaceCache(nextPageIndex)
} else{ } else {
_loadFaceResult.value = true _loadFaceResult.value = true
} }
} }
@@ -217,4 +219,23 @@ class UserViewModel : BaseViewModel() {
} }
} }
} }
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}")
}
}
}
} }
@@ -8,12 +8,13 @@
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="50dp"> android:layout_height="68dp"
android:layout_marginTop="24dp">
<ImageView <ImageView
android:id="@+id/ivBack" android:id="@+id/ivBack"
android:layout_width="50dp" android:layout_width="50dp"
android:layout_height="50dp" android:layout_height="68dp"
android:padding="10dp" android:padding="10dp"
android:src="@drawable/ic_back" /> android:src="@drawable/ic_back" />
@@ -23,7 +24,7 @@
android:layout_gravity="center" android:layout_gravity="center"
android:text="菜品采集" android:text="菜品采集"
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="18sp" /> android:textSize="26sp" />
</FrameLayout> </FrameLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
@@ -31,6 +32,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="3dp" android:layout_marginHorizontal="3dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="3" app:spanCount="3"
tools:itemCount="6" tools:itemCount="6"
@@ -39,21 +42,24 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="20dp" android:layout_marginLeft="24dp"
android:layout_marginTop="48dp"
android:layout_marginRight="24dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <EditText
android:id="@+id/tvSelectedFood" android:id="@+id/editFoodName"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginHorizontal="6dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/bg_gray"
android:gravity="center_vertical" android:gravity="center_vertical"
android:hint="输入菜品名称"
android:imeOptions="actionSearch"
android:paddingStart="5dp" android:paddingStart="5dp"
android:paddingEnd="5dp" android:paddingEnd="5dp"
android:hint="选择菜品名称" android:singleLine="true"
android:background="@drawable/bg_gray"
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="18sp" /> android:textSize="18sp" />
@@ -61,24 +67,28 @@
android:id="@+id/btnFoodSearch" android:id="@+id/btnFoodSearch"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="6dp" android:layout_marginLeft="12dp"
android:text="菜品检索" android:text="菜品检索"
android:textColor="#ffffff" android:textColor="#ffffff"
android:textSize="16sp" /> android:textSize="16sp" />
</LinearLayout> </LinearLayout>
<Space <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1"/> android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_weight="1" />
<Button <Button
android:id="@+id/btnSave" android:id="@+id/btnSave"
android:layout_width="260dp" android:layout_width="260dp"
android:layout_height="60dp" android:layout_height="60dp"
android:text="保存"
android:layout_marginVertical="20dp"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginVertical="20dp"
android:text="保存"
android:textColor="#ffffff" android:textColor="#ffffff"
android:textSize="20sp" /> android:textSize="20sp" />
+2 -1
View File
@@ -24,9 +24,9 @@
<Spinner <Spinner
android:id="@+id/spinner" android:id="@+id/spinner"
style="@style/customSpinnerStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/customSpinnerStyle"
android:layout_gravity="end" android:layout_gravity="end"
android:padding="0dp" android:padding="0dp"
android:popupBackground="@drawable/shape_for_custom_spinner" /> android:popupBackground="@drawable/shape_for_custom_spinner" />
@@ -43,6 +43,7 @@
android:text="羊肉臊子荞面饸饹" /> android:text="羊肉臊子荞面饸饹" />
<FrameLayout <FrameLayout
android:id="@+id/foodDisplayLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="531dp" android:layout_height="531dp"
android:layout_marginHorizontal="60dp" android:layout_marginHorizontal="60dp"
@@ -1,32 +1,32 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="110dp" android:layout_height="200dp"
android:background="@drawable/bg_gray"
android:layout_margin="3dp" android:layout_margin="3dp"
xmlns:tools="http://schemas.android.com/tools"> android:background="@drawable/bg_gray">
<ImageView <ImageView
android:id="@+id/imageView" android:id="@+id/imageView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:scaleType="center" android:scaleType="center"
tools:src="@drawable/ic_camera256"/> tools:src="@drawable/ic_camera256" />
<ImageView <ImageView
android:id="@+id/ivDelete" android:id="@+id/ivDelete"
android:layout_width="40dp" android:layout_width="48dp"
android:layout_height="40dp" android:layout_height="48dp"
android:layout_gravity="end|top" android:layout_gravity="end|top"
android:paddingTop="10dp" android:paddingTop="10dp"
android:paddingBottom="10dp" android:paddingBottom="10dp"
android:src="@drawable/ic_delete_red"/> android:src="@drawable/ic_delete_red" />
<ImageView <ImageView
android:id="@+id/ivFinish" android:id="@+id/ivFinish"
android:layout_width="30dp" android:layout_width="48dp"
android:layout_height="30dp" android:layout_height="48dp"
android:padding="5dp"
android:layout_gravity="end|bottom" android:layout_gravity="end|bottom"
android:src="@drawable/ic_finish"/> android:padding="5dp"
android:src="@drawable/ic_finish" />
</FrameLayout> </FrameLayout>