kotlin - How do you detect if the user has clicked outside of a composable using Jetpack Compose? - Stack Overflow

admin2025-04-18  4

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 }) { }

                }
            }
 

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 }) { }

                }
            }
 
Share Improve this question edited Jan 29 at 16:10 Bob Rasner asked Jan 29 at 15:41 Bob RasnerBob Rasner 2951 silver badge8 bronze badges 1
  • Just out of curiosity have you checked if my solution works for you? – F.Mysir Commented Feb 4 at 12:35
Add a comment  | 

2 Answers 2

Reset to default 1

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...")
                    }
                }
            }
        }
    }
}

I think you are over complicating the code. You can use in your main composable something like:

var isPopUpOpen by remember {
    mutableStateOf(false)
}

CustomPopUpWithAnimation(
    enter = fadeIn() + slideInVertically(),
    exit = fadeOut() + slideOutVertically(),
    isPopUpOpen = isPopUpOpen,
    closePopUp = { isPopUpOpen = false }
) {
    //create your dialogue as you wish
    Box(
        modifier = Modifier
            .align(Alignment.Center)
            //other modifiers
            //only clickable this is important as your last modifier
            .clickable(
                indication = null,
                interactionSource = remember { MutableInteractionSource() },
                onClick = {})
    ) {
        
    }
}

And your pop up composable can be used anywhere like:

@Composable
fun CustomPopUpWithAnimation(
    enter: EnterTransition,
    exit: ExitTransition,
    isPopUpOpen: Boolean,
    closePopUp: () -> Unit,
    content: @Composable BoxScope.() -> Unit
) {
    AnimatedVisibility(
        visible = isPopUpOpen,
        enter = enter,
        exit = exit,
    ) {
        Box(
            modifier = Modifier
                .fillMaxSize()
                .clickable(
                    indication = null,
                    interactionSource = remember { MutableInteractionSource() },
                    onClick = closePopUp
                )
        ) {
            content()
        }
    }
}
转载请注明原文地址:http://anycun.com/QandA/1744955472a89989.html