如何在CameraX执行照片处理流程时冻结相机预览?
解决CameraX图片处理时预览冻结的问题
好的,咱们来搞定这个问题!你遇到的情况是因为CameraX的Preview和ImageCapture是两个独立的UseCase——当你在后台处理拍摄的图片时,Preview还在持续绑定到生命周期,所以预览会一直实时更新。要实现处理过程中冻结预览,有两种常用的方案,我结合你的代码给你详细讲:
方案一:临时解绑Preview UseCase(简单直接)
这个思路很直白:点击拍摄按钮后,先把Preview从CameraX的生命周期绑定中解绑,这样相机预览就会停止;等图片处理完成(或者出错)后,再重新绑定Preview恢复实时预览。
步骤1:把Preview UseCase保存为成员变量
首先,在你的CameraFragment里添加一个成员变量来持有Preview实例:
class CameraFragment : Fragment() { private lateinit var previewUseCase: Preview // 新增成员变量 // ... 其他代码 }
步骤2:在buildPreviewUseCase中赋值
修改buildPreviewUseCase方法,把创建好的Preview赋值给成员变量:
private fun buildPreviewUseCase(): Preview { val preview = Preview( UseCaseConfigBuilder.buildPreviewConfig( viewFinder.display ) ) preview.setOnPreviewOutputUpdateListener { previewOutput -> updateViewFinderWithPreview(previewOutput) correctPreviewOutputForDisplay(previewOutput.textureSize) } this.previewUseCase = preview // 保存实例 return preview }
步骤3:点击拍摄时解绑,处理完成后重新绑定
修改buildImageCaptureUseCase里的点击事件逻辑:
private fun buildImageCaptureUseCase(): ImageCapture { val capture = ImageCapture( UseCaseConfigBuilder.buildImageCaptureConfig( viewFinder.display ) ) cameraCaptureImageButton.setOnClickListener { // 先解绑Preview,冻结预览 CameraX.unbind(previewUseCase) capture.takePicture( FileCreator.createTempFile(JPEG_FORMAT), Executors.newSingleThreadExecutor(), object : ImageCapture.OnImageSavedListener { override fun onImageSaved(file: File) { val bitmap = BitmapFactory.decodeFile(file.absolutePath) val rotatedBitmap = bitmap.rotate(90) val croppedImage = cropImage(rotatedBitmap, viewFinder, rectangle) val path = saveImage(croppedImage) requireActivity().runOnUiThread { // 处理完成,重新绑定Preview恢复预览 CameraX.bindToLifecycle(this@CameraFragment, previewUseCase) launchGalleryFragment(path) } } override fun onError( imageCaptureError: ImageCapture.ImageCaptureError, message: String, cause: Throwable? ) { requireActivity().runOnUiThread { // 出错也要恢复预览,避免用户一直看不到画面 CameraX.bindToLifecycle(this@CameraFragment, previewUseCase) Toast.makeText(requireContext(), "Error: $message", Toast.LENGTH_LONG).show() Log.e("CameraFragment", "Capture error $imageCaptureError: $message", cause) } } }) } return capture }
方案二:显示最后一帧预览图(视觉更流畅)
如果觉得解绑/重新绑定Preview会有短暂黑屏,你可以选择捕获拍摄瞬间的最后一帧预览图,用一个ImageView覆盖在TextureView上面,这样视觉上是“冻结”的效果,体验更好。
步骤1:添加ImageView到布局
在你的fragment_camera.xml里,给viewFinder(TextureView)添加一个同级的ImageView,初始设置为gone:
<FrameLayout> <androidx.camera.view.PreviewView android:id="@+id/viewFinder" android:layout_width="match_parent" android:layout_height="match_parent"/> <ImageView android:id="@+id/previewFreezeImageView" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" android:visibility="gone"/> </FrameLayout>
步骤2:添加ImageAnalysis捕获预览帧
添加一个ImageAnalysis UseCase来实时捕获预览帧,保存最新的一帧:
class CameraFragment : Fragment() { private var currentPreviewBitmap: Bitmap? = null // ... 其他成员变量 private fun setupCamera() { CameraX.unbindAll() CameraX.bindToLifecycle( this, buildPreviewUseCase(), buildImageCaptureUseCase(), buildImageAnalysisUseCase() // 新增绑定这个UseCase ) } private fun buildImageAnalysisUseCase(): ImageAnalysis { val analysisConfig = UseCaseConfigBuilder.buildImageAnalysisConfig(viewFinder.display) val analysis = ImageAnalysis(analysisConfig) analysis.setAnalyzer(Executors.newSingleThreadExecutor()) { imageProxy -> // 将ImageProxy转换为Bitmap,注意处理旋转 val bitmap = imageProxy.toBitmap() currentPreviewBitmap = bitmap.rotate(90) // 和你处理拍摄图片的旋转一致 imageProxy.close() // 必须调用close释放资源 } return analysis } // 新增扩展方法:ImageProxy转Bitmap private fun ImageProxy.toBitmap(): Bitmap { val buffer = planes[0].buffer buffer.rewind() val bytes = ByteArray(buffer.capacity()) buffer.get(bytes) return BitmapFactory.decodeByteArray(bytes, 0, bytes.size) } }
步骤3:点击拍摄时显示冻结图,处理完成后恢复
修改拍摄按钮的点击逻辑:
cameraCaptureImageButton.setOnClickListener { // 显示最后一帧预览图,冻结视觉效果 currentPreviewBitmap?.let { bitmap -> requireActivity().runOnUiThread { viewFinder.visibility = View.GONE previewFreezeImageView.visibility = View.VISIBLE previewFreezeImageView.setImageBitmap(bitmap) } } capture.takePicture( FileCreator.createTempFile(JPEG_FORMAT), Executors.newSingleThreadExecutor(), object : ImageCapture.OnImageSavedListener { override fun onImageSaved(file: File) { // 你的图片处理逻辑... val bitmap = BitmapFactory.decodeFile(file.absolutePath) val rotatedBitmap = bitmap.rotate(90) val croppedImage = cropImage(rotatedBitmap, viewFinder, rectangle) val path = saveImage(croppedImage) requireActivity().runOnUiThread { // 恢复实时预览 viewFinder.visibility = View.VISIBLE previewFreezeImageView.visibility = View.GONE launchGalleryFragment(path) } } override fun onError(...) { requireActivity().runOnUiThread { // 出错也要恢复预览 viewFinder.visibility = View.VISIBLE previewFreezeImageView.visibility = View.GONE Toast.makeText(requireContext(), "Error: $message", Toast.LENGTH_LONG).show() Log.e("CameraFragment", "Capture error $imageCaptureError: $message", cause) } } }) }
两种方案对比
- 方案一:代码改动少,逻辑简单,但重新绑定Preview时可能会有短暂的黑屏或画面闪烁。
- 方案二:视觉体验更流畅,用户看不到黑屏,但需要额外处理预览帧的捕获和显示,代码稍复杂一点。
你可以根据自己的需求选择合适的方案~
内容的提问来源于stack exchange,提问作者R Rifa Fauzi Komara
相关产品推荐
相关产品推荐

