androidandroid-jetpack-composeandroid-tvandroid-jetpack-compose-tv

Jetpack Compose focus jumps erratically using D-Pad navigation on Android TV


I have been experiencing very erratic jumping when using Jetpack Compose’s LazyColumns on Android TV. D-Pad navigation is supposed to be supported in Compose for a while now, but it seems simple cases are not supported—or I am doing something terribly wrong when setting a custom focus overlay.

The follow code results in what is shown on this video. As you can see, I am simply navigating step by step from top to bottom but the focused item jumps very randomly in between. It feels like the number are reproducible, but I have not stopped to write them down to verify.

@Composable
fun Greeting(listItems: List<Int>) {
    var currentItem by remember { mutableStateOf("None") }
    val scrollState = rememberLazyListState()
    val scope = rememberCoroutineScope()

    Row {
        Text(
            text = "Current Focus = $currentItem",
            modifier = Modifier.weight(1f)
        )

        Column(Modifier.weight(1f)) {
            Text(text = "With focus changed")
            LazyColumn(state = scrollState) {
                itemsIndexed(listItems) { index, item ->
                    Item(
                        item,
                        { currentItem = "Left $item" },
                        Modifier.onFocusChanged { focusState ->
                            scope.launch {
                                if (focusState.isFocused) {
                                    val visibleItemsInfo = scrollState.layoutInfo.visibleItemsInfo
                                    val visibleSet = visibleItemsInfo.map { it.index }.toSet()
                                    if (index == visibleItemsInfo.last().index) {
                                        scrollState.scrollToItem(index)
                                    } else if (visibleSet.contains(index) && index != 0) {
                                        scrollState.scrollToItem(index - 1)
                                    }
                                }
                            }
                        }
                    )
                }
            }
        }

        Column(Modifier.weight(1f)) {
            Text(text = "Without focus changed")
            LazyColumn {
                items(listItems) { item ->
                    Item(
                        item,
                        { currentItem = "Right $item" }
                    )
                }
            }
        }
    }
}

@Composable
fun Item(
    item: Int,
    onFocused: () -> Unit,
    modifier: Modifier = Modifier,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
    val focused by interactionSource.collectIsFocusedAsState()

    Text("$item", modifier = modifier
        .onFocusChanged { state ->
            if (state.isFocused) {
                onFocused()
            }
        }
        .focusable(true, interactionSource)
        .padding(8.dp)
        .border(if (focused) 4.dp else 0.dp, MaterialTheme.colors.primary)
        .padding(8.dp)
    )
}

At first I thought I was doing something incorrectly and it is recomposing but different ways of checking the focus as well as just using plain buttons which already have a focus state (a very bad one for TV tbf) results in the exact same issue.


Solution

  • After reporting this to Google, it turns out that it actually was a bug in Jetpack Compose, which was fixed in the latest version 1.3.0-rc01.