hierarchicalFocusGroup

Functions summary

Modifier

hierarchicalFocusGroup is used to annotate composables in an application, so we can keep track of what is the active part of the composition.

Functions

Modifier.hierarchicalFocusGroup

fun Modifier.hierarchicalFocusGroup(active: Boolean): Modifier

hierarchicalFocusGroup is used to annotate composables in an application, so we can keep track of what is the active part of the composition. In turn, this is used to coordinate focus in a declarative way, requesting focus when needed, as the user navigates through the app (such as between screens or between pages within a screen). In most cases, this is automatically handled by Wear Compose components and no action is necessary. In particular this is done by BasicSwipeToDismissBox, HorizontalPager, VerticalPager and PickerGroup. This modifier is useful if you implement a custom component that needs to direct focus to one of several children, like a custom Pager, a Tabbed layout, etc.

hierarchicalFocusGroups can be nested to form a focus tree, with an implicit root. For sibling hierarchicalFocusGroups, only one should have active = true. Within the focus tree, components that need to request focus can do so using Modifier.requestFocusOnHierarchyActive. Note that ScalingLazyColumn and TransformingLazyColumn are using it already, so there is no need to add it explicitly.

When focus changes, the focus tree is examined and the topmost (closest to the root of the tree) requestFocusOnHierarchyActive which has all its hierarchicalFocusGroup ancestors with active = true will request focus. If no such requestFocusOnHierarchyActive exists, the focus will be cleared.

NOTE: This shouldn't be used together with FocusRequester.requestFocus calls in LaunchedEffect.

Example usage:

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.wear.compose.foundation.hierarchicalFocusGroup
import androidx.wear.compose.foundation.requestFocusOnHierarchyActive

var selected by remember { mutableIntStateOf(0) }

Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
    repeat(5) { colIx ->
        Box(
            Modifier.hierarchicalFocusGroup(active = selected == colIx)
                .weight(1f)
                .clickable { selected = colIx }
                .then(
                    if (selected == colIx) {
                        Modifier.border(BorderStroke(2.dp, Color.Red))
                    } else {
                        Modifier
                    }
                )
        ) {
            // This is used a Gray background to the currently focused item, as seen by the
            // focus system.
            var focused by remember { mutableStateOf(false) }

            BasicText(
                "$colIx",
                style =
                    TextStyle(
                        color = Color.White,
                        fontSize = 20.sp,
                        textAlign = TextAlign.Center,
                    ),
                modifier =
                    Modifier.fillMaxWidth()
                        .requestFocusOnHierarchyActive()
                        .onFocusChanged { focused = it.isFocused }
                        .focusable()
                        .then(
                            if (focused) {
                                Modifier.background(Color.Gray)
                            } else {
                                Modifier
                            }
                        ),
            )
        }
    }
}

Sample using nested hierarchicalFocusGroup:

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
import androidx.wear.compose.foundation.hierarchicalFocusGroup
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.requestFocusOnHierarchyActive

Column(Modifier.fillMaxSize()) {
    var selectedRow by remember { mutableIntStateOf(0) }
    repeat(2) { rowIx ->
        Row(
            Modifier.weight(1f)
                .fillMaxWidth()
                .hierarchicalFocusGroup(active = selectedRow == rowIx)
        ) {
            var selectedItem by remember { mutableIntStateOf(0) }
            repeat(2) { itemIx ->
                Box(
                    Modifier.weight(1f).hierarchicalFocusGroup(active = selectedItem == itemIx)
                ) {
                    // ScalingLazyColumn uses requestFocusOnHierarchyActive internally
                    ScalingLazyColumn(
                        Modifier.fillMaxWidth().clickable {
                            selectedRow = rowIx
                            selectedItem = itemIx
                        }
                    ) {
                        val prefix = (rowIx * 2 + itemIx + 'A'.code).toChar()
                        items(20) {
                            BasicText(
                                "$prefix $it",
                                style =
                                    TextStyle(
                                        color = Color.White,
                                        fontSize = 20.sp,
                                        textAlign = TextAlign.Center,
                                    ),
                            )
                        }
                    }
                }
            }
        }
    }
}
Parameters
active: Boolean

Pass true when this sub tree of the focus tree is active and may require the focus - otherwise, pass false. For example, a pager can apply this modifier to each page's content with a call to hierarchicalFocusGroup, marking only the current page as active.