Thread
#compose
    Daniele Segato

    Daniele Segato

    1 year ago
    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?
    r

    Ralston Da Silva

    1 year ago
    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.
    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)
    Daniele Segato

    Daniele Segato

    1 year ago
    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:
    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:
    val relocationRequester = remember { RelocationRequester() }
    TextField(
       modifier = Modifier
                    // ...
                    .onFocusChanged { if (it.isFocused) relocationRequester.bringIntoView() }
                    // ...
    )
    Am I using it incorrectly?
    c

    Carl Benson

    1 year ago
    I'm having this exact problem, and this got me a bit further In the composable root I have
    val relocationRequester = remember { RelocationRequester() }
    val ime = LocalWindowInsets.current.ime
    and then on the textfields modifier I added
    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
    Daniele Segato

    Daniele Segato

    1 year ago
    @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

    1 year ago
    😄 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
    Daniele Segato

    Daniele Segato

    1 year ago
    Ok... kinda an ugly workaround but building on your idea @Carl Benson this is working for me:
    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

    1 year ago
    since you are using a launch effect, it only works once or? What if you close the keyboard and refocus the text field
    Daniele Segato

    Daniele Segato

    1 year ago
    It is working consistently now. After just reinstalling the app. LaunchEffect is using
    focused
    as key so it relaunch every time that change
    @Carl Benson let me know if my snipped works for you please
    c

    Carl Benson

    1 year ago
    @Daniele Segato thanks, I'll revisit this code after the summer vacations
    r

    Ralston Da Silva

    1 year ago
    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.
    Daniele Segato

    Daniele Segato

    1 year ago
    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

    1 year ago
    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?
    Daniele Segato

    Daniele Segato

    1 year ago
    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

    1 year ago
    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()
    val relocationRequester = remember { RelocationRequester() }
    Box(
        Modifier
            .border(2.dp, blue)
            .relocationRequester(relocationRequester)
    )
    But this would include the border:
    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()
    Daniele Segato

    Daniele Segato

    1 year ago
    Thank you very much! Great API.
    Cicero

    Cicero

    1 year ago
    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

    1 year ago
    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
    Cicero

    Cicero

    1 year ago
    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:
    .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
    var isFocused by remember { mutableStateOf(false) }
    var requestBringIntoView: Job? = null
    .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

    1 year ago
    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

    Cicero

    Cicero

    1 year ago
    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
    rsktash

    rsktash

    1 year ago
    @Ralston Da Silva this sample is not working with the latest compose. Is something going to be changed?
    batuhan ardor

    batuhan ardor

    10 months ago
    hey, any updates?
    Dinesh Gangatharan

    Dinesh Gangatharan

    2 months ago
    Any updates guys?