Ondřej Kycelt
09/17/2024, 1:25 PMx
coordinate, the focus search skips it and jumps to another row. The minimal repro below uses LazyVerticalGrid
, but I'm encountering the same issue with a more complex layout based on MinaBox
. Has anyone else experienced this issue? Is there any workaround other than adjusting item sizes?
@Composable
private fun TestFocusSearch(
modifier: Modifier = Modifier
) {
LazyVerticalGrid(
columns = GridCells.Fixed(4),
contentPadding = PaddingValues(horizontal = 64.dp, vertical = 48.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(12) {
FocusableBox()
}
item(
span = { GridItemSpan(maxLineSpan) }
) {
FocusableBox()
}
items(12) {
FocusableBox()
}
}
}
@Composable
private fun FocusableBox(
modifier: Modifier = Modifier
) {
var isFocused by remember { mutableStateOf(false) }
Box(
modifier = modifier
.height(40.dp)
.drawBehind {
drawRect(if (isFocused) Color.Red else Color.Gray)
}
.onFocusChanged { isFocused = it.isFocused }
.focusable()
)
}
Zach Klippenstein (he/him) [MOD]
09/17/2024, 4:04 PMOndřej Kycelt
09/17/2024, 4:20 PMOndřej Kycelt
09/30/2024, 3:36 PMTwoDimensionalFocusSearch.kt
and tried to fix it via https://android-review.googlesource.com/c/platform/frameworks/support/+/3271691. It’s my first attempt at a contribution, so not sure how the process works exactly. May I ask how long it usually takes for somebody to look at it?Zach Klippenstein (he/him) [MOD]
09/30/2024, 3:44 PMOndřej Kycelt
09/30/2024, 5:40 PMZach Klippenstein (he/him) [MOD]
09/30/2024, 5:44 PMRalston Da Silva
09/30/2024, 9:06 PMRalston Da Silva
09/30/2024, 11:26 PM@Composable
fun TestFocusSearch(
modifier: Modifier = Modifier
) {
val columnCount = 4
fun isLastLine(itemIndex: Int, itemCount: Int): Boolean = itemIndex >= itemCount - columnCount
fun isFirstLine(itemIndex: Int) = itemIndex < columnCount
val middleItem = remember { FocusRequester() }
LazyVerticalGrid(
columns = GridCells.Fixed(columnCount),
contentPadding = PaddingValues(horizontal = 64.dp, vertical = 48.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(12) {
if (isLastLine(it, 12)) {
FocusableBox(Modifier.focusProperties{ down = middleItem })
} else {
FocusableBox()
}
}
item(span = { GridItemSpan(maxLineSpan) }) {
FocusableBox(Modifier.focusRequester(middleItem))
}
items(12) {
if(isFirstLine(it)) {
FocusableBox(Modifier.focusProperties{ up = middleItem })
} else {
FocusableBox()
}
}
}
}
The benefit to this approach is that you get more control of the focus traversal order. For instance, the example video indicates another issue. Once the large item is focused pressing up/down always takes you to the 2nd column regardless of where focus came from (before the large item was focused). This can be fixed by using focus groups and specifying a custom exit property (An alternate approach would be to use onFocusChanged and the down/up focus property)
@Composable
fun TestFocusSearch(
modifier: Modifier = Modifier
) {
var nextUpItem: FocusRequester? = null
var nextDownItem: FocusRequester? = null
val columnCount = 4
fun isLastLine(itemIndex: Int, itemCount: Int): Boolean = itemIndex >= itemCount - columnCount
fun isFirstLine(itemIndex: Int) = itemIndex < columnCount
val before = remember{ Array(columnCount) { FocusRequester() } }
val after = remember{ Array(columnCount) { FocusRequester() } }
val middleItem = remember { FocusRequester() }
LazyVerticalGrid(
columns = GridCells.Fixed(columnCount),
contentPadding = PaddingValues(horizontal = 64.dp, vertical = 48.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(12) { index ->
if (isLastLine(index, 12)) {
FocusableBox(
Modifier
.focusProperties{
exit = {
if (it == Down) {
nextUpItem = before[index%columnCount]
nextDownItem = after[index%columnCount]
middleItem
} else {
Default
}
}
}
.focusGroup()
.focusRequester(before[index%columnCount])
)
} else {
FocusableBox()
}
}
item(span = { GridItemSpan(maxLineSpan) }) {
FocusableBox(
Modifier
.focusProperties {
exit = {
when(it) {
Up -> nextUpItem
Down -> nextDownItem
else -> null
} ?: Default
}
}
.focusGroup()
.focusRequester(middleItem)
)
}
items(12) { index ->
if(isFirstLine(index)) {
FocusableBox(
Modifier
.focusProperties{
exit = {
if (it == Up) {
nextUpItem = before[index%columnCount]
nextDownItem = after[index%columnCount]
middleItem
} else {
Default
}
}
}
.focusGroup()
.focusRequester(after[index%columnCount])
)
} else {
FocusableBox()
}
}
}
}
Here is what this looks like:Ondřej Kycelt
10/01/2024, 7:46 AMMinaBox
(https://github.com/oleksandrbalan/minabox) which is a 2D draggable/scrollable LazyLayout
.Ondřej Kycelt
10/01/2024, 8:16 AMI noticed the sample code works in portrait mode without any changes and the fix is only needed in landscape mode. This makes me feel like adjusting the weight for the major axis might not fix this for all cases.The sample code works in portrait mode on a phone, because the full-span item isn't wide enough for the focus search algorithm to skip it. If the item height is smaller, the full-span item is skipped even on a phone in portrait mode (see the attached video).
Ondřej Kycelt
10/07/2024, 11:10 AMRalston Da Silva
10/08/2024, 11:45 PMRalston Da Silva
10/08/2024, 11:49 PMOndřej Kycelt
10/09/2024, 6:32 AMOndřej Kycelt
10/09/2024, 6:32 AMfocusProperties
? If so, is it something that can be done "quickly"?Ondřej Kycelt
10/09/2024, 6:34 AMRalston Da Silva
10/09/2024, 5:58 PMOndřej Kycelt
10/10/2024, 9:01 PMFocusRequester
s, specifying up
and down
focus property for each program, and so far it seems to perform okay even for 800+ channels, so we're probably gonna go that way. Thanks for your time 🙂