Изменение поведения фокуса

Иногда необходимо переопределить поведение фокуса по умолчанию для элементов на экране. Например, вы можете захотеть сгруппировать составные объекты , запретить фокусировку на определенном составном объекте, явно запросить фокусировку на одном из них, захватить или отпустить фокус или перенаправить фокус на вход или выход. В этом разделе описывается, как изменить поведение фокуса, если значения по умолчанию не соответствуют вашим потребностям.

Обеспечьте последовательную навигацию с фокус-группами

Иногда Jetpack Compose не сразу угадывает правильный следующий элемент для навигации с вкладками, особенно когда в игру вступают сложные родительские Composables , такие как вкладки и списки.

Хотя поиск фокуса обычно следует порядку объявления Composables , в некоторых случаях это невозможно, например, когда один из Composables в иерархии представляет собой горизонтальную прокрутку, которая не полностью видна. Это показано в примере ниже.

Jetpack Compose может решить сосредоточить внимание на следующем элементе, ближайшем к началу экрана, как показано ниже, вместо того, чтобы продолжать путь, который вы ожидаете для однонаправленной навигации:

Анимация приложения, показывающая верхнюю горизонтальную навигацию и список элементов внизу.
Рисунок 1 . Анимация приложения, показывающая верхнюю горизонтальную навигацию и список элементов внизу.

В этом примере видно, что разработчики не планировали, чтобы фокус перескакивал с вкладки «Шоколад» на первое изображение ниже, а затем обратно на вкладку «Выпечка» . Вместо этого они хотели, чтобы фокус продолжался на вкладках до последней вкладки, а затем фокусировался на внутреннем содержимом:

Анимация приложения, показывающая верхнюю горизонтальную навигацию и список элементов внизу.
Рисунок 2 . Анимация приложения, показывающая верхнюю горизонтальную навигацию и список элементов внизу.

В ситуациях, когда важно, чтобы группа компонуемых объектов получала фокус последовательно, как в строке Tab из предыдущего примера, вам необходимо обернуть Composable в родительский элемент, который имеет модификатор focusGroup() :

LazyVerticalGrid(columns = GridCells.Fixed(4)) {
    item(span = { GridItemSpan(maxLineSpan) }) {
        Row(modifier = Modifier.focusGroup()) {
            FilterChipA()
            FilterChipB()
            FilterChipC()
        }
    }
    items(chocolates) {
        SweetsCard(sweets = it)
    }
}

Двунаправленная навигация ищет ближайший компонуемый объект для данного направления — если элемент из другой группы находится ближе, чем не полностью видимый элемент в текущей группе, навигация выбирает ближайший. Чтобы избежать такого поведения, вы можете применить модификатор focusGroup() .

FocusGroup заставляет всю группу выглядеть как единое целое с точки зрения фокуса, но сама группа не получит фокус — вместо этого фокус получит ближайший ребенок. Таким образом, навигация знает, что перед выходом из группы необходимо перейти к не полностью видимому элементу.

В этом случае три экземпляра FilterChip будут сфокусированы перед элементами SweetsCard , даже если SweetsCards полностью видны пользователю и некоторые FilterChip могут быть скрыты. Это происходит потому, что модификатор focusGroup сообщает менеджеру фокуса изменить порядок, в котором элементы фокусируются, чтобы навигация была проще и более согласованной с пользовательским интерфейсом.

Без модификатора focusGroup , если FilterChipC не был виден, навигация по фокусу обнаружила бы его последним. Однако добавление такого модификатора делает его не только доступным для обнаружения, но и, как и ожидают пользователи, он также получит фокус сразу после FilterChipB .

Создание составного фокусируемого объекта

Некоторые составные элементы являются фокусируемыми по своей конструкции, например кнопка или составной объект с прикрепленным к нему clickable модификатором. Если вы хотите специально добавить фокусируемое поведение к составному объекту, вы используете модификатор focusable :

var color by remember { mutableStateOf(Green) }
Box(
    Modifier
        .background(color)
        .onFocusChanged { color = if (it.isFocused) Blue else Green }
        .focusable()
) {
    Text("Focusable 1")
}

Создание составного нефокусируемого объекта

Могут возникнуть ситуации, в которых некоторые из ваших элементов не должны участвовать в фокусе. В таких редких случаях вы можете использовать canFocus property чтобы исключить возможность фокусировки Composable .

var checked by remember { mutableStateOf(false) }

Switch(
    checked = checked,
    onCheckedChange = { checked = it },
    // Prevent component from being focused
    modifier = Modifier
        .focusProperties { canFocus = false }
)

Запросить фокус клавиатуры с помощью FocusRequester

В некоторых случаях вам может потребоваться явно запросить фокус в ответ на взаимодействие с пользователем. Например, вы можете спросить пользователя, хочет ли он возобновить заполнение формы, и если он нажмет «да», вы захотите перефокусировать первое поле этой формы.

Первое, что нужно сделать, — это связать объект FocusRequester с составным объектом, на который вы хотите переместить фокус клавиатуры. В следующем фрагменте кода объект FocusRequester связан с TextField путем установки модификатора Modifier.focusRequester :

val focusRequester = remember { FocusRequester() }
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.focusRequester(focusRequester)
)

Вы можете вызвать метод requestFocus FocusRequester для отправки фактических запросов на фокусировку. Вы должны вызывать этот метод вне контекста Composable (в противном случае он выполняется повторно при каждой рекомпозиции). В следующем фрагменте показано, как запросить систему переместить фокус клавиатуры при нажатии кнопки:

val focusRequester = remember { FocusRequester() }
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.focusRequester(focusRequester)
)

Button(onClick = { focusRequester.requestFocus() }) {
    Text("Request focus on TextField")
}

Захват и освобождение фокуса

Вы можете использовать фокус, чтобы помочь пользователям предоставить нужные данные, необходимые вашему приложению для выполнения своей задачи, например, получение действующего адреса электронной почты или номера телефона. Хотя состояния ошибок информируют ваших пользователей о том, что происходит, вам может понадобиться поле с ошибочной информацией, чтобы оставаться в центре внимания, пока она не будет исправлена.

Чтобы захватить фокус, вы можете вызвать метод captureFocus() , а затем освободить его с помощью метода freeFocus() , как в следующем примере:

val textField = FocusRequester()

TextField(
    value = text,
    onValueChange = {
        text = it

        if (it.length > 3) {
            textField.captureFocus()
        } else {
            textField.freeFocus()
        }
    },
    modifier = Modifier.focusRequester(textField)
)

Приоритет модификаторов фокуса

Modifiers можно рассматривать как элементы, имеющие только один дочерний элемент, поэтому, когда вы ставите их в очередь, каждый Modifier слева (или сверху) оборачивает Modifier , следующий за ним справа (или ниже). Это означает, что второй Modifier содержится внутри первого, так что при объявлении двух focusProperties работает только самый верхний, так как следующие содержатся в самом верхнем.

Чтобы уточнить концепцию, посмотрите следующий код:

Modifier
    .focusProperties { right = item1 }
    .focusProperties { right = item2 }
    .focusable()

В этом случае focusProperties , указывающий item2 как правый фокус, не будет использоваться, так как он содержится в предыдущем; таким образом, будет использоваться item1 .

Используя этот подход, родитель также может сбросить поведение по умолчанию, используя FocusRequester.Default :

Modifier
    .focusProperties { right = Default }
    .focusProperties { right = item1 }
    .focusProperties { right = item2 }
    .focusable()

Родитель не обязательно должен быть частью той же цепочки модификаторов. Родительский составной объект может перезаписать свойство фокуса дочернего составного объекта. Например, рассмотрим FancyButton , который делает кнопку не фокусируемой:

@Composable
fun FancyButton(modifier: Modifier = Modifier) {
    Row(modifier.focusProperties { canFocus = false }) {
        Text("Click me")
        Button(onClick = { }) { Text("OK") }
    }
}

Пользователь может снова сделать эту кнопку фокусируемой, установив для canFocus значение true :

FancyButton(Modifier.focusProperties { canFocus = true })

Как и любой Modifier , связанные с фокусом, они ведут себя по-разному в зависимости от порядка, в котором вы их объявляете. Например, код, подобный следующему, делает Box доступным для фокусировки, но FocusRequester не связан с этим объектом фокусировки, поскольку он объявлен после объекта фокусировки.

Box(
    Modifier
        .focusable()
        .focusRequester(Default)
        .onFocusChanged {}
)

Важно помнить, что focusRequester связан с первым фокусируемым элементом в иерархии, поэтому этот focusRequester указывает на первый фокусируемый дочерний элемент. Если ничего недоступно, это ни на что не укажет. Однако, поскольку Box является фокусируемым (благодаря модификатору focusable() ), вы можете перейти к нему с помощью двунаправленной навигации.

В качестве другого примера подойдет любой из следующих вариантов, поскольку модификатор onFocusChanged() ссылается на первый фокусируемый элемент, который появляется после модификаторов focusable() или focusTarget() .

Box(
    Modifier
        .onFocusChanged {}
        .focusRequester(Default)
        .focusable()
)
Box(
    Modifier
        .focusRequester(Default)
        .onFocusChanged {}
        .focusable()
)

Перенаправить фокус при входе или выходе

Иногда вам необходимо предоставить очень специфический вид навигации, как показано на анимации ниже:

Анимация экрана, на которой показаны два столбца кнопок, расположенных рядом, и анимация перехода от одного столбца к другому.
Рисунок 3 . Анимация экрана, показывающая два столбца кнопок, расположенных рядом, и анимация фокуса от одного столбца к другому.

Прежде чем мы углубимся в то, как это создать, важно понять поведение поиска фокуса по умолчанию. Без каких-либо изменений, как только поиск фокуса достигнет элемента Clickable 3 , нажатие DOWN на D-Pad (или эквивалентной клавише со стрелкой) переместит фокус на все, что отображается под Column , выходя из группы и игнорируя тот, что справа. . Если доступных для фокусировки элементов нет, фокус никуда не перемещается, а остается на Clickable 3 .

Чтобы изменить это поведение и обеспечить нужную навигацию, вы можете использовать модификатор focusProperties , который помогает вам управлять тем, что происходит, когда поиск фокуса входит или выходит из Composable :

val otherComposable = remember { FocusRequester() }

Modifier.focusProperties {
    exit = { focusDirection ->
        when (focusDirection) {
            Right -> Cancel
            Down -> otherComposable
            else -> Default
        }
    }
}

Можно направить фокус на конкретный Composable всякий раз, когда он входит в определенную часть иерархии или выходит из нее — например, когда в вашем пользовательском интерфейсе есть два столбца, и вы хотите быть уверены, что всякий раз, когда обрабатывается первый из них, фокус переключается на второй:

Анимация экрана, на которой показаны два столбца кнопок, расположенных рядом, и анимация перехода от одного столбца к другому.
Рисунок 4 . Анимация экрана, показывающая два столбца кнопок, расположенных рядом, и анимация фокуса от одного столбца к другому.

На этой гифке, как только фокус достигает « Clickable 3 Composable в Column 1», следующим элементом, на котором фокусируется, является Clickable 4 в другом Column . Этого поведения можно добиться, объединив focusDirection со значениями enter и exit внутри модификатора focusProperties . Им обоим нужна лямбда-выражение, которое принимает в качестве параметра направление, откуда исходит фокус, и возвращает FocusRequester . Эта лямбда-выражение может вести себя тремя разными способами: возврат FocusRequester.Cancel останавливает продолжение фокуса, а FocusRequester.Default не меняет его поведение. Если вместо этого предоставить FocusRequester прикрепленный к другому Composable фокус перейдет на этот конкретный Composable .

Изменить направление продвижения фокуса

Чтобы переместить фокус на следующий элемент или в точном направлении, вы можете использовать модификатор onPreviewKey и использовать LocalFocusManager для перемещения фокуса с помощью модификатора moveFocus .

В следующем примере показано поведение механизма фокуса по умолчанию: при обнаружении нажатия клавиши tab фокус перемещается на следующий элемент в списке фокуса. Хотя обычно это не то, что вам нужно настраивать, важно знать внутреннюю работу системы, чтобы иметь возможность изменить поведение по умолчанию.

val focusManager = LocalFocusManager.current
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.onPreviewKeyEvent {
        when {
            KeyEventType.KeyUp == it.type && Key.Tab == it.key -> {
                focusManager.moveFocus(FocusDirection.Next)
                true
            }

            else -> false
        }
    }
)

В этом примере функция focusManager.moveFocus() перемещает фокус на указанный элемент или в направлении, указанном в параметре функции.

{% дословно %} {% дословно %} {% дословно %} {% дословно %}