Home > Mobile >  How to make to a camera preview filled all available screen space?
How to make to a camera preview filled all available screen space?

Time:04-01

I've created a simple app with the bottom navigation bar and few fragments where one fragment (ArFragment) shows a camera preview:

Screenshot

MainActivity.kt:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        installSplashScreen()

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val bottomBar = findViewById<BottomNavigationView>(R.id.bottom_navigation)
        val navHostFragment =
            supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        bottomBar.setupWithNavController(navHostFragment.navController)
    }
}

activity_main.xml:

<LinearLayout
    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"
    android:id="@ id/main_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".activities.MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@ id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@ id/bottom_navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?android:attr/windowBackground"
        app:menu="@menu/bottom_navigation_menu" />

</LinearLayout>

ArFragment.kt:

class ArFragment : Fragment() {
    private lateinit var cameraExecutor: ExecutorService
    private lateinit var binding: FragmentArBinding
    private val cameraPermissionResultReceiver =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) {
            if (it) {
                startCamera()
            } else {
                binding.viewFinder.visibility = View.GONE
                binding.errorTextView.visibility = View.VISIBLE
                binding.frameLayout.invalidate()
            }
        }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentArBinding.inflate(inflater, container, false)
        // Request camera permissions
        when {
            allPermissionsGranted() -> startCamera()
            shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
                showRationaleDialog()
            }
            else -> cameraPermissionResultReceiver.launch(Manifest.permission.CAMERA)
        }
        cameraExecutor = Executors.newSingleThreadExecutor()
        return binding.root
    }

    private fun showRationaleDialog() {
        AlertDialog.Builder(requireContext())
            .setTitle(R.string.rationale_title_camera)
            .setMessage(R.string.rationale_body_camera)
            .setPositiveButton(R.string.action_ok) { _, _ ->
                cameraPermissionResultReceiver.launch(Manifest.permission.CAMERA)
            }
            .create()
            .show()
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }

    private fun startCamera() {
        context?.let { ctx ->
            val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx)
            cameraProviderFuture.addListener({
                // Used to bind the lifecycle of cameras to the lifecycle owner
                val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

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

                // Select back camera as a default
                val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

                try {
                    // Unbind use cases before rebinding
                    cameraProvider.unbindAll()

                    // Bind use cases to camera
                    cameraProvider.bindToLifecycle(
                        this, cameraSelector, preview
                    )

                } catch (exc: Exception) {
                    Log.e(TAG, "Use case binding failed", exc)
                }

            }, ContextCompat.getMainExecutor(ctx))
        }
    }

    private fun allPermissionsGranted() = ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED

    companion object {
        private const val TAG = "ArFragment"
    }
}

fragment_ar.xml:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@ id/frameLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".fragments.ArFragment">

    <androidx.camera.view.PreviewView
        android:id="@ id/viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <TextView
        android:id="@ id/errorTextView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:layout_margin="24dp"
        android:visibility="gone"
        android:text="@string/error_no_camera_permissions" />

</FrameLayout>

The camera preview works perfectly but I can't make it to fit all screen space, please, don't confuse with a fullscreen mode. I want to make status, tab and navigation bars transparent for ArFragment (and only for ArFragment). In short, to the camera preview became like a root layer background. A something similar to our iOS version:

Screenshot 2

P.S. I've tried solutions from similar questions. To use android:fitsSystemWindows="true" and so on. It works, it makes them transparent but I need to the camera preview could fill all available screen space. At this moment I think that the camera output must be rendered as a root layout background but I am not sure if it's a right way. And even if it's a right way, I don't know how to implement it.

CodePudding user response:

I'm doing something similar to this. Not exactly the same, but hopefully this is close enough to working instructions to get you there.

You need to make the fragment view fill the whole parent, so it is behind the navigation bar. But you also need to change it to be above the navigation bar when you're on a fragment that isn't the camera view, or else some of your content will be overlayed with the navigation bar and bottom system bar.

I would use a ConstraintLayout to achieve this.

You need to switch from fragment to androidx.fragment.app.FragmentContainerView so you'll be able to modify layout params to switch between the two states.

Also, make the bottom navigation view 0dp elevation because if it's transparent, its elevation shadow will look super weird.

The bottom navigation view will provide the color that will also go behind the bottom system bar

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@ id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.fragment.app.FragmentContainerView
        android:id="@ id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:elevation="0dp"
        app:navGraph="@navigation/nav_graph" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@ id/nav_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:elevation="0dp"
        app:menu="@menu/bottom_navigation_menu" />

</androidx.constraintlayout.widget.ConstraintLayout>

Your theme should have the following properties. This makes the system navigation bar invisible (or at least translucent prior on pre-Android Q devices). On pre-Android L devices, I think it doesn't do anything and you're just out of luck for making it transparent.

<item name="android:navigationBarColor" tools:targetApi="l">@android:color/transparent</item>
<item name="android:windowDrawsSystemBarBackgrounds" tools:targetApi="l">true</item>
<item name="android:enforceNavigationBarContrast" tools:targetApi="q">false</item>

In your Activity's onCreate add this line, which is also necessary for the translucent/transparent bottom navigation bar.

WindowCompat.setDecorFitsSystemWindows(window, false)

And you need to replace findNavController(nav_host_fragment) with this since we switched from a fragment element to a FragmentContainerView:

(supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController

Finally, create a function in the Activity that you'll call whenever switching away or to the camera fragment. (You can use navController.addOnDestinationChangedListener to call this.)

private fun setFragmentBehindNavigation(behind: Boolean) {
    // swap where the bottom edge of fragment is aligned
    binding.navHostFragmentActivityMain.apply {
        layoutParams = (layoutParams as ConstraintLayout.LayoutParams).apply {
            if (behind) {
                bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
                bottomToTop = ConstraintLayout.LayoutParams.UNSET
            } else {
                bottomToBottom = ConstraintLayout.LayoutParams.UNSET
                bottomToTop = R.id.nav_view
            }
        }
    }

    // swap between opaque and translucent bottom nav color
    binding.navView.setBackgroundColor(
        resources.getColor(
            if (fits) R.color.someTranslucentColor else R.color.someOpaqueColor
        )
    )
}

We still haven't handled the problem of your views going behind the top status bar when you aren't showing the camera preview. If you're using a Toolbar on all your other fragments, I think it will be fine because Toolbars know how to use fitsSystemWindows (not all types of view can handle it correctly). If you're not, one solution is to insert a plain old View between the top of the parent constraint layout and the top of the fragment container using constraints. Then at runtime, you set this view to the height of the top status bar. Set it between GONE and VISIBLE in the setFragmentBehindNavigation function above.

The hard part is making it the same height as the system bar. I've run out of time I can devote to this answer (I thought it would be quick, but it's already been over an hour!) so I can't add any more instructions for that, but you can read this article about how to do it. It's very complicated!

CodePudding user response:

use getActionbar.hide() to avoid showing actionbar on screen and if your action bar is hided the camera will expanded to take whole screen

  • Related