androidkotlinandroid-jetpack-compose

How to set an offset for adjustResize / imePadding?


He||o, using Kotlin and Compose, I created a Column in a Box, I use android:windowSoftInputMode="adjustResize" in the manifest file

The Column's Modifier uses .align(Alignment.BottomCenter) and .offset(y = (-110).dp), it looks like this:

no keyboard

The Column's Modifier also uses .imePadding(), it looks like this when the keyboard is shown:

keyboard shown

Is there some way to use some offset so when the keyboard appears, the Column goes lower?

So there is no space between the Login button and the keyboard


Solution

  • Modifier.imePadding() is just a padding that changes based on keyboard height. If there are 100.dp space between a button it still be a 100.dp when keyboard is opened.

    As mentioned in question you need to have a layout or offset Modifier that do not change position in parent, in OP's case there are no siblings but offset does not change position of siblings as padding does so it works unlike imePadding because when there is no space padding does not work as expected but it works as intended, because it's a padding modifier.

    enter image description here

    First you need to set

    WindowCompat.setDecorFitsSystemWindows(window, false)
    

    to be able to get keyboard size with

    val density = LocalDensity.current
    val imeBottom = WindowInsets.ime.getBottom(density)
    

    Create an offset Modifier that changes with keyboard height

    fun Modifier.imeOffset(contentBottom: Int = 0) = composed {
        val density = LocalDensity.current
        val imeBottom = WindowInsets.ime.getBottom(density)
        val navBarBottom = WindowInsets.navigationBars.getBottom(density)
        Modifier.offset {
            IntOffset(
                x =0,
                y = (-imeBottom + contentBottom + navBarBottom)
                    .coerceAtMost(0)
            )
        }
    }
    

    contentBottom is the bottom position of Composable that this Modifier will be assigned to which is calculated with

    .onPlaced {
        val parentHeight = it.parentLayoutCoordinates?.size?.height ?: 0
        bottom = (parentHeight - it.boundsInParent().bottom.toInt())
    } 
    

    Demo

    @Composable
    private fun ImeOffsetSample() {
        var bottom by remember {
            mutableIntStateOf(0)
        }
    
        Box(
            modifier = Modifier.fillMaxSize()
                .systemBarsPadding()
                .border(2.dp, Color.Red),
            contentAlignment = Alignment.Center
        ) {
            Column(
                modifier = Modifier
                    .offset(y = (150).dp)
                    .onPlaced {
                        if (bottom == 0) {
                            val parentHeight = it.parentLayoutCoordinates?.size?.height ?: 0
                            bottom = (parentHeight - it.boundsInParent().bottom.toInt())
                                .coerceAtLeast(0)
                        }
                    }
                    .imeOffset(bottom)
                    .border(2.dp, Color.Black)
                    .fillMaxWidth(),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
    
                var email by remember {
                    mutableStateOf("")
                }
    
                var password by remember {
                    mutableStateOf("")
                }
    
                TextField(
                    value = email,
                    onValueChange = { email = it },
                    label = { Text("email") }
                )
    
                TextField(
                    value = password,
                    onValueChange = { password = it },
                    label = { Text("password") }
                )
    
                Button(
                    onClick = {}
                ) {
                    Text("Login")
                }
            }
        }
    }
    

    Another sample with a Column that contains other Composbles above the child to be assigned with imeOffset. In this case padding would shrink last Compoasble(TextField and Button in Column) because the amount of space is Column height - imePadding which is not enough to fit last composable.

    enter image description here

    Button and TextField can be moved above keyboard even if whole Composable is filled with other sibling Composables.

    @Composable
    private fun ImeOffsetSample() {
        var bottom by remember {
            mutableIntStateOf(0)
        }
    
        Column(
            modifier = Modifier.fillMaxSize().systemBarsPadding()
        ) {
    
            TopAppBar(
                title = {
                    Text("TopAppbar")
                }
            )
            Column(
                modifier = Modifier.padding(horizontal = 16.dp)
            ) {
                Spacer(Modifier.height(16.dp))
    
                repeat(4) {
                    SomeComposable()
                    Spacer(Modifier.height(16.dp))
                }
    
                repeat(7) {
                    Text("Some Text")
                    Spacer(Modifier.height(16.dp))
                }
    
                Spacer(Modifier.weight(1f))
    
                Column(
                    modifier = Modifier
                        .onPlaced {
                            if (bottom == 0) {
                                val parentHeight = it.parentLayoutCoordinates?.size?.height ?: 0
                                bottom = (parentHeight - it.boundsInParent().bottom.toInt())
                                    .coerceAtLeast(0)
    
                                println("Parent height: $parentHeight, bottom: $bottom")
                            }
                        }
                        .imeOffset(bottom)
                        .fillMaxWidth()
                        .background(Color.White)
                ) {
                    SomeComposable()
                    Spacer(Modifier.height(16.dp))
    
                    Button(
                        modifier = Modifier
    
                            .fillMaxWidth()
                            .background(Color.White)
                            .padding(bottom = 16.dp),
                        onClick = {}
                    ) {
                        Text("Login")
                    }
                }
            }
        }
    }
    
    @Composable
    fun SomeComposable() {
        var text by remember {
            mutableStateOf("")
        }
    
        TextField(
            modifier = Modifier.fillMaxWidth(),
            value = text,
            onValueChange = { text = it },
            label = { Text("text") }
        )
    }