androidandroid-layoutandroid-constraintlayoutandroid-viewbinding

Cannot dynamically adjust z-index of ConstraintLayout children when using a camera stream & map


In my layout i display a stream widget (the WidgetFPV one) which contains a surface onto which a camera stream is projected via DJI'S MSDK5 sdk. I also have a card mapCard which hosts a MapBox map fragment that displays a map. I'm now allowing users to click on the map (which is minimized to the bottom-left corner of the screen) in order to maximize it and consequently I minimize the FPV stream. However, I'm noticing that when the map is expanded and thus the stream minimized, the stream then gets hidden behind the expanded map despite me setting the elevation to a higher value than the map.

If i set the mapCard to View.INVISIBLE then I can see the stream "behind" it. Any ideas what I'm doing wrong? I also tried the following:

  1. Using bringToFront on the FPV widget for example and also calling clRoot.invalidate()
  2. Using the cardElevation and elevation setters as well on the mapCard
  3. Using setZ() and/or setTranslationZ()

All to no avail sadly.

layout.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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/clRoot"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.pckg.dji.widgets.ui.fpv.FPVWidget
        android:id="@+id/widgetFPV"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:keepScreenOn="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <other components of the layout>

    <androidx.cardview.widget.CardView
        android:id="@+id/mapCard"
        android:layout_width="@dimen/dji_widgetMap_width"
        android:layout_height="@dimen/dji_widgetMap_height"
        android:layout_marginStart="@dimen/core_margin_s"
        android:layout_marginBottom="@dimen/core_margin_s"
        app:cardCornerRadius="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent">

        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/map"
            android:name="com.pkg.dji.ui.map.Dji5MapFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:layout="@layout/fragment_dji_map" />
    </androidx.cardview.widget.CardView>
....
</ConstraintLayout>

The code that adjusts the elevation and constraints dynamically is the following:

private fun handleMapExpansionToggle(isMapExpanded: Boolean) {
    with(binding) {
        val widgetFPVDims = if (!isMapExpanded) MATCH_PARENT to MATCH_PARENT else 400 to 212
        val mapCardDims = if (!isMapExpanded) 160 to 86 else MATCH_PARENT to MATCH_PARENT
        val displayDensityFactor: Float =
            requireContext().resources.displayMetrics.density // used to convert pixels to dp
        widgetFPV.reset()
        // scale down camera surface
        widgetFPV.setup(width = widgetFPVDims.first, height = widgetFPVDims.second)

        // scale down/up fpv widget
        widgetFPV.layoutParams =
            ConstraintLayout.LayoutParams(widgetFPVDims.first, widgetFPVDims.second)
        widgetFPV.translationZ = if (!isMapExpanded) 0f else 30f
        widgetFPV.updateLayoutParams<ConstraintLayout.LayoutParams> {
            startToStart = clRoot.id
            bottomToBottom = clRoot.id
            if (!isMapExpanded) {
                endToEnd = clRoot.id
                topToTop = clRoot.id
            }
            marginStart = if (!isMapExpanded) 0 else 12
            bottomMargin = if (!isMapExpanded) 0 else 12
        }

        // scale up/down map
        val mapLayoutParams =
            ConstraintLayout.LayoutParams(
                (mapCardDims.first * displayDensityFactor).toInt(),
                (mapCardDims.second * displayDensityFactor).toInt()
            )
        mapLayoutParams.setMargins(
            if (!isMapExpanded) 12 else 0, 0, 0, if (!isMapExpanded) 12 else 0
        )
        mapCard.layoutParams = mapLayoutParams
        mapCard.elevation  =  if (!isMapExpanded) 21f else 0f
        mapCard.cardElevation = if (!isMapExpanded) 21f else 0f
        map.translationZ =  if (!isMapExpanded) 21f else 0f
        mapCard.translationZ =  if (!isMapExpanded) 21f else 0f
        mapCard.radius = if (!isMapExpanded) 8f else 0f
        mapCard.updateLayoutParams<ConstraintLayout.LayoutParams> {
            startToStart = clRoot.id
            bottomToBottom = clRoot.id
            if (isMapExpanded) {
                endToEnd = clRoot.id
                topToTop = clRoot.id
            }
            marginStart = if (!isMapExpanded) 12 else 0
            bottomMargin = if (!isMapExpanded) 12 else 0
        }
        if (isMapExpanded) {
            mapCard.setPadding(0, 12, 0, 0)
        }

        // update telemetry widget constraints
        widgetTelemetry.updateLayoutParams<ConstraintLayout.LayoutParams> {
            startToEnd = if (isMapExpanded) {
                widgetFPV.id
            } else {
                mapCard.id
            }
        }

        llCamera.isVisible = !isMapExpanded
        llPhotoVideo.isVisible = !isMapExpanded
        widgetLightsControl.isVisible = !isMapExpanded
        imgBtnMapLayers.isVisible = isMapExpanded
    }
}

Solution

  • Thanks to @CommonsWare suggestions, using a TextureView instead fixed the issue right away. Here's what I had to change:

    In the original layout i was using a SurfaceView, so that got swapped for:

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/core_dark100">
    
        <TextureView
            android:id="@+id/surface"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center" />
    
    </FrameLayout>
    

    Here I wrapped it in a FrameLayout so that I could control "its" color while the video is still loading. Then in the custom ConstraintLayout implementation for my FPV widget, i replaced the SurfaceView's callbacks with the appropriate one for the TextureView:

    binding.surface.isOpaque = false // used to display the FrameLayout's bg color
    
    binding.surface.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
        override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
            isSurfaceReady = true
            surface = Surface(surfaceTexture)
    
            surface?.let { doStuff }
        }
    
        override fun onSurfaceTextureSizeChanged(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
            surface?.let { doStuff }
            isSurfaceReady = true
        }
    
        override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
            isSurfaceReady = false
            surface?.release()
            surface = null
            return true
        }
    
        override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) {
            // Called when the SurfaceTexture is updated via updateTexImage()
        }
    }
    

    That's all i had to do, now I could pass the surface directly to the DJI API.