Skip to content

Instantly share code, notes, and snippets.

@Skaldebane
Last active March 5, 2023 11:24
Show Gist options
  • Save Skaldebane/944ba6487c9c16459db31037fc472d93 to your computer and use it in GitHub Desktop.
Save Skaldebane/944ba6487c9c16459db31037fc472d93 to your computer and use it in GitHub Desktop.
Popup with Show/Hide animations. Read comment for more details.
/**
* This popup uses two hacks:
* 1. Deferring the AnimatedVisibility's `visible` state: If `AnimatedVisibility` enters the composition
* with `visible = true` by default, it won't animate. So I work around that by setting it to a state defaulting to
* `false`, then instantly switch it to `true` during composition by means of a `LaunchedEffect`.
*
* 2. Deferring the Popup's hiding until the animation is complete: When we set `show = false`, the `Popup` leaves
* the composition immediately, so the exit animation isn't shown. To work around this, I set the aforementioned
* internal state of the AnimatedVisibility to `false` first (which will start the exit animation), then set
* `show = false` once the exit animation is finished by means of a `DisposableEffect`, whose `onDispose` lambda
* gets called at the end of the exit animation when all the content leaves the composition.
*
* All of this can be avoided by simply not using the Compose-provided `Popup`, and just using a `Box` where the
* popup can be at the top of everything. However, that causes issues if the popup covers a TextField, as the cursor
* handle of a TextField is using `Popup` internally, and hence covers EVERYTHING in the app, including any Composable
* that covers the TextField, and also covers the IME, if shown, which doesn't look all that great.
* In this case using the Compose `Popup` is the only way to be on top of that handle, hence why this is needed in the
* first place. If an app doesn't have this issue (e.g. no TextFields to be covered), I'd recommend shying away from
* using `Popup` in favor of something else.
* */
@Preview
@Composable
fun AnimationInPopupTest() {
AppTheme {
Surface(
color = MaterialTheme.colors.background,
modifier = Modifier.fillMaxSize()
) {
var show by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
) {
Button(
onClick = { show = !show },
modifier = Modifier.padding(12.dp)
) {
Text("Show/Hide Popup")
}
if (show) Popup {
var animate by remember { mutableStateOf(false) }
Box(Modifier.fillMaxSize()) {
AnimatedVisibility(
visible = show && animate,
enter = expandIn(expandFrom = Alignment.TopEnd),
exit = shrinkOut(shrinkTowards = Alignment.TopEnd),
modifier = Modifier
.align(Alignment.TopEnd)
.padding(12.dp)
.shadow(
elevation = 12.dp,
shape = MaterialTheme.shapes.large
)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(
color = MaterialTheme.colors.surface,
shape = MaterialTheme.shapes.large
)
.padding(12.dp)
) {
Text(text = "Hello, world!")
}
DisposableEffect(Unit) {
onDispose {
show = !show
}
}
BackHandler {
animate = false
}
}
}
LaunchedEffect(Unit) {
animate = true
}
}
}
}
}
}
@Skaldebane
Copy link
Author

This popup uses two hacks:

  1. Deferring the AnimatedVisibility's visible state: If AnimatedVisibility enters the composition
    with visible = true by default, it won't animate. So I work around that by setting it to a state defaulting to
    false, then instantly switch it to true during composition by means of a LaunchedEffect.
  2. Deferring the Popup's hiding until the animation is complete: When we set show = false, the Popup leaves
    the composition immediately, so the exit animation isn't shown. To work around this, I set the aforementioned
    internal state of the AnimatedVisibility to false first (which will start the exit animation), then set
    show = false once the exit animation is finished by means of a DisposableEffect, whose onDispose lambda
    gets called at the end of the exit animation when all the content leaves the composition.

All of this can be avoided by simply not using the Compose-provided Popup, and just using a Box where the
popup can be at the top of everything. However, that causes issues if the popup covers a TextField, as the cursor
handle of a TextField is using Popup internally, and hence covers EVERYTHING in the app, including any Composable
that covers the TextField, and also covers the IME, if shown, which doesn't look all that great.

In this case using the Compose Popup is the only way to be on top of that handle, hence why this is needed in the
first place. If an app doesn't have this issue (e.g. no TextFields to be covered), I'd recommend shying away from
using Popup in favor of something else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment