Search code examples
androidandroid-jetpack-composeandroid-tvandroid-jetpack-compose-tv

How to return focus back the the previous view (remote control click)


There is such a screen (Android TV)

введите сюда описание изображения

I need to implement the following:

  1. When the screen opens, the focus should be on the first view in the left panel (Left Panel: 0).
  2. The user can navigate (with the remote) through the elements in the left panel up and down.
  3. If the user presses right, the focus moves to the first view in the right panel (Right Panel: 0).
  4. The user can move the focus freely between the elements in the right panel.
  5. If the user is in the right panel and presses left, the focus should return to the element in the left panel from which the user moved to the right panel. That is, if the user was on the 3rd element in the left panel and pressed right, when they return from the right panel to the left panel, the focus should move back to the 3rd element where it was before.

Everything works as needed except for the 5th point.

Here is the code:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Test_delete_itTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    shape = RectangleShape
                ) {
                    Greeting()
                }
            }
        }
    }
}

@Composable
fun Greeting() {
    val leftPanelFocusRequester: FocusRequester = remember { FocusRequester() }
    val rightPanelFocusRequester: FocusRequester = remember { FocusRequester() }

    Row(modifier = Modifier
        .fillMaxSize()
    ) {
        LeftPanel(
            focusRequester = leftPanelFocusRequester,
            onRightDirectionClicked = {
                rightPanelFocusRequester.requestFocus()
            }
        )
        RightPanel(focusRequester = rightPanelFocusRequester)
    }
}

@Composable
fun RowScope.LeftPanel(
    focusRequester: FocusRequester,
    onRightDirectionClicked: () -> Unit
) {
    LaunchedEffect(Unit) {
        this.coroutineContext.job.invokeOnCompletion {
            focusRequester.requestFocus()
        }
    }

    Column(
        modifier = Modifier
            .background(Color.Blue.copy(alpha = 0.1f))
            .fillMaxHeight()
            .weight(1f)
            .onKeyEvent {
                if (it.key == Key.DirectionRight) {
                    onRightDirectionClicked()
                    true
                } else {
                    false
                }
            },
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        repeat(5) {
            Button(
                modifier = Modifier
                    .let { modifier ->
                        if (it == 0) {
                            modifier.focusRequester(focusRequester)
                        } else {
                            modifier
                        }
                    },
                onClick = { }
            ) {
                Text(text = "Left Panel: $it")
            }
        }
    }
}

@Composable
fun RowScope.RightPanel(focusRequester: FocusRequester) {
    Column(
        modifier = Modifier
            .background(Color.Green.copy(alpha = 0.1f))
            .fillMaxHeight()
            .weight(1f),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        val buttons by rememberSaveable { mutableStateOf(List(10) { "Button ${it + 1}" }) }

        LazyVerticalGrid(
            columns = GridCells.Fixed(2),
            modifier = Modifier.padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            itemsIndexed(
                items = buttons,
                key = { idx, _ -> idx }
            ) { idx, _ ->
                Button(
                    modifier = Modifier
                        .padding(8.dp)
                        .let {
                            if (idx == 0) {
                                it.focusRequester(focusRequester)
                            } else {
                                it
                            }
                        }
                    ,
                    onClick = { }
                ) {
                    Text(text = "Right Panel: $idx")
                }
            }
        }
    }
}

Here is the translation:


As far as I can imagine, it's necessary to remember the index from which the user moved to the right panel (let's say button 3) and track when the user presses left. When the user presses left, request focus on the saved index (in our example, 3). But I have two questions:

  1. How to distinguish a right click between elements in the right panel (for example, Right Panel 3 -> Right Panel 2 click) from the desired click (for example, Right Panel: 6 -> Left Panel 3)?
  2. It seems to me that such a solution looks too cumbersome, and somehow this should work more simply.

Any ideas are welcome :)


Solution

  • You can make use of the existing focusRestorer modifier to achieve this.

    @Composable
    fun App() {
       Row {
          LeftPanel()
          RightPanel()
       }
    }
    
    @Composable
    fun LeftPanel() {
       val firstItemFr = remember { FocusRequester() }
       LazyColumn(Modifier.focusRestorer { firstItemFr }) {
           item { ListItem(Modifier.focusRequester(firstItemFr)) }
           item { ListItem() }
           item { ListItem() }
           item { ListItem() }
       }
    }
    
    @Composable
    fun RightPanel() {
       val firstItemFr = remember { FocusRequester() }
       LazyColumn(Modifier.focusRestorer { firstItemFr }) {
           item { ListItem(Modifier.focusRequester(firstItemFr)) }
           item { ListItem() }
           item { ListItem() }
           item { ListItem() }
       }
    }
    
    

    LazyColumn can also be replaced with LazyVerticalGrid with 2 columns as you have done in your question. :)

    When we visit an unvisited LazyColumn, since nothing was previously focused, it will move focus to the first item. If visiting a visited column, focusRestorer will move focus to the last focused item from that LazyColumn.