shalkoff.ru
1035 слова
5 минуты
Автоподскролл к полю ввода при фокусе (Android Compose)

Вступление#

Привет! Часто при реализации различных фич нам необходимо использовать поля ввода данных. Банальный пример — экран регистрации, там обычно много различных полей, есть какие-нибудь чекбоксы и радиокнопки. Такие экраны, как правило содержат много элементов, и важно организовать комфортные условия для пользователя, когда он будет заполнять эти поля данными.

Типичные проблемы, с которыми мы можем столкнуться:

  1. Контент уезжает наверх вместе с TopBar’ом при открытой клавиатуре.
  2. Клавиатура перекрывает контент при клике на поле ввода (особенно часто наблюдается на маленьких экранах).
  3. Нет автоматического подскролла к полю ввода.
  4. Неправильные отступы при открытой клавиатуре.

В этой статье мы решим все эти проблемы и сделаем пользователя счастливым.

Пример #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])
                })
        }
    }
}

При запуске сразу видим проблемы с отступами:

Проблему с отступами у status bar'а и navigation bar'а
Картинка 1. Проблему с отступами у status bar'а и navigation bar'а

Они решаются путём добавления одного модификатора к нашему контенту: 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), теперь всё хорошо отображается:

Добавили TopAppBar
Картинка 2. Добавили TopAppBar
Для информации

Подробнее про Scaffold можно почитать тут

Но давайте посмотрим, как отображается контент при фокусе на элемент?

TopAppBar уходит наверх при открытии клавиатуры
Картинка 3. TopAppBar уходит наверх при открытии клавиатуры

Как видим при фокусе 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" можно почитать тут

Теперь контент не уходит наверх, если клавиатуре не хватает места. Но появилась новая проблема, при фокусе на элемент, клавиатура перекрывает поле ввода и не происходит автоматического подскролла к полю. Для самого нижнего элемента это очень критично, потому что даже подскроллить в ручную нельзя, чтобы увидеть вводимый текст:

Клавиатура перекрывает поля ввода
Картинка 4. Клавиатура перекрывает поля ввода

Чтобы решить эту проблему, есть тоже очень простое решение, нужно применить к нашему Scaffold модификатор .imePadding()

Scaffold(
    modifier = Modifier
        .imePadding()
        .systemBarsPadding(),
    topBar = {
        //код без изменений
    },
    content = { innerPadding ->
        //код без изменений
    }
)
Для информации

Подробнее про инсеты можно почитать тут

Теперь всё работает хорошо:

Фикс проблемы, когда клавиатура перекрывает поля ввода
Картинка 5. Фикс проблемы, когда клавиатура перекрывает поля ввода

Пример #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("Далее") }
        }
    }
)
К клавиатуре прилипает BottomBar
Картинка 6. К клавиатуре прилипает BottomBar

Но возможно вас(или вашего дизайнера) не устраивает, что кнопка внизу экрана прилипает к низу клавиатуры и есть задача, убрать эту функциональность. Данную проблему можно решить с помощью модификатора .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 = {
        //код без изменений
    }
)
Фикс проблемы, когда к клавиатуре прилипает BottomBar
Картинка 7. Фикс проблемы, когда к клавиатуре прилипает BottomBar

Автофокус при старте приложения#

В Androd 8.1(API 27) и ниже, когда Activity запускается, система попытается передать фокус первому элементу внутри рутового контейнера, поэтому если запустить наше приложение, то при старте автоматически будет фокус на первом поле ввода:

Автофокус на первом элементе на Android 8.1(API 27) и ниже
Картинка 8. Автофокус на первом элементе на Android 8.1(API 27) и ниже

Такое поведение, не всегда нужно, тем более начиная с Android 9(API 28) и выше, автофокуса - нет. Чтобы избавиться от такого поведения, можно использовать модификатор .focusable() для нашего корневого контейнера, т.е для Scaffold:

Scaffold(
    modifier = Modifier
        .focusable()
        .systemBarsPadding(),
    topBar = {
        //код без изменений
    },
    content = { innerPadding ->
        //код без изменений
    },
    bottomBar = {
        //код без изменений 
    }
)

Результат:

Фикс проблемы с автофокусом на первом элементе на Android 8.1(API 27) и ниже
Картинка 9. Фикс проблемы с автофокусом на первом элементе на Android 8.1(API 27) и ниже

Заключение#

Статья подходит к концу. Надеюсь, вам было интересно почитать про особенности реализации удобного UX, стало понятнее как работать с полями ввода, клавиатурой и подскроллом на Android с использованием фреймворка Compose. На первый взгляд всё неочевидно, и нужно понимать, какие модификаторы, где и в какой последовательности использовать. Но в целом, по моему опыту работы с XML, в Compose данная функциональность реализуется быстрее и легче — главное, знать как. Спасибо, что были со мной! С кодом вы также можете ознакомиться на моём GitHub.

shalkov
/
TextFieldScroll
Waiting for api.github.com...
00K
0K
0K
Waiting...