添加了图片拍照及识别逻辑

This commit is contained in:
zxj
2025-08-14 09:34:49 +08:00
parent 1ac73df8d9
commit 10d4a4a41d
16 changed files with 542 additions and 396 deletions
@@ -98,6 +98,16 @@ fun ReceiptProductScreen(
}
}
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
Timber.d("开始传感器采集")
viewModel.startSensorScale()
onDispose {
Timber.d("停止传感器采集")
viewModel.stopSensorScale()
}
}
Column(modifier = modifier) {
TopTitleBar()
Box(
@@ -241,6 +251,10 @@ fun TabScreen(viewModel: ReceiptViewModel) {
val tabs =
listOf("未确认(${purchaseUnadjustedList.size})", "已确认(${purchaseAdjustList.size})")
LaunchedEffect(purchaseUnadjustedList) {
viewModel.updateReceiptList(purchaseUnadjustedList)
}
Column(
modifier = Modifier
.background(
@@ -493,7 +507,6 @@ private fun AdjustedRowInputLayout(
@Composable
fun ReceiptRightView(modifier: Modifier = Modifier, viewModel: ReceiptViewModel) {
val searchResultList by viewModel.searchListItems.collectAsState()
val selectedItem by viewModel.selectedItem.collectAsState()
Column(
modifier = modifier
@@ -507,7 +520,11 @@ fun ReceiptRightView(modifier: Modifier = Modifier, viewModel: ReceiptViewModel)
) {
if (selectedItem == null) {
Spacer(modifier = Modifier.height(30.dp))
IdentityView(list = searchResultList, showSearchView = false, onOptionSelected = {})
IdentityView(
showSearchView = false,
onOptionSelected = {},
baseViewModel = viewModel
)
} else {
ReceiptProductEditView(viewModel)
}
@@ -525,16 +542,6 @@ private fun ReceiptProductEditView(
) {
val selectedItem by viewModel.selectedItem.collectAsState()
val warehouseTypeList = GlobalData.warehouseTypeList
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
Timber.d("开始传感器采集")
viewModel.startSensorScale()
onDispose {
Timber.d("停止传感器采集")
viewModel.stopSensorScale()
}
}
selectedItem?.let {
Column(
@@ -24,10 +24,6 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -256,13 +252,7 @@ fun ContentRightView(
*/
@Composable
fun ProductIdentification(viewModel: SelfProcurementViewModel) {
val searchResultList by viewModel.searchListItems.collectAsState()
// 需要搜索的信息
var searchInfo by remember { mutableStateOf("") }
var pageNum by remember { mutableIntStateOf(1) }
val isMoreLoading by viewModel.isMoreLoading.collectAsState()
val canLoadMore by viewModel.canLoadMore.collectAsState()
val selectedItem by viewModel.selectedItem.collectAsState()
Column(
modifier = Modifier
.width(457.dp)
@@ -275,19 +265,9 @@ fun ProductIdentification(viewModel: SelfProcurementViewModel) {
.fillMaxWidth()
) {
// 菜品识别
IdentityView(list = searchResultList, showSearchView = true, onSearchClick = {
searchInfo = it
viewModel.searchGoodsInfoList(it)
}, onOptionSelected = {
IdentityView(showSearchView = true, onOptionSelected = {
viewModel.updateSelectedItemWithSearch(it)
}, onLoadMore = {
Timber.d("加载更多 canLoadMore = $canLoadMore")
if (!canLoadMore) {
return@IdentityView
}
pageNum++
viewModel.searchGoodsInfoList(goodsName = searchInfo, pageNo = pageNum)
}, isMoreLoading = isMoreLoading, canLoadMore = canLoadMore)
}, checkedItem = selectedItem, baseViewModel = viewModel)
}
Spacer(modifier = Modifier.height(10.dp))
Row(
@@ -873,7 +853,9 @@ private fun SelfProductEditView(
textColor = colorResource(R.color.green),
borderColor = colorResource(R.color.green),
onClick = {
viewModel.updateLastPhotoUri(null)
viewModel.updateSelectedItem(null)
viewModel.updateIdentityList(emptyList())
})
CustomButton(
modifier = Modifier
@@ -1,18 +1,12 @@
package com.sw.inbound.ui.weight
import android.net.Uri
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.Toast
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -21,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -37,8 +30,6 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.LocalLifecycleOwner
import coil.compose.AsyncImage
import com.sw.inbound.GlobalData
@@ -47,8 +38,8 @@ import com.sw.inbound.ext.dashedBorder
import com.sw.inbound.ui.theme.AppTypography
import com.sw.inbound.utils.FileUtils
import com.sw.inbound.utils.ToastUtils
import com.sw.inbound.utils.rememberPhotoCapture
import timber.log.Timber
import java.io.File
/**
* 相机预览 -默认不预览,支持拍照
@@ -72,6 +63,24 @@ fun CameraCaptureLayout(
}
}
// 创建拍照工具实例
val (photoCaptureHelper, takePhoto) = rememberPhotoCapture(
cameraController = cameraController,
onSuccess = { savedUri ->
// 处理拍照成功的逻辑
photoUri = savedUri
Timber.d("photoUri = $photoUri")
Toast.makeText(context, "图片已保存", Toast.LENGTH_SHORT).show()
showCamera = false
GlobalData.imageUri = photoUri
},
onError = { error ->
// 处理拍照失败的逻辑
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
GlobalData.imageUri = null
}
)
Column(
modifier = modifier
.width(428.dp)
@@ -165,38 +174,7 @@ fun CameraCaptureLayout(
ToastUtils.showToast("请先开启图片预览")
return@CustomButton
}
val executor = ContextCompat.getMainExecutor(context)
val cacheDir = context.cacheDir
val photoFile = File.createTempFile(
"IMG_${System.currentTimeMillis()}",
".jpg",
cacheDir
)
val cacheOutputOptions =
ImageCapture.OutputFileOptions.Builder(photoFile).build()
cameraController.takePicture(
cacheOutputOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val savedUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
photoUri = savedUri
Timber.d("photoUri = $photoUri")
Toast.makeText(context, "图片已保存", Toast.LENGTH_SHORT).show()
showCamera = false
GlobalData.imageUri = photoUri
}
override fun onError(exception: ImageCaptureException) {
GlobalData.imageUri = null
Toast.makeText(
context,
"拍照失败: ${exception.message}",
Toast.LENGTH_SHORT
).show()
}
})
takePhoto()
},
borderColor = colorResource(R.color.green),
textColor = colorResource(R.color.white),
@@ -212,53 +190,4 @@ fun CameraCaptureLayout(
cameraController.bindToLifecycle(lifecycleOwner)
onDispose { }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CameraContent(modifier: Modifier, cameraController: LifecycleCameraController) {
val lifecycleOwner = LocalLifecycleOwner.current
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
//在Compose中使用View系统中的PreviewView
AndroidView(
modifier = Modifier
.fillMaxSize(),
factory = { context ->
PreviewView(context).apply {
//设置布局宽度和高度占据全屏
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
//设置背景颜色
setBackgroundColor(android.graphics.Color.BLACK)
//设置渲染的实现模式
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
//设置缩放方式
scaleType = PreviewView.ScaleType.FILL_START
}.also {
it.controller = cameraController
cameraController.bindToLifecycle(lifecycleOwner)
}
},
onReset = {},
onRelease = {
Timber.d("cameraController.unbind()")
cameraController.unbind()
}
)
Image(
modifier = Modifier
.fillMaxSize()
.padding(30.dp),
painter = painterResource(R.mipmap.ic_scan),
contentDescription = "扫描"
)
}
}
}
@@ -1,5 +1,6 @@
package com.sw.inbound.ui.weight
import android.net.Uri
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.compose.foundation.Image
@@ -11,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import coil.compose.AsyncImage
import com.sw.inbound.R
/**
@@ -19,7 +21,8 @@ import com.sw.inbound.R
@Composable
fun CameraPreview(
controller: LifecycleCameraController,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
photoUri: Uri? = null
) {
Box(modifier = modifier) {
AndroidView(
@@ -31,6 +34,13 @@ fun CameraPreview(
},
modifier = Modifier.fillMaxSize()
)
if (photoUri != null) {
AsyncImage(
modifier = Modifier.fillMaxSize(),
model = photoUri,
contentDescription = "照片",
)
}
Image(
modifier = Modifier
.fillMaxSize()
@@ -1,5 +1,6 @@
package com.sw.inbound.ui.weight
import android.widget.Toast
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.compose.foundation.layout.Column
@@ -8,39 +9,103 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.sw.inbound.ext.medium
import com.sw.inbound.model.request.PurchaseWarehouseParam
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.ui.theme.AppTypography
import com.sw.inbound.utils.InteractionUtils.Debouncer
import com.sw.inbound.utils.rememberPhotoCapture
import com.sw.inbound.viewmodel.BaseViewModel
import timber.log.Timber
/**
* 菜品识别组件
*/
@Composable
fun IdentityView(
list: List<SearchGoodsInfo.Record>,
showSearchView: Boolean = false,
onSearchClick: (String) -> Unit = {},
onOptionSelected: (SearchGoodsInfo.Record) -> Unit = {},
onLoadMore: (Int) -> Unit = {},
isMoreLoading: Boolean = false,
canLoadMore: Boolean = true
checkedItem: PurchaseWarehouseParam? = null,
baseViewModel: BaseViewModel,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val weightInfo by baseViewModel.weightInfo.collectAsState()
val identityList by baseViewModel.identityListItems.collectAsState()
val searchList by baseViewModel.searchListItems.collectAsState()
// var lastWeight by remember { mutableStateOf<Double?>(null) }
var lastWeight by remember { mutableDoubleStateOf(0.0) }
val lastPhotoUri by baseViewModel.lastPhotoUri.collectAsState()
var pageNum by remember { mutableIntStateOf(1) }
val isMoreLoading by baseViewModel.isMoreLoading.collectAsState()
val canLoadMore by baseViewModel.canLoadMore.collectAsState()
var list = if (identityList.isNotEmpty()) {
identityList
} else {
searchList
}
val debouncer = remember { Debouncer(2000) }
// CameraX 控制器
val cameraController = remember {
LifecycleCameraController(context).apply {
setEnabledUseCases(CameraController.IMAGE_CAPTURE)
bindToLifecycle(lifecycleOwner)
// 必须设置有效的用例
setEnabledUseCases(
CameraController.IMAGE_CAPTURE or
CameraController.VIDEO_CAPTURE
)
}
}
// 需要搜索的信息
var searchInfo by remember { mutableStateOf("") }
// 创建拍照工具实例
val (photoCaptureHelper, takePhoto) = rememberPhotoCapture(
cameraController = cameraController,
onSuccess = { savedUri ->
// 处理拍照成功的逻辑
baseViewModel.updateLastPhotoUri(savedUri)
Timber.d("photoUri = $savedUri")
baseViewModel.getIdentityList(savedUri)
},
onError = { error ->
// 处理拍照失败的逻辑
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
baseViewModel.updateLastPhotoUri(null)
}
)
// 绑定生命周期
DisposableEffect(lifecycleOwner) {
cameraController.bindToLifecycle(lifecycleOwner)
onDispose { /* 清理资源 */ }
}
LaunchedEffect(weightInfo) {
Timber.d("LaunchedEffect weightInfo1 = $weightInfo, lastWeight = $lastWeight")
if (weightInfo - lastWeight > 1) {
Timber.d("weightInfo2 = $weightInfo")
debouncer.debounce {
takePhoto()
}
}
lastWeight = weightInfo
}
Column(
modifier = Modifier
.fillMaxSize(),
@@ -50,11 +115,15 @@ fun IdentityView(
modifier = Modifier
.width(397.dp)
.height(298.dp),
controller = cameraController
controller = cameraController,
photoUri = lastPhotoUri
)
if (showSearchView) {
Spacer(modifier = Modifier.height(30.dp))
CustomSearchView(onSearchClick = onSearchClick)
CustomSearchView(onSearchClick = {
searchInfo = it
baseViewModel.searchGoodsInfoList(it)
})
}
Spacer(modifier = Modifier.height(30.dp))
if (showSearchView) {
@@ -63,7 +132,15 @@ fun IdentityView(
.height(64.dp)
.width(192.dp), options = list,
onOptionSelected = onOptionSelected,
onLoadMore = onLoadMore,
onLoadMore = {
Timber.d("加载更多 canLoadMore = $canLoadMore")
if (!canLoadMore) {
return@SingleSelectButtonGroup
}
pageNum++
baseViewModel.searchGoodsInfoList(goodsName = searchInfo, pageNo = pageNum)
},
checkedItem = list.find { it.goodsId == checkedItem?.goodsId },
isMoreLoading = isMoreLoading,
canLoadMore = canLoadMore
)
@@ -96,7 +96,7 @@ fun ReceiptUnadjustedListView(
key = { index, it -> it.id ?: index }) { index, it ->
val checkedBg =
if (selectIndex == index) Gray_DCDCF0 else Color.Transparent
if (/*selectIndex == index ||*/ checkedItem?.id == it.id) Gray_DCDCF0 else Color.Transparent
Row(
modifier = Modifier
.background(color = checkedBg)
@@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
@@ -72,10 +72,11 @@ fun SingleSelectButtonGroup(
unSelectedTextStyle: TextStyle = AppTypography.gray96a0aaTextStyle,
onLoadMore: (Int) -> Unit = {},
isMoreLoading: Boolean = false,
canLoadMore: Boolean = true
canLoadMore: Boolean = true,
checkedItem: SearchGoodsInfo.Record? = null
) {
val context = LocalContext.current
var selectedOption by remember { mutableStateOf(options.firstOrNull() ?: "") }
var selectedItem by remember { mutableStateOf(options.firstOrNull() ?: "") }
val lazyGridState = rememberLazyGridState()
var pageNum by remember { mutableIntStateOf(1) }
@@ -109,26 +110,27 @@ fun SingleSelectButtonGroup(
horizontalArrangement = Arrangement.spacedBy(horizontalSpacing),
verticalArrangement = Arrangement.spacedBy(verticalSpacing)
) {
items(items = options, key = { it.goodsId ?: it.goodsCode!! }) { option ->
itemsIndexed(items = options, key = { index, it -> index }) { index, item ->
val isChecked = item.goodsId == checkedItem?.goodsId
Box(
modifier = modifier
.border(
width = 2.dp,
shape = RoundedCornerShape(10.dp),
color = if (option == selectedOption) selectedColor else unselectedColor,
color = if (isChecked) selectedColor else unselectedColor,
)
.clip(RoundedCornerShape(10.dp))
.background(if (option == selectedOption) selectedColor else Color.Transparent)
.background(if (isChecked) selectedColor else Color.Transparent)
.clickable {
selectedOption = option
onOptionSelected(option)
// selectedItem = item
onOptionSelected(item)
},
contentAlignment = Alignment.Center
// .padding(vertical = 20.dp)
) {
Text(
text = option.goodsNameStr,
style = if (option == selectedOption) AppTypography.BlueTextStyle else unSelectedTextStyle,
text = item.goodsNameStr,
style = if (isChecked) AppTypography.BlueTextStyle else unSelectedTextStyle,
modifier = Modifier
// .width(192.dp)
// .height(64.dp)