Status Update
Comments
ma...@gmail.com <ma...@gmail.com> #2
Routing per go/androidx/owners for insets.
rv...@google.com <rv...@google.com> #3
Do you mind checking why FragmentContainerView
still calls ViewCompat.dispatchApplyWindowInsets
when the OnApplyWindowInsetsListener
set to it always returns the consumed insets as
I also found that FragmentContainerView.kt:210
doesn't call ViewCompat.dispatchApplyWindowInsets
but androidx.fragment.fragment 1.8.6
from the stack trace.
ma...@gmail.com <ma...@gmail.com> #4
Not sure, FragmentContainerView
doesn't have that kind of control and just hands off to the core functions. Also the lines not matching up does likely mean the version are off. Can we get a sample project that reproduces the issue and we can have a closer look?
ma...@gmail.com <ma...@gmail.com> #5
Hi enxyox@,
Do you know if this can happen on Pixel devices or emulators which run Android 8.0.0? If so, do you mind sharing a sample app for us to reproduce and investigate?
ni...@persgroep.net <ni...@persgroep.net> #6
I couldn't reproduce this myself on an emulator running Android 8.0.0, so I only have stack traces from Crashlytics/Sentry. We've now gathered a bit more data, and the crash also occurs on Android 9.0 on the S119 device, though the majority (96%) is still on Android 8.0.0.
Some users experienced the crash when switching fragments (navigating to another screen), some right upon opening the app, and others when changing configuration. I can say for sure that this happens under rare conditions, as in our case, it has affected about 14% of users on Android 8.0.0.
ho...@gmail.com <ho...@gmail.com> #7
rv...@google.com <rv...@google.com> #8
I do have a hacky way to do this. I am not proud of this approach but it gets the job done.
==============================
IMPORTANT!!!!!!!!!: For this to work, the buttons in the cards need to have different widths. They should differ by at least 5px. You can easily do this by adding some 2dp
or 3dp
padding to one of them which is barely noticeable._
This is needed because I am using the widths to identify each button in BringIntoView spec.
==============================
Checkout the attached video for demo.
App.kt
@Composable
fun App() {
var cardWidth by remember { mutableStateOf<Float?>(null) }
var firstButtonWidth by remember { mutableStateOf<Float?>(null) }
var secondButtonWidth by remember { mutableStateOf<Float?>(null) }
var cardLeftOffset by remember { mutableStateOf<Float?>(null) }
var firstButtonLeftOffset by remember { mutableStateOf<Float?>(null) }
var secondButtonLeftOffset by remember { mutableStateOf<Float?>(null) }
val firstButtonDistanceFromCardLeft = remember(firstButtonLeftOffset, cardLeftOffset) {
derivedStateOf {
when {
firstButtonLeftOffset == null -> null
cardLeftOffset == null -> null
else -> firstButtonLeftOffset!! - cardLeftOffset!!
}
}
}
val secondButtonDistanceFromCardLeft =
remember(secondButtonLeftOffset, cardLeftOffset) {
derivedStateOf {
when {
secondButtonLeftOffset == null -> null
cardLeftOffset == null -> null
else -> secondButtonLeftOffset!! - cardLeftOffset!!
}
}
}
PositionFocusedItemInLazyLayoutHacky(
cardWidth = cardWidth ?: 0f,
firstButtonWidth = firstButtonWidth ?: 0f,
secondButtonWidth = secondButtonWidth ?: 0f,
firstButtonLeftOffset = firstButtonDistanceFromCardLeft.value ?: 0f,
secondButtonLeftOffset = secondButtonDistanceFromCardLeft.value ?: 0f,
) {
LazyRow(
Modifier
.fillMaxWidth()
.padding(top = 20.dp),
contentPadding = PaddingValues(20.dp),
horizontalArrangement = Arrangement.spacedBy(20.dp)
) {
items(20) { buttonIndex ->
Surface(
modifier = Modifier
.width(220.dp)
.aspectRatio(16f / 9)
.onPlaced {
if (buttonIndex == 0 && cardLeftOffset == null && cardWidth == null) {
cardLeftOffset = it.positionInRoot().x
cardWidth = it.size.width.toFloat()
}
},
shape = RoundedCornerShape(10.dp)
) {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Button(
onClick = { },
modifier = Modifier.onPlaced {
if (buttonIndex == 0 && firstButtonWidth == null && firstButtonLeftOffset == null) {
firstButtonWidth = it.size.width.toFloat()
firstButtonLeftOffset = it.positionInRoot().x
}
},
) {
Text("Button 1")
}
Spacer(Modifier.width(20.dp))
Button(
onClick = { },
modifier = Modifier.onPlaced {
if (buttonIndex == 0 && secondButtonWidth == null && secondButtonLeftOffset == null) {
secondButtonWidth = it.size.width.toFloat()
secondButtonLeftOffset = it.positionInRoot().x
}
},
) {
Text("Button 100")
}
}
}
}
}
}
}
PositionFocusedItemInLazyLayoutHacky.kt
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PositionFocusedItemInLazyLayoutHacky(
parentFraction: Float = 0.3f,
childFraction: Float = 0f,
cardWidth: Float = 0f,
firstButtonWidth: Float = 0f,
secondButtonWidth: Float = 0f,
firstButtonLeftOffset: Float = 0f,
secondButtonLeftOffset: Float = 0f,
content: @Composable () -> Unit,
) {
val bringIntoViewSpec =
remember(
parentFraction,
childFraction,
firstButtonWidth,
secondButtonWidth,
firstButtonLeftOffset,
secondButtonLeftOffset,
cardWidth
) {
object : BringIntoViewSpec {
override fun calculateScrollDistance(
// initial position of item requesting focus
offset: Float,
// size of item requesting focus
size: Float,
// size of the lazy container
containerSize: Float
): Float {
val childSmallerThanParent = cardWidth <= containerSize
val initialTargetForLeadingEdge =
parentFraction * containerSize - (childFraction * cardWidth)
val spaceAvailableToShowItem = containerSize - initialTargetForLeadingEdge
val targetForLeadingEdge =
if (childSmallerThanParent && spaceAvailableToShowItem < cardWidth) {
containerSize - cardWidth
} else {
initialTargetForLeadingEdge
}
val cardOffset = when (size) {
firstButtonWidth -> offset - firstButtonLeftOffset
secondButtonWidth -> offset - secondButtonLeftOffset
// if you have more buttons, add more conditions here
else -> offset
}
return cardOffset - targetForLeadingEdge
}
}
}
// LocalBringIntoViewSpec will apply to all scrollables in the hierarchy.
CompositionLocalProvider(
LocalBringIntoViewSpec provides bringIntoViewSpec,
content = content,
)
}
ma...@gmail.com <ma...@gmail.com> #9
its sometimes hard to understand if the replies from google or just someone with a google email..
either way I'm grateful for replying and try to solve the issue, but this is too hacky to use on production, and its not solve a lot of issues that still can happen (like a button that is depend of the other button and can change it size, a button that can be disappeared after clicking the first button and etc..)
rv...@google.com <rv...@google.com> #10
Thanks for the feedback! As mentioned in the solution, the approach is a workaround specifically for the use case described in
The PivotOffset solution in BringIntoViewContainer is designed with Android TV UI guidelines in mind. These guidelines recommend a single focusable element, typically a card, within a LazyRow. This is because TVs generally have a different interaction mechanism than mobile or desktop devices.
Our library aims to provide APIs and components that cater to the majority of use cases (around 90%). Supporting every possible scenario could significantly increase complexity for most users. The TV guidelines themselves are based on extensive research and analysis of popular TV apps, aiming to address common design patterns.
Your specific use case, involving buttons with dynamic behavior and dependencies, represents a more specialized and complex UI experience. While we appreciate your input, such designs might deviate from established TV UI guidelines.
In situations where the library's current functionality doesn't fully align with your needs, we encourage developers to explore customizing the components. In this instance, you could consider forking the LazyRow component and modifying the BringIntoView spec implementation to achieve your desired behavior.
Rest assured, we are continuously monitoring user feedback and requests. If we observe a growing demand for similar functionalities, we will certainly re-evaluate our design choices and potentially introduce new features or updates to better accommodate such use cases.
rv...@google.com <rv...@google.com> #11
To ensure a smooth and intuitive user experience, we recommend following the Android TV UI guidelines. These guidelines are based on extensive UX research and offer valuable insights for designing effective TV interfaces.
We encourage you to follow these guidelines while building UI experiences for TV devices. Following these guidelines can also help you leverage existing components that work seamlessly out-of-the-box or require minimal adjustments, streamlining your UI development process.
You can find the guidelines here:
Checkout all the following sections from the above doc:
- Foundations
- Components
- Styles
ho...@gmail.com <ho...@gmail.com> #12
ty...@gmail.com <ty...@gmail.com> #13
ru...@gmail.com <ru...@gmail.com> #14
This is also blocking our team from going full compose for some layouts. For now we have to use
ca...@gmail.com <ca...@gmail.com> #15
as a temporary workaround, you can use the following approach until an official fix is released:
@OptIn(ExperimentalFoundationApi::class)
internal fun Modifier.bringIntoViewIfChildrenAreFocused(): Modifier = composed(
inspectorInfo = debugInspectorInfo { name = "bringIntoViewIfChildrenAreFocused" },
factory = {
var rect by remember { mutableStateOf(Rect.Zero) }
onSizeChanged {
// Update the current visible rectangle based on the container's size.
rect = Rect(
topLeft = Offset.Zero,
bottomRight = Offset(it.width.toFloat(), it.height.toFloat())
)
}.bringIntoViewResponder(
remember {
object : BringIntoViewResponder {
@ExperimentalFoundationApi
override fun calculateRectForParent(localRect: Rect): Rect = rect
@ExperimentalFoundationApi
override suspend fun bringChildIntoView(localRect: () -> Rect?) {
// No action required as the container is non-scrollable.
}
}
}
)
}
)
apply it to your cell's modifiers like this:
Cell(
modifier = modifier
.bringIntoViewIfChildrenAreFocused()
// ...
)
this ensures the view behaves correctly in the interim
ma...@gmail.com <ma...@gmail.com> #16
To be clear let's say I have a lazyColunn and inside of it items and on each item I have 2 buttons
Should I add it to to the lazyColunn/ item / each button ?
That's not so clear.
Another thing is - have you develop a stable fix for this issue ? If the next release will have better performance we would like to wait for it
This would be much appreciated if we get a full implementation of it, it's not clear also if the lazyColunn should be wrapped in PivotOffset composable as well.
ca...@gmail.com <ca...@gmail.com> #17
you should add the modifier to the parent item (e.g. a Card
) that contains the focusable buttons. this approach ensures that the parent item is brought into the desired position when a child button is focused.
in your example (LazyColumn/ item/ each button), the modifier should be applied to the item
.
this solution is stable and has been used in production. however, please note that I am not an official Google developer, so I can't speak to when an official fix for this issue might be released.
here's a simple implementation example:
LazyRow(...) {
Card(
modifier = Modifier.bringIntoViewIfChildrenAreFocused()
) {
Button()
Button()
}
}
Description
Version used: kotlin 2 (automatic jetpack compose compiler)
Devices/Android versions reproduced on: TV devices
If this is a bug in the library, we would appreciate if you could attach:
In TV development we sometimes need child components that is focusable (buttons) inside the component in the lazy lists (TVLazyRow / TVLazyColumn).
in picture "expected behavior pivotoffsets" - raw explanation of the expected behavior from the PivotOffset
in picture "actually behavior pivotoffsets" - what actually happens.
in video - "column shaking and focus change effects pivotoffsets" - in this video you can see a dummy lazy column and scroll between items that has another 2 components that are focusable.
video sections in time line
00:00 ~ 00:15 - scrolling to the bottom of the list and then one time up and then focus right to button 2 (list is unexpected scroll vertically) and repeat.
00:18 ~ 00:33 - scroll from the top of the list down. after reaching to item 3 until almost the end of the list, the list start to shaking, (its not a recomposition by the layout inspector)
I think the bug lays when u searching the focused item (isFocused) and not the parent item that has focus (hasFocus)
for reproduce it in the project in main activity use
val bugType = BugType.FocusRestorerBug
- Sample project to trigger the issue - attached
please choose at the main activity -
val bugType = BugType.PivotOffsetsBug
- A screenrecord or screenshots showing the issue (if UI related) - attached