Вступление
Привет! Часто при реализации различных фич нам необходимо использовать поля ввода данных. Банальный пример — экран регистрации, там обычно много различных полей, есть какие-нибудь чекбоксы и радиокнопки. Такие экраны, как правило содержат много элементов, и важно организовать комфортные условия для пользователя, когда он будет заполнять эти поля данными.
Типичные проблемы, с которыми мы можем столкнуться:
- Контент уезжает наверх вместе с TopBar’ом при открытой клавиатуре.
- Клавиатура перекрывает контент при клике на поле ввода (особенно часто наблюдается на маленьких экранах).
- Нет автоматического подскролла к полю ввода.
- Неправильные отступы при открытой клавиатуре.
В этой статье мы решим все эти проблемы и сделаем пользователя счастливым.
Пример #1. Добавление отступов
Создадим пример Compose функции, которая будет имитировать контент с полями регистрации:
@Composable
private fun RegistrationContent(
modifier: Modifier
) {
val states = remember {
mutableStateListOf("", "", "", "", "", "", "", "", "")
}
val labels = listOf(
"Имя", "Отчество", "Фамилия",
"Логин", "Почта", "Адрес", "Дата рождения",
"Пароль", "Повтор пароля"
)
LazyColumn(
state = rememberLazyListState(),
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
itemsIndexed(states) { i, _ ->
TextField(value = states[i],
modifier = Modifier.padding(top = 16.dp),
onValueChange = {
states[i] = it
},
label = {
Text(labels[i])
})
}
}
}
При запуске сразу видим проблемы с отступами:
Они решаются путём добавления одного модификатора к нашему контенту: systemBarsPadding()
, будет сразу применён отступ и к status bar’у и к navigation bar’у. В месте вызова это функции добавляем модификатор:
setContent {
MyApplicationTheme {
RegistrationContent(
modifier = Modifier.systemBarsPadding()
)
}
}
Теперь всё работает хорошо, даже правильный подскролл к полям при фокусе и клавиатура ничего не перекрывает. Но представим, что по дизайну у нас есть TopBar
.
Пример #2. Добавление TopBar
Добавляем TopAppBar
в код с помощью Scaffold
:
Scaffold(
modifier = Modifier.systemBarsPadding(),
topBar = {
TopAppBar(
title = { Text("Регистрация") },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Gray
)
)
},
content = { innerPadding ->
RegistrationContent(
modifier = Modifier.padding(innerPadding)
)
}
)
Заметьте, что теперь модификатор systemBarsPadding()
применяется к Scaffold
, но проблема с отступами всё-равно будет, чтобы этого избежать, нужно применить к нашему контенту внутренние отступы: Modifier.padding(innerPadding)
, теперь всё хорошо отображается:
Для информацииПодробнее про
Scaffold
можно почитать тут
Но давайте посмотрим, как отображается контент при фокусе на элемент?
Как видим при фокусе TopAppBar
уходит наверх, если места для клавиатуры не хватает. Чтобы избежать такого поведения, необходимо в манифест для Activity
добавить атрибут android:windowSoftInputMode="adjustResize"
:
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.MyApplication"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Для информацииПодробнее про
android:windowSoftInputMode="adjustResize"
можно почитать тут
Теперь контент не уходит наверх, если клавиатуре не хватает места. Но появилась новая проблема, при фокусе на элемент, клавиатура перекрывает поле ввода и не происходит автоматического подскролла к полю. Для самого нижнего элемента это очень критично, потому что даже подскроллить в ручную нельзя, чтобы увидеть вводимый текст:
Чтобы решить эту проблему, есть тоже очень простое решение, нужно применить к нашему Scaffold
модификатор .imePadding()
Scaffold(
modifier = Modifier
.imePadding()
.systemBarsPadding(),
topBar = {
//код без изменений
},
content = { innerPadding ->
//код без изменений
}
)
Для информацииПодробнее про инсеты можно почитать тут
Теперь всё работает хорошо:
Пример #3. Добавление BottomBar
Следующий кейс, когда у нас еще есть BottomBar
, при его добавлении, всё работает хорошо:
Scaffold(
modifier = Modifier
.imePadding()
.systemBarsPadding(),
topBar = {
//код без изменений
},
content = { innerPadding ->
//код без изменений
},
bottomBar = {
Box(
modifier = Modifier
.background(Color.Gray)
.padding(vertical = 8.dp)
.fillMaxWidth()
) {
Button(
modifier = Modifier.align(Alignment.Center),
onClick = { }
) { Text("Далее") }
}
}
)
Но возможно вас(или вашего дизайнера) не устраивает, что кнопка внизу экрана прилипает к низу клавиатуры и есть задача, убрать эту функциональность. Данную проблему можно решить с помощью модификатора .consumeWindowInsets()
, передав в качестве аргумента, размер нижнего отступа у внутреннего паддинга innerPadding.calculateBottomPadding()
, а так же переместив моификатор .imePadding()
из Scaffold
непосредственно в контент RegistrationContent
Scaffold(
modifier = Modifier
.systemBarsPadding(),
topBar = {
//код без изменений
},
content = { innerPadding ->
RegistrationContent(
modifier = Modifier
.consumeWindowInsets(
PaddingValues(
bottom = innerPadding.calculateBottomPadding()
)
)
.imePadding()
.padding(innerPadding)
)
},
bottomBar = {
//код без изменений
}
)
Автофокус при старте приложения
В Androd 8.1(API 27) и ниже, когда Activity запускается, система попытается передать фокус первому элементу внутри рутового контейнера, поэтому если запустить наше приложение, то при старте автоматически будет фокус на первом поле ввода:
Такое поведение, не всегда нужно, тем более начиная с Android 9(API 28) и выше, автофокуса - нет. Чтобы избавиться от такого поведения, можно использовать модификатор .focusable()
для нашего корневого контейнера, т.е для Scaffold
:
Scaffold(
modifier = Modifier
.focusable()
.systemBarsPadding(),
topBar = {
//код без изменений
},
content = { innerPadding ->
//код без изменений
},
bottomBar = {
//код без изменений
}
)
Результат:
Заключение
Статья подходит к концу. Надеюсь, вам было интересно почитать про особенности реализации удобного UX, стало понятнее как работать с полями ввода, клавиатурой и подскроллом на Android с использованием фреймворка Compose. На первый взгляд всё неочевидно, и нужно понимать, какие модификаторы, где и в какой последовательности использовать. Но в целом, по моему опыту работы с XML, в Compose данная функциональность реализуется быстрее и легче — главное, знать как. Спасибо, что были со мной! С кодом вы также можете ознакомиться на моём GitHub.