androidandroid-viewandroid-camerax

How to mirror the PreviewView in CameraX?


Disclaimer: I'm aware of the existence of this question, but it currently stands unresolved and I'm trying to provide extra information without polluting that one with useless answers that won't solve the problem anyway.

I have a custom device with a front camera that is mirrored by default, so I want to display the preview normally and I need to horizontally flip the content of PreviewView, but I'm stuck. Other people in the past have suggested using PreviewView#setScaleX(-1) but it either doesn't work at all or it needs to be called at a very specific point in the code, which I haven't found yet.

The code below is a simplified version of CameraFragment.kt in the official CameraXBasic example; I've added comments where I've already tired calling viewFinder.scaleX = -1f with no success. Honestly I don't really think that the place makes a difference because if I call it with any value other than 1 it works fine with both scaleX and scaleY, but it always ignores the negative sign so it never flips.

private lateinit var viewFinder: PreviewView

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    ...
    viewFinder = view.findViewById(R.id.view_finder)
    // HERE
    viewFinder.post {
        // HERE
        setupCamera()
    }
}

private fun setupCamera() {
    val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
    cameraProviderFuture.addListener(Runnable {
        val cameraProvider = cameraProviderFuture.get()

        val cameraSelector = CameraSelector.Builder()
            .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
            .build()

       val preview = Preview.Builder()
            .build()
            .also {
                // HERE
                it.setSurfaceProvider(viewFinder.surfaceProvider)
            }

       cameraProvider.unbindAll()

        try {
            cameraProvider.bindToLifecycle(this, cameraSelector, preview)
            // HERE
        } catch (exc: Exception) {
            Log.e(TAG, "Use case binding failed", exc)
        }
        // HERE
    }, ContextCompat.getMainExecutor(requireContext()))
    // HERE
}

Solution

  • Depending on the implementation type (COMPATIBLE vs PERFORMANCE) and the device, PreviewView can either use a TextureView or a SurfaceView, in your case I'm assuming PreviewView is using a SurfaceView, you can confirm this by getting access to PreviewView's first child view (View.getChildAt(0)).

    TextureView is just like any other View, which is why when PreviewView uses it, setting its scaleX to -1 should mirror the displayed preview. You can call PreviewView.setScaleX(-1F) once the layout is created (e.g. In onViewCreated()).

    With SurfaceView, it's a bit tricky, as the Surface and the View are independent in some aspects: The Surface is placed behind the window that holds the View, and the View hierarchy handles correctly composing the entire layout by punching a hole in the window in order to display SurfaceView's Surface. This might explain why mirroring the content SurfaceView displays isn't possible, though I'm not sure why zooming in (scaleX set to values > 1) and out (scaleX set to values between 0 and 1) work nonetheless.