Using Jetpack Compose, I'm trying to create a custom popup without using the built in popup composable because the latter doesn't allow for animated entry. I've got it working mostly. The popup appears above everything on the screen in the location of my choosing. The only problem is that I can't make the popup disappear when clicking outside of it's bounds.
I tried to develop this feature using the onGloballyPositioned
modifier on the custom popup and capturing it's size and position into variables. I then put a pointer input modifier around the entire screen, creating a conditional within it that detects if the position of the pointer event was within the position of the popup. It didn't work.
Here is the code for my failed implementation.
// Variables Initialized Outside Of An Activity, ViewModel, or Composable
var isPopUpOpen by mutableStateOf(false)
var size by mutableStateOf(Offset.Zero)
var position by mutableStateOf(Offset.Zero)
// Popup Composable
@Composable
fun CustomPopUp() {
AnimatedVisibility(
visible = isPopUpOpen,
enter = fadeIn(),
exit = fadeOut(),
) {
Box(
modifier = Modifier
.offset(x = 90.dp, y = 150.dp)
.onGloballyPositioned {
size = Offset(it.size.width.toFloat(), it.size.height.toFloat())
position = Offset(it.positionInRoot().x, it.positionInRoot().y)
}
.background(Color.Blue)
.size(200.dp)
)
}
}
// Main UI; Located In Main Activity
CustomPopUp()
Box(
modifier = Modifier
// Click Outside Logic
.pointerInput(isPopUpOpen) {
while (isPopUpOpen) {
awaitPointerEventScope {
if (awaitFirstDown().position != position) {
isPopUpOpen = false
}
}
}
}
.background(if (isPopUpOpen) Color.Black.copy(alpha = 0.2f) else Color.Transparent)
.fillMaxSize(),
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// Button That Displays Popup
Button(onClick = { isPopUpOpen = !isPopUpOpen }) { }
}
}
The mistake is you are comparing first touch position with top left position of your custom PopUp.
You should get Rect
from onGloballyPositioned and check if touch is not inside Rect of your Composable while popUp is open.
var rect by mutableStateOf(Rect.Zero)
@Preview
@Composable
fun PopUpTouchTest() {
CustomPopUp()
Box(
modifier = Modifier
// Click Outside Logic
.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown()
val position = down.position
if (rect.contains(position).not() && isPopUpOpen) {
isPopUpOpen = false
}
waitForUpOrCancellation()
}
}
.background(if (isPopUpOpen) Color.Black.copy(alpha = 0.2f) else Color.Transparent)
.fillMaxSize(),
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// Button That Displays Popup
Button(onClick = { isPopUpOpen = !isPopUpOpen }) {
Text("Click")
}
}
}
}
@Composable
fun CustomPopUp() {
AnimatedVisibility(
visible = isPopUpOpen,
enter = fadeIn(),
exit = fadeOut(),
) {
Box(
modifier = Modifier
.offset(x = 90.dp, y = 150.dp)
.onGloballyPositioned {
rect = it.boundsInRoot()
}
.background(Color.Blue)
.size(200.dp)
)
}
}
Also, default PopUp
allows fadeIn/FadeOut or other animations as well.
Here is a sample with AnimatedVisibility
to open/close a popUp with fadeIn/fadeOut or any other available animations for AnimatedVisibility.
@Composable
fun AnimatedVisibilityTransitionSample() {
val visibleState: MutableTransitionState<Boolean> = remember { MutableTransitionState(false) }
val transition: Transition<Boolean> = rememberTransition(visibleState)
Column(
modifier = Modifier.fillMaxSize()
) {
Button(
onClick = {
visibleState.targetState = visibleState.targetState.not()
}
) {
Text("Update Target State")
}
Text(
"State currentState: ${visibleState.currentState}\n" +
"targetState: ${visibleState.targetState}\n" +
"isIdle: ${visibleState.isIdle}",
fontSize = 16.sp
)
if (transition.targetState || transition.currentState) {
Popup(
properties = PopupProperties(focusable = true),
offset = IntOffset(200, 400),
onDismissRequest = {
visibleState.targetState = false
}
) {
transition.AnimatedVisibility(
visible = { targetSelected -> targetSelected },
enter = fadeIn(
animationSpec = tween(600)
),
exit = fadeOut(
animationSpec = tween(600)
)
) {
Box(modifier = Modifier.background(Color.Red).padding(16.dp)) {
Text("Popup Content...")
}
}
}
}
}
}