Search code examples
android-jetpack-composeandroid-jetpackandroid-tvandroid-jetpack-compose-tv

How to always focus on the first item while focussing on a TVLazyRow?


I am trying to implement a TVLazyRow where when we focus on the row from some other composable above the row it must always focus on the first item first. Currently, when pressing down, the focus goes to whichever item in the row is directly below the composable above. How can I achieve this behaviour?

Here is my code for more context:

val tvListState = rememberTvLazyListState()
val coScope = rememberCoroutineScope()

TvLazyRow(
        horizontalArrangement = Arrangement.spacedBy(15.dp),
        state = tvListState,
        modifier = modifier
            .fillMaxHeight()
            .padding(end = 5.dp)
            .onFocusChanged {
                if (it.isFocused) {
                    coScope.launch {
                        tvListState.scrollToItem(0)
                    }
                }
            }, pivotOffsets = PivotOffsets(0f)
) { *items* }

Solution

  • Latest: November 1st, 2023

    The focus restoration API is now improved and accepts a fallback focus requester which will be used when visiting a unvisited list. Because of this new change, we now don't need the modifier factory that I created earlier.

    val firstItemToGainFocusFr = remember { FocusRequester() }
    
    TvLazyRow(
      modifier = Modifier.focusRestorer { firstItemToGainFocusFr }
    ) {
      item { 
        Button(
          onClick = {}, 
          modifier = Modifier.focusRequester(firstItemToGainFocusFr)
        ) { 
          Text("My Button 1") 
        } 
      }
      item { Button(onClick = {}) { Text("My Button 2") } }
      item { Button(onClick = {}) { Text("My Button 3") } }
      // ...
    }
    

    Edit August 11th, 2023

    You can make use of the focusRestoration APIs which were released recently in the alpha version of compose foundation. Reference: saveFocusedChild() and restoreFocusedChild()

    You can tap into the focusProperties and when the focus enters the container, you can check if there was a previously saved focused child. If yes, you transfer focus to that child, else, you transfer focus to the first child. While exiting the container, you can save the previously focused child.

    You can create the following modifier factory which will abstract away this logic for you:

    data class FocusRequesterModifiers(
        val parentModifier: Modifier,
        val childModifier: Modifier
    )
    
    @OptIn(ExperimentalComposeUiApi::class)
    @Composable
    fun createFocusRestorationModifiers(): FocusRequesterModifiers {
        val focusRequester = remember { FocusRequester() }
        val childFocusRequester = remember { FocusRequester() }
    
        val parentModifier = Modifier
            .focusRequester(focusRequester)
            .focusProperties {
                exit = {
                    focusRequester.saveFocusedChild()
                    FocusRequester.Default
                }
                enter = {
                    if (!focusRequester.restoreFocusedChild())
                        childFocusRequester
                    else
                        FocusRequester.Cancel
                }
            }
    
        val childModifier = Modifier.focusRequester(childFocusRequester)
    
        return FocusRequesterModifiers(parentModifier, childModifier)
    }
    

    With the above factory in place, you can make use of it in your lazy containers like following:

    val modifiers = createFocusRestorationModifiers()
    
    TvLazyRow(
      modifier = Modifier.then(modifiers.parentModifier)
    ) {
      item { 
        Button(
          onClick = {}, 
          modifier = Modifier.then(modifiers.childModifier)
        ) { 
          Text("My Button 1") 
        } 
      }
      item { Button(onClick = {}) { Text("My Button 2") } }
      item { Button(onClick = {}) { Text("My Button 3") } }
      // ...
    }
    

    Notice, in the above example usage that we just need to assign the parent modifier to the TvLazyRow container and the child modifier to the first item. You can choose to assign it to the second or third item as well in case you desire that the second or third item should gain focus for the first time.