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 | |
} | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This popup uses two hacks:
visible
state: IfAnimatedVisibility
enters the compositionwith
visible = true
by default, it won't animate. So I work around that by setting it to a state defaulting tofalse
, then instantly switch it totrue
during composition by means of aLaunchedEffect
.show = false
, thePopup
leavesthe 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 setshow = false
once the exit animation is finished by means of aDisposableEffect
, whoseonDispose
lambdagets 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 aBox
where 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
Popup
internally, 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
Popup
is 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
Popup
in favor of something else.