https://kotlinlang.org logo
d

Daniele Segato

06/30/2021, 4:27 PM
question: Is it possible to avoid the soft keyboard (IME) covering an input
TextField
when opening? I've a big form with TextFields. When i click on one close to the bottom of the screen the IME open up and cover it is there a way to make the view scroll up just enough so that the TextField is visible? Is this supposed to happen or am I doing something wrong?
6
r

Ralston Da Silva

06/30/2021, 8:34 PM
Sorry that you are facing this issue. This is a known issue that is being tracked by https://issuetracker.google.com/178211874 We don't have an easy fix right now, but here is a workaround: We have some experimental API that can be used to bring something into view by asking the parents to scroll. This will ultimately be added to TextField so that it automatically scrolls. For now, you can use this modifier to bring the text field into view.
Copy code
val relocationRequester = remember { RelocationRequester() }
TextField(
    value = "...",
    onValueChange = {},
    modifier = Modifier.relocationRequester(relocationRequester)
)
You can then use
relocationRequester.bringIntoView()
to programmatically scroll the item into view. This workaround is not perfect because , I don't have recommendation on where you could call
bringIntoView()
right now. Here are some suggestions you could try: You could use it by adding a
Modifier.onFocusChanged{ ...}
, but if you do so, it will not work if you dismiss the keyboard and then click on it again, as the focus state isn't changed. You could use the window inset APIs to figure out when the keyboard is shown, and then call bringIntoView for the TextField that is currently focused: https://developer.android.com/reference/androidx/core/view/WindowInsetsAnimationCompat.Callback#onEnd(androidx.core.view[…]dowInsetsAnimationCompat)
d

Daniele Segato

06/30/2021, 11:07 PM
I did try to use
bringIntoView
onFocusChanged
(with an "if focused") today but it didn't consistently work. I'll try the modifier. Does the field need to be a direct children of the scrollable view for this to work? Does it interfere with anything? I've applied the accompanist insets library in my app. Without using nested navigation for the ime.
@Ralston Da Silva thanks for your answer, I'm trying your suggestion:
Copy code
TextField(
   modifier = Modifier
                // ...
                .relocationRequester(remember { RelocationRequester() })
                // ...
)
But when I click on a field the keyboard still open covering it. The same thing happens if I try like this:
Copy code
val relocationRequester = remember { RelocationRequester() }
TextField(
   modifier = Modifier
                // ...
                .onFocusChanged { if (it.isFocused) relocationRequester.bringIntoView() }
                // ...
)
Am I using it incorrectly?
c

Carl Benson

07/01/2021, 7:56 AM
I'm having this exact problem, and this got me a bit further In the composable root I have
Copy code
val relocationRequester = remember { RelocationRequester() }
val ime = LocalWindowInsets.current.ime
and then on the textfields modifier I added
Copy code
modifier = Modifier.weight(0.5f)
                    .relocationRequester(relocationRequester)
                    .onFocusEvent {
                        if (it.isFocused && !ime.animationInProgress && ime.isVisible) {
                            relocationRequester.bringIntoView()
                        }
                    }
the problem is just that it scrolls too far, like below the Scaffold TopAppBar
also on the first click, it will show and dismiss the keyboard, but on second tap it shows the keyboard again
d

Daniele Segato

07/01/2021, 8:00 AM
@Carl Benson thanks but I prefer to have the keyboard covering the TextField than having it jump up and down and add weird padding 😄 I wonder if it's the
imePadding()
modifier +
android:windowSoftInputMode="adjustResize"
causing this not to work
c

Carl Benson

07/01/2021, 8:02 AM
😄 yeah I'm not comfortable with using this API either, but it seems to be experimental and that it will be fixed to work out of the box
d

Daniele Segato

07/01/2021, 10:05 AM
Ok... kinda an ugly workaround but building on your idea @Carl Benson this is working for me:
Copy code
val relocationRequester = remember { RelocationRequester() }
    var focused by remember { mutableStateOf(false) }
    val ime = LocalWindowInsets.current.ime
    LaunchedEffect(focused) {
        if (focused) {
            var done = false
            while (!done) {
                if (ime.isVisible && !ime.animationInProgress) {
                    relocationRequester.bringIntoView()
                    done = true
                }
                delay(100L)
            }
        }
    }
    TextField(
        modifier = Modifier
                // ...
                .onFocusChanged { focused = it }
                // ...
    )
I'd like some feedback from @Ralston Da Silva on whatever I'm doing something really really bad 🙂
ah, nope it doesn't work 100% of the time either, but when it works it works well
c

Carl Benson

07/01/2021, 2:40 PM
since you are using a launch effect, it only works once or? What if you close the keyboard and refocus the text field
d

Daniele Segato

07/01/2021, 2:42 PM
It is working consistently now. After just reinstalling the app. LaunchEffect is using
focused
as key so it relaunch every time that change
🙏 1
@Carl Benson let me know if my snipped works for you please
c

Carl Benson

07/02/2021, 11:24 AM
@Daniele Segato thanks, I'll revisit this code after the summer vacations
r

Ralston Da Silva

07/02/2021, 11:30 PM
Sorry I missed the last few messages. RelocationRequester.bringIntoView() is a pretty new API. I'll try to answer some of the questions: 1. Does the field need to be a direct chlid of the scrollable component? No, the relocation requester sends a request up the hierarchy to all scrollable parents, and asks them to scroll. Finally when it reaches the root compose view, it calls Android's view.requestRectangleOnScreen (If the compose view is part of a larger Classic Android app that needs scrolling from the View system). 2. Does bringIntoView work for all scrollable parents? In the current release bringIntoView works on all parents that use a Modifier.horizontalScroll or Modifier.verticalScroll. BringIntoView has some issues with multiple scrollable parents. Thes issues are fixed if you use the latest snapshot build. You can get the latest snapshot build from androidx.dev 3. Does bringIntoView work for LazyRow/LazyColumn No. Right now it doesn't work for LazyLists. We need to add additional API to LazyList to support this. In the latest version, we have removed relocation support from Modifier.horizontalScroll and Modifier.vertical scroll as we didn't want to use experimental APIs across our internal module boundaries. (Relocation is part of the ui package and Scrollable is part of the foundation package) I suggest you use the latest snapshot build from androidx.dev and instead of using Modifier.verticalScroll copy and paste Modifier.verticalScrollWithRelocation from this example: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]in/java/androidx/compose/ui/demos/scroll/BringIntoViewDemo.kt When the Relocation API graduates out of the experimental stage and we will add it to TextField, Modifier.focusable(), Modifier.scrollable() etc and you won't have to call bringIntoView() directly. This is just a workaround for now.
d

Daniele Segato

07/04/2021, 9:35 AM
I see, thank you @Ralston Da Silva does it mean that with the RC release my current workaround will stop working unless I grab the
Modifier.verticalScrollWithRelocation
and replace it to the
verticalScroll
modifier? Also, in your linked example I noticed you placed the
Modifier.relocationRequester()
in the box rather than the
Button
. I placed it directly in the TextField, should i place it in some outer layer? thank you very much for your insights!
r

Ralston Da Silva

07/06/2021, 5:01 PM
Your current workaround will work with the RC release (and 1.0). But going forward you would have to use
Modifier.verticalScrollWithRelocaiton()
until the Relocation API graduates out of Experimental and we can use it in
Modifier.VerticalScroll()
. The relocationRequester uses the coordinates of the item that the Modifier is attached to, and brings those coordinates on screen.In the example I used the
Modifier.relocationRequester()
on the green and red boxes which I want to bring into view (Lines 70 and 81). Then in the Button's onClick callback, I call
relocationRequester.bringIntoView()
so that I can use the button click to trigger the relocation. Additional Question: We are considering adding an extra parameter to allow you to bring a part of an item into view. Something like
fun RelocationRequester.bringIntoView(rect: Rect)
If we do this, you would have the ability to specify which part of the item you want to bring into view. Do you think this would be helpful?
d

Daniele Segato

07/06/2021, 5:01 PM
Thank you! I believe it might be useful in some case. Probably not for standard widget, but it could be extremely helpful for complex custom widgets. Specially one that draw on screen directly with a Canvas. would that API allow me to also specify birder border than the current widget? like a "padding" around the widget? I'm thinking of a TextField with some decorator around it showing errors or similar. The
relocationRequester()
could be just placed at an external box of course, just asking.
r

Ralston Da Silva

07/07/2021, 6:32 PM
Yes you could do that. If you want to include the padding, then you would add the Modifier.relocationRequester() before the border: For example, this would only use the size of the Box's content when you call relocationRequester.bringIntoView()
Copy code
val relocationRequester = remember { RelocationRequester() }
Box(
    Modifier
        .border(2.dp, blue)
        .relocationRequester(relocationRequester)
)
But this would include the border:
Copy code
val relocationRequester = remember { RelocationRequester() }
Box(
    Modifier
        .relocationRequester(relocationRequester)
        .border(2.dp, blue)
)
And if you want to bring part of the Box into view, you would specify a rect (in the Box's coordinates):
relocationRequester.bringIntoView(Rect(0f, 0f, 10f, 10f)
or use some helper function overloads:
relocationRequester.bringBottomEdgeIntoView()
d

Daniele Segato

07/08/2021, 9:35 AM
Thank you very much! Great API.
c

Cicero

07/09/2021, 8:31 AM
Do we have any snippet for a final version of this? I have something really unstable atm. I will spend some time improving it but it would be nice to have something “ready”, at least working mildly stable. What was funny for me was that I couldn’t use: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]in/java/androidx/compose/ui/demos/scroll/BringIntoViewDemo.kt
r

Ralston Da Silva

07/09/2021, 9:17 AM
We are still working on finalizing this API, that Demo demonstrates API tha twas added recently, after our latest release. If you want to try this out, you could use the latest build from androidx.dev
💪🏽 1
c

Cicero

07/09/2021, 12:01 PM
Ok, so literally androidx.dev im your browser
If we could listen to this ime changes as state it would be beautiful too
I imagined something like:
Copy code
.onGloballyPositioned {
                if (isFocused) {
                    GlobalScope.async(<http://Dispatchers.IO|Dispatchers.IO>) {
                        button1Job?.cancel()
                        button1Job = async(<http://Dispatchers.IO|Dispatchers.IO>) {
                            var counter = 0
                            while (counter < 100000 && this.isActive) {
                                counter++
                                if (counter % 10000 == 1)
                            }
                            if (counter >= 100000 && this.isActive) {
                                button1Job?.cancel()
                                relocationRequester.bringIntoView()
                            }

                        }
                    }
                }
            }
It’s a little rough of an implementation but I realized that onGloballyPositioned changed when you opened the keyboard and stopped when it stopped. This is not the best solution but I wish I could apply something that could rely more on the end of something rather than an arbitrary delay
I’m going to implement something more like delta time here and see how it performs, counting around of 1000000 really makes it reliable
Copy code
var isFocused by remember { mutableStateOf(false) }
var requestBringIntoView: Job? = null
Copy code
.relocationRequester(relocationRequester)
.onFocusChanged {
    isFocused = it.isFocused
}
.onGloballyPositioned {
    if (isFocused) {
        GlobalScope.async() {
            requestBringIntoView?.cancel()
            requestBringIntoView = async() {
                var counter = 0
                while (counter < 15 && this.isActive) {
                    counter++
                    delay(10)
                }
                if (this.isActive) {
                    requestBringIntoView?.cancel()
                    relocationRequester.bringIntoView()
                }

            }
        }
    }
}
This did the trick reliably, I used delay instead of some shady way of trying to implement some sort of deltaTime which delivered awesomely. It is still arbitrary but at least it’s arbitrary towards the end of the changes on the global position which is a signal that the keyboard stopped animating. Obviously I’m doing this because of a special situation. Not only I can’t watch Ime as state, which brings me to some sort of a loop checker anyway, but the Ime values are always true or false. Don’t quite understand why but I wasn’t able to use them to tell or react to the keyboard changes (for example I can’t watch any changes in isAnimating). Another characteristic is that this solution is wrapped around the textfield global position. Ps: I bet there is space to polish the way I’m using coroutines, I know, I would be glad to receive recomendations.
r

Ralston Da Silva

07/13/2021, 12:41 AM
It looks like you want to know when the IME is visible. We don't have a compose API for this, but you could use the Android Inset APIs. Check out this video.

https://youtu.be/acC7SR1EXsI?t=319

c

Cicero

07/13/2021, 11:35 AM
To be honest, I believed this whole discussion was about it 😂 For example this delay thst Daniele is setting, he only needs it because there is nothing to be brought to view when the user touches the keyboard but just after a couple milliseconds when the keyboard is up. That is why, to be more precise, I wait until there are no more changes on global position to assume the keyboard had stopped moving. I will take a look into this IME API, thank you very much
r

rsktash

08/25/2021, 3:48 PM
@Ralston Da Silva this sample is not working with the latest compose. Is something going to be changed?
b

batuhan ardor

11/01/2021, 8:21 PM
hey, any updates?
d

Dinesh Gangatharan

07/25/2022, 9:09 PM
Any updates guys?
a

Andranik Azizbekyan

10/31/2022, 12:55 PM
My turn to ask: any update guys?
a

Anuta Vlad Sv

11/23/2022, 9:38 AM
Hi! Are there any updates on this?
d

Daniele Segato

11/23/2022, 12:47 PM
24 Views