Search code examples
kotlinandroid-camerax

How to make CameraX Preview freeze when take a photo?


I have a flow with my custom CameraX like this:

  • Open camera preview (live)
  • Click button to take a photo
  • Have a process when clicking that button (convert the path to bitmap, rotated the image, cropping the image automatically, save into device)
  • After running all the process and successfully, send the image to other Fragment and display it into a glide

The question is when running all the process (in step 3) have a delayed 2 seconds and the camera preview still live (not freeze or lock). How to make camera preview freeze or lock when running the process?

This is my code to running camera preview in Camera X:

class CameraFragment : Fragment() {

        override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            // Inflate the layout for this fragment
            return inflater.inflate(R.layout.fragment_camera, container, false)
        }

        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            viewFinder.post { setupCamera() }
        }

        private fun setupCamera() {
            CameraX.unbindAll()
            CameraX.bindToLifecycle(
                this,
                buildPreviewUseCase(),
                buildImageCaptureUseCase()
            )
        }

        private fun buildPreviewUseCase(): Preview {
            val preview = Preview(
                UseCaseConfigBuilder.buildPreviewConfig(
                    viewFinder.display
                )
            )
            preview.setOnPreviewOutputUpdateListener { previewOutput ->
                updateViewFinderWithPreview(previewOutput)
                correctPreviewOutputForDisplay(previewOutput.textureSize)
            }
            return preview
        }

        private fun buildImageCaptureUseCase(): ImageCapture {
            val capture = ImageCapture(
                UseCaseConfigBuilder.buildImageCaptureConfig(
                    viewFinder.display
                )
            )
            cameraCaptureImageButton.setOnClickListener {
                capture.takePicture(
                    FileCreator.createTempFile(JPEG_FORMAT),
                    Executors.newSingleThreadExecutor(),
                    object : ImageCapture.OnImageSavedListener {
                        override fun onImageSaved(file: File) {
                            // I want make a freeze camera preview when execute this before launch *launchGalleryFragment(path)*
                            val bitmap = BitmapFactory.decodeFile(file.absolutePath)
                            val rotatedBitmap = bitmap.rotate(90)
                            val croppedImage = cropImage(rotatedBitmap, viewFinder, rectangle)
                            val path = saveImage(croppedImage)
                            requireActivity().runOnUiThread {
                                launchGalleryFragment(path)
                            }
                        }

                        override fun onError(
                            imageCaptureError: ImageCapture.ImageCaptureError,
                            message: String,
                            cause: Throwable?
                        ) {
                            Toast.makeText(requireContext(), "Error: $message", Toast.LENGTH_LONG)
                                .show()
                            Log.e("CameraFragment", "Capture error $imageCaptureError: $message", cause)
                        }
                    })
            }
            return capture
        }

        private fun launchGalleryFragment(path: String) {
            val action = CameraFragmentDirections.actionLaunchGalleryFragment(path)
            findNavController().navigate(action)
        }

    }

Solution

  • Maybe you can try to unbind the preview use case :

    version 1.0.0-alpha06: CameraX.unbind(preview);

    version > 1.0.0-alpha07: cameraProvider.unbind(preview);

    In your case, you need to save the preview use case into a variable to then unbind it:

    // Declare the preview use case variable (as in the CameraXBasic example)
    private var preview: Preview? = null
    

    Then instantiate the variable (like you did):

    private fun buildPreviewUseCase(): Preview {
        preview = Preview(
            UseCaseConfigBuilder.buildPreviewConfig(
                viewFinder.display
            )
        )
        preview.setOnPreviewOutputUpdateListener { previewOutput ->
            updateViewFinderWithPreview(previewOutput)
            correctPreviewOutputForDisplay(previewOutput.textureSize)
        }
        return preview
    }
    

    Then, when you want to freeze the preview just unbind the use case:

    CameraX.unbind(preview);
    

    EDIT As @Billda said in this post : CameraX - crash when unbinding Preview UseCase :

    To freeze a preview you should not unbind Preview usecase. There may API for that in the future, but currently the recommended way is to store latest frame from ImageAnalysis and put it to ImageView overlapping the preview.

    So I decided to update my answer to give another solution implementing an analyser with ImageAnalysis (1.0.0-beta02).

    1- Create the FreezeAnalyzer class:

    class FreezeAnalyzer(private val callback: FreezeCallback) : ImageAnalysis.Analyzer {
        private var flag = false
    
        override fun analyze(image: ImageProxy) {
            if(flag){
                flag = false
                val bitmap = toBitmap(image)
                callback.onLastFrameCaptured(bitmap)
            }
            image.close()
        }
    
        fun freeze(){
            flag = true
        }
    
        private fun toBitmap(image: ImageProxy): Bitmap {
            // Convert the imageProxy to Bitmap
            // ref https://stackoverflow.com/questions/56772967/converting-imageproxy-to-bitmap
            // ISSUE, on my android 7 when converting the imageProxy to Bitmap I have a problem with the colors...
            var bitmap = ...
    
            // Rotate the bitmap
            val rotationDegrees = image.imageInfo.rotationDegrees.toFloat()
            if (rotationDegrees != 0f) {
                val matrix = Matrix()
                matrix.postRotate(rotationDegrees)
                bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
            }
            return bitmap
        }
    }
    

    2- XML

    <androidx.camera.view.PreviewView
        android:id="@+id/preview_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <ImageView
        android:id="@+id/image_view"
        android:visibility="invisible"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    

    3- Initialize the imageAnalyser

    val resolutionSize = Size(preview_view.width, preview_view.height)
    
    // Set up analyser
    imageAnalysis = ImageAnalysis.Builder().apply {
        setTargetResolution(resolutionSize)
        setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    }.build()
    
    val analyzer = FreezeAnalyzer(object : FreezeCallback {
        override fun onLastFrameCaptured(bitmap: Bitmap) {
            runOnUiThread {
                preview_view.visibility = View.INVISIBLE
                image_view.visibility = View.VISIBLE
                image_view.setImageBitmap(bitmap)
            }
        }
    })
    imageAnalysis.setAnalyzer(executor, analyzer)
    

    4- Bind the imageAnalysis use case

    try {
        val camera = cameraProvider.bindToLifecycle(
            this,
            cameraSelector,
            preview,
            imageAnalysis,
            imageCapture
        )
        preview.setSurfaceProvider(preview_view.createSurfaceProvider(camera.cameraInfo))
    }
    

    5- Capture the picture

    btn_capture.setOnClickListener {
        file = File(externalMediaDirs.first(), "${System.currentTimeMillis()}.jpg")
        val outputFileOptions: ImageCapture.OutputFileOptions =
            ImageCapture.OutputFileOptions.Builder(file!!).build()
        analyzer.freeze()
        imageCapture.takePicture(outputFileOptions, executor, onImageSavedCallback)
    }
    

    6- Release

    btn_release.setOnClickListener {
        preview_view.visibility = View.VISIBLE
        image_view.visibility = View.INVISIBLE
    }
    

    I hope it helps, I'm not an expert so if you have some improvements you are welcome !