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.visible
import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.utils.FaceDbImporter
import com.sw.dualscreen.utils.FoodVectorTool
import com.sw.dualscreen.utils.L
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.plate.utils.AppUtil
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.withContext
class InitActivity : BaseActivity<ActivityInitBinding>() {
companion object {
@@ -69,28 +65,6 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
}
showWaitingDialog("网络连接中...")
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() {
@@ -118,7 +92,6 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
}
}
}
checkCameraPermissionAndGo()
}
private fun goMainActivity() {
@@ -264,24 +237,24 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
private fun loadTestDb() {
// 从测试库导入人脸数据
FaceDbImporter.importFromAssets(this, object : FaceDbImporter.ImportCallback {
public override fun onProgress(current: Int, total: Int) {
showWaitingDialog("导入人脸数据 " + current + "/" + total)
}
public override fun onError(message: String?) {
hideWaitingDialog()
ToastUtils.showToast("离线数据导入失败:" + message)
}
public override fun onComplete() {
hideWaitingDialog()
ToastUtils.showToast("离线数据导入成功")
// 后续流程会自动跳转
}
})
}
// private fun loadTestDb() {
// // 从测试库导入人脸数据
// FaceDbImporter.importFromAssets(this, object : FaceDbImporter.ImportCallback {
// public override fun onProgress(current: Int, total: Int) {
// showWaitingDialog("导入人脸数据 " + current + "/" + total)
// }
//
// public override fun onError(message: String?) {
// hideWaitingDialog()
// ToastUtils.showToast("离线数据导入失败:" + message)
// }
//
// public override fun onComplete() {
// hideWaitingDialog()
// ToastUtils.showToast("离线数据导入成功")
// // 后续流程会自动跳转
// }
// })
// }
}
@@ -34,6 +34,7 @@ import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.ImageUploader
import com.sw.dualscreen.utils.ImageUtil
import com.sw.plate.utils.ToastUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
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 {
val index = foodCollectionList.indexOfFirst { it.imageFile == null }
if (index == -1) {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
settingActivity?.hideWaitingDialog()
rerurn@ cameraCallback
return@cameraCallback
}
activity?.runOnUiThread {
activity?.lifecycleScope?.launch(Dispatchers.Main) {
foodCollectionList[index].let {
it.imageVector = null
it.bitmap = null
@@ -116,7 +117,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
collectionAdapter.notifyItemChanged(index)
}
// settingActivity?.hideWaitingDialog()
activity?.lifecycleScope?.launch {
activity?.lifecycleScope?.launch(Dispatchers.IO) {
ImageUtil.uriToBitmap(requireActivity(), uri)?.let { bitmap ->
getImageVector(index, bitmap)
}
@@ -149,19 +150,16 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
)
settingActivity?.log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
activity?.runOnUiThread {
foodCollectionList[index].let {
it.imageVector = imageVector
it.bitmap = null
it.isShowCamera = false
it.imageFile = file
}
collectionAdapter.notifyItemChanged(index)
foodCollectionList[index].let {
it.imageVector = imageVector
it.bitmap = null
it.isShowCamera = false
it.imageFile = file
}
settingActivity?.hideWaitingDialog()
if (bitmap.isRecycled.not()) {
bitmap.recycle()
}
settingActivity?.hideWaitingDialog()
}
@SuppressLint("NotifyDataSetChanged")
@@ -171,19 +169,15 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
ToastUtils.showToast("每次只允许保存${MAX_COUNT}条数据")
return
}
settingActivity?.showWaitingDialog("采集中……")
cameraUtils.takePhoto(cameraCallback) { errMsg ->
cameraUtils.takePhoto(succCallback = { uri ->
settingActivity?.lifecycleScope?.launch((Dispatchers.IO)) {
cameraCallback(uri)
}
}, failCallback = { errMsg ->
settingActivity?.hideWaitingDialog()
// ToastUtils.showToast("拍照异常,请重新操作")
// if (cameraErrorCount >= 10) {
// ToastUtils.showToast("拍照异常,请重新操作")
// settingActivity?.log("拍照异常,请重新操作:$errMsg")
// cameraErrorCount = 0
// return@takePhoto
// }
// cameraErrorCount++
// takePhoto()
}
})
}
private var cameraErrorCount = 0
@@ -153,7 +153,9 @@ object FoodModule {
suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 50): List<IdNameScore> {
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}")
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
@@ -34,7 +34,6 @@ class CameraUtils(private var activity: ComponentActivity) {
photoCaptureHelper = PhotoCaptureHelper(
context = activity,
cameraController = cameraController!!,
onSuccess = {},
onError = { msg ->
//toast(msg)
failCallback?.invoke(msg)
@@ -15,17 +15,23 @@ object ImageUtil {
fun uriToBitmap(context: Context, uri: Uri): Bitmap? {
return try {
val options = BitmapFactory.Options()
//options.inSampleSize = 2; // 这会将图片的尺寸缩小到原来的1/2
options.inJustDecodeBounds = false
// options.inPreferredConfig = Bitmap.Config.ARGB_8888
options.inPreferredConfig = Bitmap.Config.RGB_565
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
// 先只读尺寸,计算合适的采样率
context.contentResolver.openInputStream(uri)?.use { stream ->
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 ->
BitmapFactory.decodeStream(stream)
BitmapFactory.decodeStream(stream, null, options)
}
} catch (e: Exception) {
e.printStackTrace()
@@ -20,13 +20,8 @@ import java.io.File
class PhotoCaptureHelper(
private val context: Context,
private val cameraController: CameraController,
private val onSuccess: (Uri) -> Unit = {},
private val onError: (String) -> Unit = {}
) {
companion object {
/** 临时测试开关:true 时跳过拍照,直接返回固定图片Uri */
const val TEST_MODE = true
}
private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf()
fun addSuccessCallback(callback:(Uri) -> Unit) {
@@ -49,28 +44,6 @@ class PhotoCaptureHelper(
fileNamePrefix: String = "IMG_",
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("开始拍照采集")
try {
@@ -91,7 +64,6 @@ class PhotoCaptureHelper(
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
Timber.d("照片保存成功: $photoUri")
onSuccess(photoUri)
callbackList.forEach {
it(photoUri)
}