Reputation: 4413
I've noticed an issue that consistently happens when combining LazyVerticalGrid
default focus handling with smooth scroll animations. If I hold down the up button on the remote the grid will start up focusing to the item directly above the previous item in the same column then as the scroll speeds up it will jump over to the last column.
I have a guess as to why this is. The system does not see the target item in the layout so it defaults to the "closest" 1-dimensional item index the target row relative to the previous item which is always in the last (far right) column.
Any idea how to workaround this limitation and maintain column during focus traversal? I've tried setting up custom focus traversal with .focusProperties {}
modifier, defining the up and down directions, but once the target item is no longer in the layout it throws an exception because it's trying to traverse to a FocusRequester the isn't installed.
Update: Goggle knows about this on their issue tracker. Appears it has to do with LazyGrid
using the same "BeyondBoundsLayout modifier" as LazyList
which looks only at the first offscreen item to move focus to, not the entire row.
Minimal Reproducible Example (Trouble Building? Paste into New Project -> Television -> Empty Activity template)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val colors = arrayOf(
Color.Red,
Color(255, 165, 0),
Color.Yellow,
Color.Green
)
setContent {
LazyVerticalGrid(
state = rememberLazyGridState(),
columns = GridCells.Fixed(4)
) {
for (i in 0 until 100) {
item {
ClassicCard(
modifier = Modifier.padding(10.dp),
image = {
Box(modifier = Modifier
.fillMaxWidth()
.aspectRatio(16/9f)
.background(colors[i % 4]))
},
title = {
Text("Item ${i + 1}")
},
onClick = {}
)
}
}
}
}
}
}
Custom Focus Traversal Example (Throws IllegalStateException if up or down button is held)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val numItems = 100
val cols = 4
val colors = arrayOf(
Color.Red,
Color(255, 165, 0),
Color.Yellow,
Color.Green
)
val focusRequesters = Array(numItems) { FocusRequester() }
setContent {
LazyVerticalGrid(
state = rememberLazyGridState(),
columns = GridCells.Fixed(cols)
) {
for (i in 0 until numItems) {
val addUpFocus = i - cols in focusRequesters.indices
val addDownFocus = i + cols in focusRequesters.indices
val focusPropertiesMod = when {
addUpFocus && addDownFocus -> {
Modifier.focusProperties {
up = focusRequesters[i - cols]
down = focusRequesters[i + cols]
}
}
addUpFocus -> {
Modifier.focusProperties {
up = focusRequesters[i - cols]
}
}
addDownFocus -> {
Modifier.focusProperties {
down = focusRequesters[i + cols]
}
}
else -> {
Modifier
}
}
item {
ClassicCard(
modifier = Modifier
.padding(10.dp)
.focusRequester(focusRequesters[i])
.then(focusPropertiesMod),
image = {
Box(modifier = Modifier
.fillMaxWidth()
.aspectRatio(16/9f)
.background(colors[i % 4]))
},
title = {
Text("Item ${i + 1}")
},
onClick = {}
)
}
}
}
}
}
}
Dependencies
[versions]
agp = "8.6.0-alpha06"
kotlin = "1.9.0"
coreKtx = "1.15.0"
appcompat = "1.7.0"
composeBom = "2025.01.01"
tvFoundation = "1.0.0-alpha12"
tvMaterial = "1.0.0"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.10.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-tv-foundation = { group = "androidx.tv", name = "tv-foundation", version.ref = "tvFoundation" }
androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.ref = "tvMaterial" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
Upvotes: 1
Views: 22