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

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

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

Иногда 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() перемещает фокус на указанный элемент или в направлении, указанном в параметре функции.

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