Иногда необходимо переопределить поведение фокуса по умолчанию для элементов на экране. Например, вы можете захотеть сгруппировать составные объекты , запретить фокусировку на определенном составном объекте, явно запросить фокусировку на одном из них, захватить или отпустить фокус или перенаправить фокус на вход или выход. В этом разделе описывается, как изменить поведение фокуса, если значения по умолчанию не соответствуют вашим потребностям.
Обеспечьте последовательную навигацию с фокус-группами
Иногда Jetpack Compose не сразу угадывает правильный следующий элемент для навигации с вкладками, особенно когда в игру вступают сложные родительские Composables
, такие как вкладки и списки.
Хотя поиск фокуса обычно следует порядку объявления Composables
, в некоторых случаях это невозможно, например, когда один из Composables
в иерархии представляет собой горизонтальную прокрутку, которая не полностью видна. Это показано в примере ниже.
Jetpack Compose может решить сосредоточить внимание на следующем элементе, ближайшем к началу экрана, как показано ниже, вместо того, чтобы продолжать путь, который вы ожидаете для однонаправленной навигации:
![Анимация приложения, показывающая верхнюю горизонтальную навигацию и список элементов внизу.](https://developer.android.com/static/develop/ui/compose/images/touchinput/one_directional_navigation_1.gif?authuser=7&hl=ru)
В этом примере видно, что разработчики не планировали, чтобы фокус перескакивал с вкладки «Шоколад» на первое изображение ниже, а затем обратно на вкладку «Выпечка» . Вместо этого они хотели, чтобы фокус продолжался на вкладках до последней вкладки, а затем фокусировался на внутреннем содержимом:
![Анимация приложения, показывающая верхнюю горизонтальную навигацию и список элементов внизу.](https://developer.android.com/static/develop/ui/compose/images/touchinput/one_directional_navigation_2.gif?authuser=7&hl=ru)
В ситуациях, когда важно, чтобы группа компонуемых объектов получала фокус последовательно, как в строке 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() ) |
Перенаправить фокус при входе или выходе
Иногда вам необходимо предоставить очень специфический вид навигации, как показано на анимации ниже:
![Анимация экрана, на которой показаны два столбца кнопок, расположенных рядом, и анимация перехода от одного столбца к другому.](https://developer.android.com/static/develop/ui/compose/images/touchinput/two_dimensional_navigation_1.gif?authuser=7&hl=ru)
Прежде чем мы углубимся в то, как это создать, важно понять поведение поиска фокуса по умолчанию. Без каких-либо изменений, как только поиск фокуса достигнет элемента 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
всякий раз, когда он входит в определенную часть иерархии или выходит из нее — например, когда в вашем пользовательском интерфейсе есть два столбца, и вы хотите быть уверены, что всякий раз, когда обрабатывается первый из них, фокус переключается на второй:
![Анимация экрана, на которой показаны два столбца кнопок, расположенных рядом, и анимация перехода от одного столбца к другому.](https://developer.android.com/static/develop/ui/compose/images/touchinput/two_dimensional_navigation_2.gif?authuser=7&hl=ru)
На этой гифке, как только фокус достигает « 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()
перемещает фокус на указанный элемент или в направлении, указанном в параметре функции.
Рекомендуется для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Реагировать на фокусировку
- Фокус в создании
- Изменить порядок обхода фокуса