You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 09:29:06