Last active
March 5, 2023 11:24
-
-
Save skaldebane/944ba6487c9c16459db31037fc472d93 to your computer and use it in GitHub Desktop.
Popup with Show/Hide animations. Read comment for more details.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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 | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This popup uses two hacks:
visiblestate: IfAnimatedVisibilityenters the compositionwith
visible = trueby default, it won't animate. So I work around that by setting it to a state defaulting tofalse, then instantly switch it totrueduring composition by means of aLaunchedEffect.show = false, thePopupleavesthe composition immediately, so the exit animation isn't shown. To work around this, I set the aforementioned
internal state of the AnimatedVisibility to
falsefirst (which will start the exit animation), then setshow = falseonce the exit animation is finished by means of aDisposableEffect, whoseonDisposelambdagets 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 aBoxwhere thepopup 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
Popupinternally, and hence covers EVERYTHING in the app, including any Composablethat covers the TextField, and also covers the IME, if shown, which doesn't look all that great.
In this case using the Compose
Popupis the only way to be on top of that handle, hence why this is needed in thefirst place. If an app doesn't have this issue (e.g. no TextFields to be covered), I'd recommend shying away from
using
Popupin favor of something else.