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