Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jetpack Compose: Make full-screen (absolutely positioned) component

How can I go about making a composable deep down within the render tree full screen, similar to how the Dialog composable works?

Say, for example, when a use clicks an image it shows a full-screen preview of the image without changing the current route.

I could do this in CSS with position: absolute or position: fixed but how would I go about doing this in Jetpack Compose? Is it even possible?

One solution would be to have a composable at the top of the tree that can be passed another composable as an argument from somewhere else in the tree, but this sounds kind of messy. Surely there is a better way.

like image 732
foxtrotuniform6969 Avatar asked Jul 20 '21 19:07

foxtrotuniform6969


2 Answers

From what I can tell you want to be able to draw from a nested hierarchy without being limited by the parent constraints.

We faced similar issues and looked at the implementation how Composables such as Popup, DropDown and Dialog function.

What they do is add an entirely new ComposeView to the Window.
Because of this they are basically starting from a blank canvas.
By making it transparent it looks like the Dialog/Popup/DropDown appears on top.

Unfortunately we could not find a Composable that provides us the functionality to just add a new ComposeView to the Window so we copied the relevant parts and made following.

@Composable
fun FullScreen(content: @Composable () -> Unit) {
    val view = LocalView.current
    val parentComposition = rememberCompositionContext()
    val currentContent by rememberUpdatedState(content)
    val id = rememberSaveable { UUID.randomUUID() }

    val fullScreenLayout = remember {
        FullScreenLayout(
            view,
            id
        ).apply {
            setContent(parentComposition) {
                currentContent()
            }
        }
    }

    DisposableEffect(fullScreenLayout) {
        fullScreenLayout.show()
        onDispose { fullScreenLayout.dismiss() }
    }
}

@SuppressLint("ViewConstructor")
private class FullScreenLayout(
    private val composeView: View,
    uniqueId: UUID
) : AbstractComposeView(composeView.context) {

    private val windowManager =
        composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager

    private val params = createLayoutParams()

    override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
        private set

    init {
        id = android.R.id.content
        ViewTreeLifecycleOwner.set(this, ViewTreeLifecycleOwner.get(composeView))
        ViewTreeViewModelStoreOwner.set(this, ViewTreeViewModelStoreOwner.get(composeView))
        ViewTreeSavedStateRegistryOwner.set(this, ViewTreeSavedStateRegistryOwner.get(composeView))

        setTag(R.id.compose_view_saveable_id_tag, "CustomLayout:$uniqueId")
    }

    private var content: @Composable () -> Unit by mutableStateOf({})

    @Composable
    override fun Content() {
        content()
    }

    fun setContent(parent: CompositionContext, content: @Composable () -> Unit) {
        setParentCompositionContext(parent)
        this.content = content
        shouldCreateCompositionOnAttachedToWindow = true
    }

    private fun createLayoutParams(): WindowManager.LayoutParams =
        WindowManager.LayoutParams().apply {
            type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
            token = composeView.applicationWindowToken
            width = WindowManager.LayoutParams.MATCH_PARENT
            height = WindowManager.LayoutParams.MATCH_PARENT
            format = PixelFormat.TRANSLUCENT
            flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
                WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
        }

    fun show() {
        windowManager.addView(this, params)
    }

    fun dismiss() {
        disposeComposition()
        ViewTreeLifecycleOwner.set(this, null)
        windowManager.removeViewImmediate(this)
    }
}

Here is an example how you can use it

@Composable
internal fun Screen() {
    Column(
        Modifier
            .fillMaxSize()
            .background(Color.Red)
    ) {
        Text("Hello World")

        Box(Modifier.size(100.dp).background(Color.Yellow)) {
            DeeplyNestedComposable()
        }
    }
}

@Composable
fun DeeplyNestedComposable() {
    var showFullScreenSomething by remember { mutableStateOf(false) }
    TextButton(onClick = { showFullScreenSomething = true }) {
        Text("Show full screen content")
    }

    if (showFullScreenSomething) {
        FullScreen {
            Box(
                Modifier
                    .fillMaxSize()
                    .background(Color.Green)
            ) {
                Text("Full screen text", Modifier.align(Alignment.Center))
                TextButton(onClick = { showFullScreenSomething = false }) {
                    Text("Close")
                }
            }
        }
    }
}

The yellow box has set some constraints, which would prevent the Composables from inside to draw outside its bounds.

enter image description here

like image 161
timr Avatar answered Nov 13 '22 08:11

timr


Using the Dialog composable, I have been able to get a proper fullscreen Composable in any nested one. It's quicker and easier than some of other answers.

Dialog(
    onDismissRequest = { /* Do something when back button pressed */ },
    properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = false, usePlatformDefaultWidth = false)
){
    /* Your full screen content */
}
like image 35
Melio500 Avatar answered Nov 13 '22 06:11

Melio500