refactor(camera): 优化相机拍照功能实现

- 移除 PhotoCaptureHelper 中的测试模式和无用的 onSuccess 回调参数
- 将 CollectFragment 中的 UI 操作迁移到正确的协程调度器 (Main 和 IO)
- 使用 lifecycleScope 替代 runOnUiThread 确保生命周期安全
- 重构 ImageUtil.uriToBitmap 方法,添加图片尺寸压缩逻辑以减少内存占用
- 从 InitActivity 中移除已注释的人脸识别相关初始化代码
- 修复 cameraCallback 中的拼写错误 (rerurn@ -> return@)
This commit is contained in:
2026-05-29 13:52:07 +08:00
parent 737baf8486
commit 37f86e202f
6 changed files with 53 additions and 107 deletions
@@ -14,7 +14,6 @@ import com.sw.dualscreen.ext.clickWithDebounce
import com.sw.dualscreen.ext.invisible import com.sw.dualscreen.ext.invisible
import com.sw.dualscreen.ext.visible import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.objbox.FoodModule import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.utils.FaceDbImporter
import com.sw.dualscreen.utils.FoodVectorTool import com.sw.dualscreen.utils.FoodVectorTool
import com.sw.dualscreen.utils.L import com.sw.dualscreen.utils.L
import com.sw.dualscreen.utils.NetworkUtils import com.sw.dualscreen.utils.NetworkUtils
@@ -23,10 +22,7 @@ import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.NetViewModelV2 import com.sw.dualscreen.viewmodel.NetViewModelV2
import com.sw.plate.utils.AppUtil import com.sw.plate.utils.AppUtil
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.faceserver.FaceServer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class InitActivity : BaseActivity<ActivityInitBinding>() { class InitActivity : BaseActivity<ActivityInitBinding>() {
companion object { companion object {
@@ -69,28 +65,6 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
} }
showWaitingDialog("网络连接中...") showWaitingDialog("网络连接中...")
checkNetworkTimer?.start() checkNetworkTimer?.start()
// val isSuccess = viewModel.checkEquipmentInfo()
// if (isSuccess) {
// goMainActivity()
// return
// }
// binding.imgQr.setImageBitmap(
// QRCodeUtil.generateQRCode(
// content = GlobalData.deviceId,
// size = 200
// )
// )
// binding.initButton.setOnClickListener {
// viewModel.getDeviceToken()
// }
lifecycleScope.launch {
withContext(Dispatchers.IO) {
FaceServer.getInstance().clearAllFaces()
loadTestDb()
}
}
} }
override fun registerDataChange() { override fun registerDataChange() {
@@ -118,7 +92,6 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
} }
} }
} }
checkCameraPermissionAndGo()
} }
private fun goMainActivity() { private fun goMainActivity() {
@@ -264,24 +237,24 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
private fun loadTestDb() { // private fun loadTestDb() {
// 从测试库导入人脸数据 // // 从测试库导入人脸数据
FaceDbImporter.importFromAssets(this, object : FaceDbImporter.ImportCallback { // FaceDbImporter.importFromAssets(this, object : FaceDbImporter.ImportCallback {
public override fun onProgress(current: Int, total: Int) { // public override fun onProgress(current: Int, total: Int) {
showWaitingDialog("导入人脸数据 " + current + "/" + total) // showWaitingDialog("导入人脸数据 " + current + "/" + total)
} // }
//
public override fun onError(message: String?) { // public override fun onError(message: String?) {
hideWaitingDialog() // hideWaitingDialog()
ToastUtils.showToast("离线数据导入失败:" + message) // ToastUtils.showToast("离线数据导入失败:" + message)
} // }
//
public override fun onComplete() { // public override fun onComplete() {
hideWaitingDialog() // hideWaitingDialog()
ToastUtils.showToast("离线数据导入成功") // ToastUtils.showToast("离线数据导入成功")
// 后续流程会自动跳转 // // 后续流程会自动跳转
} // }
}) // })
} // }
} }
@@ -34,6 +34,7 @@ import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.ImageUploader import com.sw.dualscreen.utils.ImageUploader
import com.sw.dualscreen.utils.ImageUtil import com.sw.dualscreen.utils.ImageUtil
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
@@ -97,15 +98,15 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
} }
private val cameraCallback: (Uri) -> Unit = { uri -> private val cameraCallback: (Uri) -> Unit = cameraCallback@{ uri ->
try { try {
val index = foodCollectionList.indexOfFirst { it.imageFile == null } val index = foodCollectionList.indexOfFirst { it.imageFile == null }
if (index == -1) { if (index == -1) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据") ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
settingActivity?.hideWaitingDialog() settingActivity?.hideWaitingDialog()
rerurn@ cameraCallback return@cameraCallback
} }
activity?.runOnUiThread { activity?.lifecycleScope?.launch(Dispatchers.Main) {
foodCollectionList[index].let { foodCollectionList[index].let {
it.imageVector = null it.imageVector = null
it.bitmap = null it.bitmap = null
@@ -116,7 +117,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
collectionAdapter.notifyItemChanged(index) collectionAdapter.notifyItemChanged(index)
} }
// settingActivity?.hideWaitingDialog() // settingActivity?.hideWaitingDialog()
activity?.lifecycleScope?.launch { activity?.lifecycleScope?.launch(Dispatchers.IO) {
ImageUtil.uriToBitmap(requireActivity(), uri)?.let { bitmap -> ImageUtil.uriToBitmap(requireActivity(), uri)?.let { bitmap ->
getImageVector(index, bitmap) getImageVector(index, bitmap)
} }
@@ -149,19 +150,16 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
) )
settingActivity?.log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}") settingActivity?.log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
activity?.runOnUiThread {
foodCollectionList[index].let { foodCollectionList[index].let {
it.imageVector = imageVector it.imageVector = imageVector
it.bitmap = null it.bitmap = null
it.isShowCamera = false it.isShowCamera = false
it.imageFile = file it.imageFile = file
} }
collectionAdapter.notifyItemChanged(index) settingActivity?.hideWaitingDialog()
}
if (bitmap.isRecycled.not()) { if (bitmap.isRecycled.not()) {
bitmap.recycle() bitmap.recycle()
} }
settingActivity?.hideWaitingDialog()
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
@@ -171,19 +169,15 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据") ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
return return
} }
settingActivity?.showWaitingDialog("采集中……") settingActivity?.showWaitingDialog("采集中……")
cameraUtils.takePhoto(cameraCallback) { errMsg -> cameraUtils.takePhoto(succCallback = { uri ->
settingActivity?.hideWaitingDialog() settingActivity?.lifecycleScope?.launch((Dispatchers.IO)) {
// ToastUtils.showToast("拍照异常,请重新操作") cameraCallback(uri)
// if (cameraErrorCount >= 10) {
// ToastUtils.showToast("拍照异常,请重新操作")
// settingActivity?.log("拍照异常,请重新操作:$errMsg")
// cameraErrorCount = 0
// return@takePhoto
// }
// cameraErrorCount++
// takePhoto()
} }
}, failCallback = { errMsg ->
settingActivity?.hideWaitingDialog()
})
} }
private var cameraErrorCount = 0 private var cameraErrorCount = 0
@@ -153,7 +153,9 @@ object FoodModule {
suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 50): List<IdNameScore> { suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 50): List<IdNameScore> {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
val floatArray = bitmap2FloatArray(bitmap, false) val floatArray = withContext(Dispatchers.IO) {
bitmap2FloatArray(bitmap, false)
}
Timber.tag(TAG).d("bitmap2FloatArray,耗时:${System.currentTimeMillis() - startTime}") Timber.tag(TAG).d("bitmap2FloatArray,耗时:${System.currentTimeMillis() - startTime}")
val nameScoreList = queryFoodNameScore(floatArray, queryCount) val nameScoreList = queryFoodNameScore(floatArray, queryCount)
@@ -34,7 +34,6 @@ class CameraUtils(private var activity: ComponentActivity) {
photoCaptureHelper = PhotoCaptureHelper( photoCaptureHelper = PhotoCaptureHelper(
context = activity, context = activity,
cameraController = cameraController!!, cameraController = cameraController!!,
onSuccess = {},
onError = { msg -> onError = { msg ->
//toast(msg) //toast(msg)
failCallback?.invoke(msg) failCallback?.invoke(msg)
@@ -15,17 +15,23 @@ object ImageUtil {
fun uriToBitmap(context: Context, uri: Uri): Bitmap? { fun uriToBitmap(context: Context, uri: Uri): Bitmap? {
return try { return try {
val options = BitmapFactory.Options() val options = BitmapFactory.Options().apply {
//options.inSampleSize = 2; // 这会将图片的尺寸缩小到原来的1/2 inJustDecodeBounds = true
options.inJustDecodeBounds = false }
// options.inPreferredConfig = Bitmap.Config.ARGB_8888 // 先只读尺寸,计算合适的采样率
options.inPreferredConfig = Bitmap.Config.RGB_565
context.contentResolver.openInputStream(uri)?.use { stream -> context.contentResolver.openInputStream(uri)?.use { stream ->
BitmapFactory.decodeStream(stream, null, options) BitmapFactory.decodeStream(stream, null, options)
// BitmapFactory.decodeStream(stream) }
// 目标最长边不超过 800px,减少内存占用
val maxDim = maxOf(options.outWidth, options.outHeight)
val sampleSize = (maxDim / 800).coerceAtLeast(1)
options.apply {
inJustDecodeBounds = false
inSampleSize = sampleSize
inPreferredConfig = Bitmap.Config.RGB_565
} }
context.contentResolver.openInputStream(uri)?.use { stream -> context.contentResolver.openInputStream(uri)?.use { stream ->
BitmapFactory.decodeStream(stream) BitmapFactory.decodeStream(stream, null, options)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -20,13 +20,8 @@ import java.io.File
class PhotoCaptureHelper( class PhotoCaptureHelper(
private val context: Context, private val context: Context,
private val cameraController: CameraController, private val cameraController: CameraController,
private val onSuccess: (Uri) -> Unit = {},
private val onError: (String) -> Unit = {} private val onError: (String) -> Unit = {}
) { ) {
companion object {
/** 临时测试开关:true 时跳过拍照,直接返回固定图片Uri */
const val TEST_MODE = true
}
private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf() private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf()
fun addSuccessCallback(callback:(Uri) -> Unit) { fun addSuccessCallback(callback:(Uri) -> Unit) {
@@ -49,28 +44,6 @@ class PhotoCaptureHelper(
fileNamePrefix: String = "IMG_", fileNamePrefix: String = "IMG_",
fileExtension: String = ".jpg" fileExtension: String = ".jpg"
) { ) {
// TODO: 测试完成后删除此段代码,并将 TEST_MODE 置为 false
if (TEST_MODE) {
val testDir = context.externalCacheDir ?: context.cacheDir
val testFile = File(testDir, "test_photo_${System.currentTimeMillis()}.jpg")
if (!testFile.exists()) {
testFile.parentFile?.mkdirs()
val bitmap = android.graphics.Bitmap.createBitmap(480, 640, android.graphics.Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(bitmap)
canvas.drawColor(0xFFCCCCCC.toInt())
testFile.outputStream().use { fos ->
bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 95, fos)
}
bitmap.recycle()
}
val authority = "${context.packageName}.fileprovider"
val testUri = FileProvider.getUriForFile(context, authority, testFile)
Timber.d("测试模式:跳过拍照,返回 content://Uri: $testUri")
onSuccess(testUri)
callbackList.forEach { it(testUri) }
return
}
Timber.d("开始拍照采集") Timber.d("开始拍照采集")
try { try {
@@ -91,7 +64,6 @@ class PhotoCaptureHelper(
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile) val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
Timber.d("照片保存成功: $photoUri") Timber.d("照片保存成功: $photoUri")
onSuccess(photoUri)
callbackList.forEach { callbackList.forEach {
it(photoUri) it(photoUri)
} }