Search code examples
androidandroid-fragmentskotlinandroid-camerax

Why View.display returns null?


I am trying to implement CameraX using this tutorial: https://codelabs.developers.google.com/codelabs/camerax-getting-started/#5 My app has one activity, navigation host and two fragments. Also I am using data binding on my fragment. When I try to get display rotation, I get an error.

Here is my layout file:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="viewModel"
            type="com.ayonix.faceticket.camerapreview.CameraPreviewViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".camerapreview.CameraPreviewFragment">

        <TextureView
            android:id="@+id/camera_preview"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintDimensionRatio="9:16"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Here is my fragment code:

class CameraPreviewFragment : Fragment() {

    private lateinit var binding: CameraPreviewFragmentBinding
    private lateinit var viewModel: CameraPreviewViewModel

    private val previewConfig = PreviewConfig.Builder().build()
    private val preview = Preview(previewConfig)

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = CameraPreviewFragmentBinding.inflate(inflater, container, false)
        binding.lifecycleOwner = this

        val viewModelFactory = CameraPreviewViewModelFactory((activity as MainActivity).faceIDUtils)
        viewModel = ViewModelProviders.of(this, viewModelFactory).get(CameraPreviewViewModel::class.java)
        binding.viewModel = viewModel

        startCamera()

        return binding.root
    }

    private fun startCamera() {
        preview.setOnPreviewOutputUpdateListener {
            val parent = binding.cameraPreview.parent as ViewGroup
            parent.removeView(binding.cameraPreview)
            parent.addView(binding.cameraPreview, 0)

            binding.cameraPreview.surfaceTexture = it.surfaceTexture
            updateTransform()
        }

        CameraX.bindToLifecycle(this, preview, viewModel.analyzerUseCase)
    }

    private fun updateTransform() {
        val matrix = Matrix()

        val centerX = binding.cameraPreview.width / 2f
        val centerY = binding.cameraPreview.height / 2f

        val rotationDegrees = when(binding.cameraPreview.display.rotation) {
            Surface.ROTATION_0 -> 0
            Surface.ROTATION_90 -> 90
            Surface.ROTATION_180 -> 180
            Surface.ROTATION_270 -> 270
            else -> return
        }
        matrix.postRotate(-rotationDegrees.toFloat(), centerX, centerY)

        binding.cameraPreview.setTransform(matrix)
    }

}

I have tried to get rotation like this binding.root.display.rotation. But it gives same error too.

Here is the error: java.lang.IllegalStateException: binding.cameraPreview.display must not be null


Solution

  • I found the reason after 3 months. startCamera() must be invoked in onViewCreated. You shouldn't do anything in onCreateView() but inflate layout. The fixed code is like this:

    class CameraPreviewFragment : Fragment() {
    
        private lateinit var binding: CameraPreviewFragmentBinding
        private lateinit var viewModel: CameraPreviewViewModel
    
        private val previewConfig = PreviewConfig.Builder().build()
        private val preview = Preview(previewConfig)
    
        override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            binding = CameraPreviewFragmentBinding.inflate(inflater, container, false)
            binding.lifecycleOwner = this
    
            return binding.root
        }
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            val viewModelFactory = CameraPreviewViewModelFactory((activity as MainActivity).faceIDUtils)
            viewModel =
                ViewModelProviders.of(this, viewModelFactory).get(CameraPreviewViewModel::class.java)
            binding.viewModel = viewModel
    
            startCamera()
        }
    
        private fun startCamera() {
            preview.setOnPreviewOutputUpdateListener {
                val parent = binding.cameraPreview.parent as ViewGroup
                parent.removeView(binding.cameraPreview)
                parent.addView(binding.cameraPreview, 0)
    
                binding.cameraPreview.surfaceTexture = it.surfaceTexture
                updateTransform()
            }
    
            CameraX.bindToLifecycle(this, preview, viewModel.analyzerUseCase)
        }
    
        private fun updateTransform() {
            val matrix = Matrix()
    
            val centerX = binding.cameraPreview.width / 2f
            val centerY = binding.cameraPreview.height / 2f
    
            val rotationDegrees = when (binding.cameraPreview.display.rotation) {
                Surface.ROTATION_0 -> 0
                Surface.ROTATION_90 -> 90
                Surface.ROTATION_180 -> 180
                Surface.ROTATION_270 -> 270
                else -> return
            }
            matrix.postRotate(-rotationDegrees.toFloat(), centerX, centerY)
    
            binding.cameraPreview.setTransform(matrix)
        }
    }