Status Update
Comments
kl...@google.com <kl...@google.com>
kl...@google.com <kl...@google.com> #2
Project: platform/frameworks/support
Branch: androidx-main
Author: Louis Pullen-Freilich <
Link:
Adds OverscrollEffect#withoutDrawing and OverscrollEffect#withoutEventHandling
Expand for full commit details
Adds OverscrollEffect#withoutDrawing and OverscrollEffect#withoutEventHandling
These APIs allow overscroll to have events dispatched to it by one component, and rendered in a separate component.
Fixes: b/266550551
Fixes: b/204650733
Fixes: b/255554340
Fixes: b/229537244
Test: OverscrollTest
Relnote: "Adds OverscrollEffect#withoutDrawing and OverscrollEffect#withoutEventHandling APIs - these APIs create a wrapped instance of the provided overscroll effect that doesn't draw / handle events respectively, which allows for rendering overscroll in a separate component from the component that is dispatching events. For example, disabling drawing the overscroll inside a lazy list, and then drawing the overscroll separately on top / elsewhere."
Change-Id: Idbb3d91546b49c1987a041f959bce4b2b09a9f61
Files:
- M
compose/foundation/foundation/api/current.txt
- M
compose/foundation/foundation/api/restricted_current.txt
- M
compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/OverscrollDemo.kt
- M
compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
- M
compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
- M
compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Overscroll.kt
Hash: f64e25b7a473c757d080521e7dd97b3f6670f60d
Date: Fri Nov 01 18:43:56 2024
el...@gmail.com <el...@gmail.com> #3
The following release(s) address this bug.It is possible this bug has only been partially addressed:
androidx.compose.foundation:foundation:1.8.0-alpha06
androidx.compose.foundation:foundation-android:1.8.0-alpha06
androidx.compose.foundation:foundation-jvmstubs:1.8.0-alpha06
androidx.compose.foundation:foundation-linuxx64stubs:1.8.0-alpha06
el...@gmail.com <el...@gmail.com> #4
It's kind of a crazy convoluted workaround, but this seems to work very well for me:
@Composable
fun AppScreen() {
val coroutineScope = rememberCoroutineScope()
var value by remember { mutableStateOf(TextFieldValue()) }
val scrollState = rememberScrollState()
var height by remember { mutableStateOf(0) }
var layoutResult: TextLayoutResult? by remember { mutableStateOf(null) }
BasicTextField(
value = value,
onValueChange = { value = it },
decorationBox = {
Box(modifier = Modifier.verticalScroll(scrollState)) {
it()
}
},
onTextLayout = { layoutResult = it },
textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colors.onBackground),
cursorBrush = SolidColor(MaterialTheme.colors.secondary),
modifier = Modifier
.fillMaxSize()
.onSizeChanged {
coroutineScope.launch {
val cursorInView = value.isCursorInView(
layoutResult = layoutResult!!,
height = it.height.toFloat(),
scrollValue = scrollState.value.toFloat()
)
if (!cursorInView && height > it.height) {
scrollState.scrollBy(
value.calculateRequiredSizeScroll(
layoutResult = layoutResult!!,
oldHeight = height.toFloat(),
newHeight = it.height.toFloat(),
scrollValue = scrollState.value.toFloat()
)
)
}
height = it.height
}
}
)
LaunchedEffect(value.selection) {
val cursorInView = value.isCursorInView(
layoutResult = layoutResult!!,
height = height.toFloat(),
scrollValue = scrollState.value.toFloat()
)
if (!cursorInView) {
scrollState.scrollBy(
value.calculateRequiredSelectionScroll(
layoutResult = layoutResult!!,
height = height.toFloat(),
scrollValue = scrollState.value.toFloat()
)
)
}
}
}
fun TextFieldValue.isCursorInView(
layoutResult: TextLayoutResult,
height: Float,
scrollValue: Float
) = with(layoutResult) {
val currentLine = try {
getLineForOffset(selection.min)
} catch (ex: IllegalArgumentException) {
System.err.println("Corrected Wrong Offset!")
getLineForOffset(selection.min - 1)
}
val lineBottom = getLineBottom(currentLine)
val lineTop = getLineTop(currentLine)
lineBottom <= height + scrollValue && lineTop >= scrollValue
}
fun TextFieldValue.calculateRequiredSelectionScroll(
layoutResult: TextLayoutResult,
height: Float,
scrollValue: Float
) = with(layoutResult) {
val currentLine = try {
getLineForOffset(selection.min)
} catch (ex: IllegalArgumentException) {
System.err.println("Corrected Wrong Offset!")
getLineForOffset(selection.min - 1)
}
val lineTop = getLineTop(currentLine)
val lineBottom = getLineBottom(currentLine)
if (lineTop < scrollValue) -(scrollValue - lineTop)
else if (lineBottom > height + scrollValue) lineBottom - (height + scrollValue)
else 0f
}
fun TextFieldValue.calculateRequiredSizeScroll(
layoutResult: TextLayoutResult,
oldHeight: Float,
newHeight: Float,
scrollValue: Float
) = with(layoutResult) {
val currentLine = try {
getLineForOffset(selection.min)
} catch (ex: IllegalArgumentException) {
System.err.println("Corrected Wrong Offset!")
getLineForOffset(selection.min - 1)
}
val sizeDifference = oldHeight - newHeight
val lineBottom = getLineBottom(currentLine)
if (lineBottom in (newHeight + scrollValue)..(oldHeight + scrollValue))
sizeDifference - (oldHeight - (lineBottom - scrollValue))
else 0f
}
I basically wrapped the inner text field Composable in a Box with the verticalScroll modifier, and then using these parameters: TextLayoutResult (to get the current line position), scroll value, and height; all to determine if the cursor is currently visible on the screen, and if it's not, calculate the minimal amount of scroll needed to get it back on the screen, both when the TextField size changes (which is the goal), and also when navigating around the text or editing it (this works fine in Compose by default, but using a scroll modifier breaks it, so I have to manually scroll to follow the cursor).
It works perfectly well for me, with a small annoyance being that sometimes (when typing really fast) the selection I get from the TextFieldValue is higher than what the TextLayoutResult expects, causing a crash. I worked around this by wrapping it in a try/catch, and decrementing the selection value by 1 to get it working. Pretty hacky and lazy, but it works flawlessly now, and those catched exceptions don't seem to affect performance in any noticeable way.
Yet having something like this implemented properly would still be far better than these hacky and convoluted workarounds.
kl...@google.com <kl...@google.com>
ap...@google.com <ap...@google.com> #5
Branch: androidx-main
commit d31135ea17e9ba4a5e117ebbac5e45b924a1f0a3
Author: Zach Klippenstein <klippenstein@google.com>
Date: Wed Jul 06 11:05:41 2022
Add demo for full-screen text field and factor out lorem ipsum generator.
Bug:
Test: n/a, demo change only
Change-Id: I6fbe6fdd71d7debd38384228a66bd5ef77eedf25
M compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldWIthScroller.kt
M compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt
A compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LoremIpsum.kt
M compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/BrushDemo.kt
M compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
M compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/DrawTextDemo.kt
A compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/FullScreenTextFieldDemo.kt
ap...@google.com <ap...@google.com> #6
Branch: androidx-main
commit 7e77bb541331ce0d62cabad732e43eca41ac978a
Author: Zach Klippenstein <klippenstein@google.com>
Date: Wed Jul 06 11:42:21 2022
Move BringIntoView queueing into the scrollable implementation.
This change removes the over-complicated coroutine-based queuing of
bringIntoView requests in the BringIntoViewResponder, which now is only
responsible for dispatching the request to the actual responder
interface and propagating it up the modifier local chain.
This gives the actual responder implementation (i.e. in
Modifier.scrollable) all the information about conflicting
requests and manage the queue of ongoing requests with an actual,
explicit queue that is much more straightforward than the previous
coroutine job chaining mess. Each request is stored in an actual list
along with the continuation from the request, and requests are
explicitly resumed or cancelled when needed. This also makes debugging
easier, because there's an actual queue data structure we can inspect,
which contains all the actual information about each request.
It also completely changes how the scroll animation is performed.
On every frame, the scroll target is recomputed to account for
changes in the viewport size, the request size, or the set of active
requests since the last frame, while still keeping the animation
smooth. The animation logic is in its own class, which allows a single
suspend animation call to animate to a target value that can be updated
while the animation is running.
While this change is a valuable cleanup on its own, it also fixes
focused child tracking with an explicit API for keeping a thing in view.
Fixes:
Bug:
Bug:
Bug:
Bug:
Test: ScrollableFocusableInteractionTest
Test: androidx.compose.foundation.relocation.*
Test: RebasableAnimationStateTest
Test: BringIntoViewRequestPriorityQueueTest
Relnote: "Rewrote the way scrollables respond to
`bringIntoViewRequesters` and focusables to better model the
complexity of those operations and handle more edge cases."
Change-Id: I2e5fec8c8582a8fe1f191e37fd0f4f9165678664
M compose/foundation/foundation/api/current.txt
M compose/foundation/foundation/api/public_plus_experimental_current.txt
M compose/foundation/foundation/api/restricted_current.txt
M compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
M compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ScrollableFocusedChildDemo.kt
A compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/relocation/BringIntoViewResponderDemo.kt
M compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/relocation/BringRectangleIntoViewDemo.kt
M compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BringIntoViewSamples.kt
M compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt
M compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveTest.kt
M compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
M compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewScrollableInteractionTest.kt
A compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewRequestPriorityQueue.kt
M compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewModifier.kt
A compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/UpdatableAnimationState.kt
M compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
A compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/BringIntoViewRequestPriorityQueueTest.kt
A compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/UpdatableAnimationStateTest.kt
el...@gmail.com <el...@gmail.com> #7
Hi again! Any updates on this? Seems like there's some progress happening, is any version available for us to test with the fixed behavior?
kl...@google.com <kl...@google.com> #8
Releases are generally paused during most of December and resume again in January. The release including these fixes should be coming out in the next few days, if everything goes according to plan.
el...@gmail.com <el...@gmail.com> #9
Thanks, I'll be waiting :D
kl...@google.com <kl...@google.com> #10
My bad, this particular bug is still not fixed in 1.4.0-alpha04 – the case where the field takes up all available space. The cases where it doesn't are fixed. No timeline to share yet for when this case will be fixed.
el...@gmail.com <el...@gmail.com> #11
Ah that's alright, it's pretty relieving to see progress as issues with TextFields and scrolling have been seeing slow progress for quite some time.
Thanks for all the efforts! Hope 1.4.0-alpha05 fixes this one.
ma...@gmail.com <ma...@gmail.com> #12
Bug is not solved in 1.4.0-alpha05
el...@gmail.com <el...@gmail.com> #13
Yep, they have no timeline for when it's going to be fixed :p
The above workaround is super inefficient for large texts, as using verticalScroll renders the entirety of the BasicTextField instead of only the visible lines (which i guess is the default behavior of BasicTextField's internal scrolling), causing a lot of lag in scrolling.
At least having something like
yv...@gmail.com <yv...@gmail.com> #14
For automatic scrolling to the clicked row, you can programmatically press the space key to focus the basictextfield on the clicked row.
You only need to do this when the keyboard is opened and there is no selected text.
val keyOpened = keyboardAsState().value == Keyboard.Opened
var noteText by remember { mutableStateOf(TextFieldValue(text = "")) }
val scope = rememberCoroutineScope()
LaunchedEffect(keyOpened){
if (keyOpened && noteText.selection.length == 0){
scope.launch(Dispatchers.IO) {
try {
val inst = Instrumentation()
delay(100)
inst.sendKeyDownUpSync(KeyEvent.KEYCODE_SPACE)
delay(100)
inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DEL)
}catch (ex: InterruptedException) {
println("Hata Key: "+ex.localizedMessage)
}
}
}
}
BasicTextField(
value = notText,
onValueChange =
{
notText = it
},
textStyle = TextStyle(
Color.Black, fontSize = 19.sp,
fontFamily = FontManager().robotFontFamily,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Start
),
cursorBrush = Brush.verticalGradient(colors = listOf(Color(0xFF199800), Color(0xFF199800))),
modifier = Modifier
.fillMaxSize()
.focusRequester(focusRequester)
.background(Color.Transparent)
) { innerText ->
innerText()
}
ja...@gmail.com <ja...@gmail.com> #15
el...@gmail.com <el...@gmail.com> #16
Yeah it kinda sucks. They seem to be doing some architectural changes to TextFields for 1.5, so fingers crossed for that.
For anyone still experiencing this, the workaround I wrote in
el...@gmail.com <el...@gmail.com> #17
Hi! The new BasicTextField2 in Compose 1.6 alphas seems really promising. For now it seems like the issue of staying on top of the keyboard while typing is fixed; however, on a long text, selecting the text in the area where the keyboard is about to show doesn't scroll the text field to keep the cursor in view.
Once that gets fixed, this issue will be fully resolved and can be effectively closed!
Any updates or ETAs on that?
na...@gmail.com <na...@gmail.com> #18
ro...@gmail.com <ro...@gmail.com> #19
Any new developments on this issue? I'm using BasicTextField to implement a rich edit window (it can attach images, etc.), but it won't keep the text above the keyboard if I add Modifier.verticalScrollable()
:(
which I think is still pretty important
ap...@google.com <ap...@google.com> #20
Branch: androidx-main
commit 6a7220cc4ecc9b6c54e2433a2ade60a360f72690
Author: Zach Klippenstein <klippenstein@google.com>
Date: Wed Jan 17 15:52:42 2024
Ensures BTF2 cursor is scrolled back into view when typing.
If the cursor is scrolled out of view, either from the field's internal
scrollable or something outside it, and the cursor is moved or text is
entered, the cursor should be scrolled back into view as long as the
field is focused. This change makes that happen for BTF2.
This supersedes my original impl, aosp/2178021.
Bug:
Test: TextFieldScrollTest
Relnote: "`BasicTextField2` now keeps the cursor in view while typing
when it has been scrolled out of view or would move out of view due
to input."
Change-Id: Ieb85691dd1a7cf98ab5fc188721d4e4475aec762
M compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldScrollTest.kt
M compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
M compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt
M compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldDecoratorModifier.kt
M compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/TextLayoutState.kt
el...@gmail.com <el...@gmail.com> #21
Looks promising! Does that mean this is finally fixed? And when can we expect this to be released so we can test it?
kl...@google.com <kl...@google.com> #22
It will be fixed in BasicTextField2
in the next 1.7.0 alpha release, alpha02. We are not planning to fix it in the current BasicTextField
.
el...@gmail.com <el...@gmail.com> #23
Great news finally!
This is a bit unrelated, but will the name of the BasicTextField2
API be always like this? Or will it eventually replace BasicTextField
?
Because with the 2
it sounds like this "secondary" thing, not like the primary API that everyone should be using.
Other than that, thanks for the amazing work!
el...@gmail.com <el...@gmail.com> #25
Great to hear! Thanks for sharing the talk, and props to you guys for this amazing work!
kl...@google.com <kl...@google.com>
ja...@gmail.com <ja...@gmail.com> #26
el...@gmail.com <el...@gmail.com> #27
ja...@gmail.com <ja...@gmail.com> #28
when it has been scrolled out of view or would move out of view due
to input." this BasicTextField2 not scrolling text above keyboard even keyboad is not opened after clicking text for the second time
el...@gmail.com <el...@gmail.com> #29
Okay I just played with the new BasicTextField... and it seems like this has been only half-fixed.
Inputting anything from the keyboard brings the cursor into focus, which is great. Adding new lines keeps the cursor in view, which is great.
However, tapping the text field (which opens the IME) in an area that will eventually be under the keyboard, does not scroll the cursor to stay in view.
My wacky workaround at the very beginning of this issue does that correctly, and not only for the keyboard, but for any size changes (my app has, in addition to system keyboard, an internal keyboard, and it works the exact same way). If the cursor is in view, I keep it in view no matter what. If the cursor has been scrolled out-of-view, then it's okay to bring it in view only once there's input from the user.
Is this behavior intended? I'm not 100% sure, but I believe that Android's EditText
was keeping the cursor in view once the IME is shown.
I can make a video demonstration with all of these (New BasicTextField, legacy BasicTextField, my workaround, and EditText)
el...@gmail.com <el...@gmail.com> #30
In terms of the keyboard not showing at all the second time you click the text field, I think that's a different bug, and should be reported as its own issue with all the context of where and how that happens to you.
Also note that BasicTextField2
from 1.6.x releases is outdated, so make sure to use the new BasicTextField
in 1.7.0 betas.
da...@gmail.com <da...@gmail.com> #31
To
el...@gmail.com <el...@gmail.com> #32
imePadding()
Modifier on the BasicTextField
, and it doesn't work even with it.
This isn't even IME-specific; any change in the TextField's size should try to keep the cursor in the viewport (if it was already visible before the size change).
imePadding
does exactly just that: add padding, which in turn shrinks the size of the BasicTextField from the bottom. Currently, the new BasicTextField does not react to this at all, thus causing the cursor to stay under the keyboard.
My workaround (from
It is still, however, a workaround (a wacky one to be clear, I'm not even sure I understand my own code, but it works flawlessly in production), and there's nothing than a proper upstream fix for everybody, that would be more performant and just less wacky.
ja...@gmail.com <ja...@gmail.com> #33
el...@gmail.com <el...@gmail.com> #34
BasicTextField2
that's in Compose 1.6.x is outdated.
If you want to try the latest API, update to Compose 1.7.0-beta03 (which is the latest beta as of now), and use BasicTextField
(which now uses the new implementation). This issue is mostly fixed in 1.7.
bringIntoViewRequester
is irrelevant here, because we're talking about the internal scroll of the text field (especially in the case when it takes full screen space). It's not about scrolling some LazyList to keep the field in view, it's about scrolling the field itself to keep the cursor in view.
Again, if you update to 1.7.0 and still face the issue of the keyboard not opening the second time, that's a different issue, please report it separately.
mi...@gmail.com <mi...@gmail.com> #35
el...@gmail.com <el...@gmail.com> #36
Modifier.imePadding()
, but the cursor will still come into view... once you type something.
However, since I still have to use the older BasicTextField
sometimes because I need AnnotatedString
, and because the current fix doesn't satisfy me, I just use a workaround for now, using BringIntoViewRequester
. It's much simpler, cleaner and easier to understand than the abomination I wrote in
To keep the cursor in view on text changes (like the current fix), you need to add a bringIntoViewRequester
to your BasicTextField
, and then get use the TextLayoutResult
from onTextLayout
to get the bounding of the current cursor position, and call request bringIntoView
using that Rect.
To complete the fix, you'll wanna store the TextLayoutResult
in a state, and add a LaunchedEffect
on TextFieldValue.selection
/TextFieldState.selection
changes, which does the exact same thing as before (calling bringIntoView
with cursor rect).
Do the same on size changes (in Modifier.onPlaced
), and you're fully set. The cursor will always be brought to view on size/selection/text changes, while still allowing users to scroll it away when none of these change, works well on desktop, and you can of course choose which triggers to keep.
You can also add some padding to the Rect
you pass to bringIntoView
, for example to keep some space between the line where the cursor is and the IME/bottom/top of screen.
I generally use this alongside a verticalScroll
modifier to apply some internal non-clipping padding, but it should work fine without it, at least on the new BasicTextField
.
Description
Hi there! I've come across a bug in Compose's BasicTextField, which is that the cursor stays behind the keyboard when the clicked line is in the place where the keyboard is about to show. This is because the BasicTextField is occupying the whole screen.
Android's TextView behaves correctly and bumps the selected line to show above the keyboard.
A video is attached with the Jetpack Compose behavior versus how it behaves with a TextView.
This bug can be reworded as "Scroll selected line into view when BasicTextField is resized", as the BasicTextField is essentially resized because of the IME insets.
Here's the code:
Note that
android:windowSoftInputMode
is set toadjustResize
.