Rahul Rawat
Rahul Rawat

Reputation: 237

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* }

Upvotes: 5

Views: 3537

Answers (1)

vighnesh153
vighnesh153

Reputation: 5416

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.

Upvotes: 12

Related Questions