Spec-Zone.ru › Kotlin 2

Навигация с помощью клавиши Tab и фокус клавиатуры

В Compose Multiplatform для настольных компьютеров можно настроить переход между компонентами с помощью сочетания клавиш Tab для перехода к следующему компоненту и Shift+Tab для перехода к предыдущему.

Примеры в этом руководстве представляют собой запускаемые настольные приложения. Чтобы попробовать один из них:

  1. Создайте проект Compose Multiplatform с помощью плагина Kotlin Multiplatform для IDE или онлайн-мастера.

  2. Откройте файл точки входа для настольного приложения. Например, desktopApp/src/main/kotlin/com/example/my_desktop_app/main.kt.

  3. Замените его содержимое приведённым примером.

Навигация с помощью Tab по умолчанию

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

К компонентам, доступным для получения фокуса, относятся компоненты, в реализации которых используются модификаторы clickable(), selectable(), toggleable() или focusable(). Например, текстовые поля, кнопки, ползунки, элементы навигации, элементы управления выбором с callback, отличным от null, перегрузки Card() с onClick, Surface() и ListItem().

Ниже показано окно, в котором пользователь может перемещаться между пятью текстовыми полями с помощью стандартных сочетаний клавиш:

import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.TextField import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application fun main() = application { Window( onCloseRequest = ::exitApplication, state = WindowState(size = DpSize(350.dp, 500.dp)) ) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( modifier = Modifier.padding(50.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { repeat(5) { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) } } } } }
Default tab order

Пользовательские компоненты, доступные для получения фокуса

Чтобы включить в порядок навигации с помощью Tab компонент, который по умолчанию не может получить фокус, примените модификатор focusable().

Чтобы изменить внешний вид компонента при получении фокуса, передайте MutableInteractionSource модификатору focusable(), считывайте состояние фокуса с его помощью через collectIsFocusedAsState() и используйте это состояние для изменения стиля компонента: задайте другой фон, границу или любой другой способ выделения. Чтобы компонент реагировал на нажатия клавиш, обрабатывайте события клавиатуры с помощью модификатора onKeyEvent().

В следующем примере компонуемая функция Box() превращается в компонент, похожий на кнопку. Рамка выделяется при получении фокуса, а нажатие Enter или Space вызывает соответствующее действие:

import androidx.compose.foundation.background import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application fun main() = application { Window( onCloseRequest = ::exitApplication, state = WindowState(size = DpSize(350.dp, 450.dp)) ) { MaterialTheme( colorScheme = MaterialTheme.colorScheme.copy( primary = Color(10, 132, 232), secondary = Color(150, 232, 150) ) ) { var clicks by remember { mutableStateOf(0) } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( modifier = Modifier.padding(40.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { Text(text = "Clicks: $clicks") repeat(5) { index -> FocusableBox("Button ${index + 1}", onClick = { clicks++ }) } } } } } } @OptIn(ExperimentalComposeUiApi::class) @Composable fun FocusableBox( text: String = "", onClick: () -> Unit = {}, size: DpSize = DpSize(200.dp, 35.dp) ) { var isKeyPressed by remember { mutableStateOf(false) } val interactionSource = remember { MutableInteractionSource() } val isFocused by interactionSource.collectIsFocusedAsState() val backgroundColor = when { isFocused && isKeyPressed -> lerp(MaterialTheme.colorScheme.secondary, Color(64, 64, 64), 0.3f) isFocused -> MaterialTheme.colorScheme.secondary else -> MaterialTheme.colorScheme.primary } Box( modifier = Modifier .clip(RoundedCornerShape(4.dp)) .background(backgroundColor) .size(size) .onPointerEvent(PointerEventType.Press) { onClick() } .onKeyEvent { if (it.key == Key.Enter || it.key == Key.Spacebar) { when (it.type) { KeyEventType.KeyDown -> isKeyPressed = true KeyEventType.KeyUp -> { isKeyPressed = false onClick() } } } false } .focusable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text(text = text, color = Color.White) } }
A custom focusable component

Настройка порядка навигации с помощью Tab

Чтобы перемещать фокус не в порядке отображения компонентов, объедините два модификатора:

  • focusRequester() прикрепляет дескриптор FocusRequester к компоненту, доступному для получения фокуса. Если компонент по умолчанию не может получить фокус, примените модификатор focusable() после focusRequester().

  • focusProperties() задаёт элементы next и previous в порядке навигации с помощью Tab: компоненты с дескриптором FocusRequester, на которые можно перевести фокус нажатием Tab или Shift+Tab.

В следующем примере для каждого из пяти текстовых полей создаётся FocusRequester, а порядок навигации с помощью Tab по умолчанию меняется на обратный:

import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.TextField import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application fun main() = application { Window( onCloseRequest = ::exitApplication, state = WindowState(size = DpSize(350.dp, 500.dp)) ) { val focusRequesters = remember { List(5) { FocusRequester() } } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( modifier = Modifier.padding(50.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { focusRequesters.forEachIndexed { index, focusRequester -> TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine, modifier = Modifier .focusRequester(focusRequester) .focusProperties { // Reverses the default order: next = focusRequesters[(index - 1 + focusRequesters.size) % focusRequesters.size] previous = focusRequesters[(index + 1) % focusRequesters.size] } ) } } } } }
Custom tab order

Перемещение фокуса из кода

Чтобы перевести компонент в фокус без взаимодействия с пользователем, прикрепите FocusRequester к компоненту, доступному для получения фокуса, с помощью модификатора focusRequester() и вызовите FocusRequester.requestFocus(). Если компонент по умолчанию не может получить фокус, модификатор focusable() следует применить после focusRequester().

В следующем примере кнопка перемещает фокус на текстовое поле, а затем обратно на себя:

import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application fun main() = application { Window( onCloseRequest = ::exitApplication, state = WindowState(size = DpSize(350.dp, 450.dp)) ) { val buttonFocusRequester = remember { FocusRequester() } val textFieldFocusRequester = remember { FocusRequester() } var isTextFieldFocused by remember { mutableStateOf(false) } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( modifier = Modifier.padding(50.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { Button( onClick = { isTextFieldFocused = !isTextFieldFocused if (isTextFieldFocused) { textFieldFocusRequester.requestFocus() } else { buttonFocusRequester.requestFocus() } }, modifier = Modifier .fillMaxWidth() .focusRequester(buttonFocusRequester) ) { Text(text = "Focus switcher") } TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine, modifier = Modifier.focusRequester(textFieldFocusRequester) ) } } } }
Moving focus from code

Установка фокуса на компонент при его появлении

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

В следующем примере фокус устанавливается на первое текстовое поле сразу после открытия окна:

import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.TextField import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application fun main() = application { Window( onCloseRequest = ::exitApplication, state = WindowState(size = DpSize(350.dp, 300.dp)) ) { val focusRequester = remember { FocusRequester() } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( modifier = Modifier.padding(50.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine, modifier = Modifier.focusRequester(focusRequester) ) TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) } } LaunchedEffect(Unit) { focusRequester.requestFocus() } } }
Focus text field on appearance

Перемещение фокуса из многострочных текстовых полей

В многострочных текстовых полях нажатие Tab вставляет символ табуляции вместо перемещения фокуса на следующий компонент:

Column {
    repeat(5) {
        TextField(
            state = rememberTextFieldState("Hello, World!"),
            // MultiLine is the default value of lineLimits
            lineLimits = TextFieldLineLimits.MultiLine(),
            modifier = Modifier.padding(8.dp)
        )
    }
}

Это известная проблема: CMP-5822. Она затрагивает все текстовые поля, в которых можно вводить несколько строк, что является поведением по умолчанию. В качестве обходного решения перехватывайте клавишу Tab с помощью модификатора onPreviewKeyEvent и перемещайте фокус, используя FocusManager из LocalFocusManager:

import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.isShiftPressed import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication fun main() = singleWindowApplication(title = "Multiline text fields") { Column { repeat(5) { TextField( state = rememberTextFieldState("Hello, World!"), lineLimits = TextFieldLineLimits.MultiLine(), modifier = Modifier.padding(8.dp).moveFocusOnTab() ) } } } @Composable fun Modifier.moveFocusOnTab(): Modifier { val focusManager = LocalFocusManager.current return onPreviewKeyEvent { if (it.type == KeyEventType.KeyDown && it.key == Key.Tab) { focusManager.moveFocus( if (it.isShiftPressed) FocusDirection.Previous else FocusDirection.Next ) true } else { false } } }

Что дальше

  • Подробнее об обработке событий клавиатуры.

  • Узнайте о поддержке специальных возможностей на разных платформах.

  • Изучите учебные материалы о других компонентах для настольных приложений.

31 августа 2026 г.
События клавиатурыСобственные дистрибутивы

© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/multiplatform/compose-desktop-tabbing.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API