androidkotlinandroid-jetpack-composepasswordsuitextfield

How to show a floating password requirement box below TextField in Jetpack Compose?


I am building a Jetpack Compose login screen and need to display a floating password requirement box below the TextField. The box should dynamically appear when the user starts typing in the password field and should not affect or push other UI elements. Requirements: The box should appear directly below the password field when it is focused. It should not push other UI elements down when it appears. It should disappear when the field is not focused.

Issue I am facing: I tried using Box and Card, but they push other elements down when the box appears. I also tried Popup but it display over my keyboard when I am typing, but I am not sure if that’s the best approach in Jetpack Compose. I already created password validation part just want to know how to show this box below textfield which should not affect other component and it should visible over it.

I want to create something like this:-

enter image description here


Solution

  • Here is the sample implementation that uses coordinates available from onGloballyPositioned modifier and provide them to PopupPositionProvider

    @Composable
    fun PasswordFieldWithPopup() {
        var password by rememberSaveable { mutableStateOf("") }
        var isPasswordFocused by remember { mutableStateOf(false) }
    
        var textFieldPosition by remember { mutableStateOf(Offset.Zero) }
        var textFieldSize by remember { mutableStateOf(IntSize.Zero) }
    
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
    
                TextField(
                    value = password,
                    onValueChange = { password = it },
                    label = { Text("Password") },
                    modifier = Modifier
                        .onFocusChanged { focusState ->
                            isPasswordFocused = focusState.isFocused
                        }
                        .onGloballyPositioned { coordinates ->
                            textFieldPosition = coordinates.localToWindow(Offset.Zero)
                            textFieldSize = coordinates.size
                        }
                )
    
                if (isPasswordFocused && password.isNotEmpty()) {
                    val popupPositionProvider = remember(textFieldPosition, textFieldSize) {
                        object : PopupPositionProvider {
                            override fun calculatePosition(
                                anchorBounds: IntRect,
                                windowSize: IntSize,
                                layoutDirection: androidx.compose.ui.unit.LayoutDirection,
                                popupContentSize: IntSize
                            ): IntOffset {
                                val xPos = textFieldPosition.x.toInt()
                                val yPos = textFieldPosition.y.toInt() + textFieldSize.height
    
                                return IntOffset(xPos, yPos)
                            }
                        }
                    }
    
                    Popup(
                        popupPositionProvider = popupPositionProvider,
                        onDismissRequest = {  }
                    ) {
                        Card(
                            modifier = Modifier
                                .wrapContentSize()
                                .padding(8.dp)
                        ) {
                            Column(modifier = Modifier.padding(16.dp)) {
                                Text("Password requirements:")
                                Text("• At least 8 characters")
                                Text("• At least 1 capital letter")
                                Text("• At least 1 number")
                            }
                        }
                    }
                }
    
            // Just for testing
            Text(
                "Click here to see password requirements",
                modifier = Modifier
                    .padding(8.dp)
            )
        }
    }
    

    enter image description here