Android
July 21

Как улучшить UX приложения при работе с полями ввода

Всем привет дорогие друзья!

Сегодня хотелось бы поговорить о наболевшем, о пользовательском опыте на экранах с полями ввода. Как правило, именно с полей ввода начинается знакомство пользователя с приложением. Стоит отметить, что ответственность за UX лежит не только на дизайнере, но и на разработчике. Начнём с настроек клавиатуры.

Типы клавиатур

В Android доступно несколько типов клавиатуры для осуществления первичной фильтрации вводимых данных. Ниже будут приведены типы, доступные для Jetpack Compose.

Буквенные клавиатуры

К буквенным можно отнести клавиатуры с типом:

  • Text (стандартная текстовая клавиатура);
  • Ascii (клавиатура позволяет ограничить ввод только ASCII символов);
  • Password (клавиатура позволяет вводить различные знаки препинания и цифры, при открытии клавиатура переключится на латинский алфавит);
  • Email (клавиатура для ввода электронного адреса, имеет вспомогательные кнопки в нижнем ряду для ускорения ввода данных);
  • Uri (клавиатура для ввода URI, имеет вспомогательные кнопки в нижнем ряду для ускорения ввода данных).

Примеры отображения буквенных клавиатур

Числовые клавиатуры

К числовым можно отнести клавиатуры с типом:

  • Number (клавиатура для ввода цифр и ряда символов);
  • NumberPassword (клавиатура для ввода цифровых паролей, в отличие от Number не позволяет ввести ничего кроме цифр);
  • Phone (клавиатура для ввода номера телефона и USSD-команд);
  • Decimal (клавиатура для ввода десятичных дробей).
Примеры отображения числовых клавиатур

Указание типа клавиатуры в Jetpack Compose

Указать тип клавиатуры для поля ввода можно при помощи класса KeyboardOptions.

TextField(
    keyboardOptions = KeyboardOptions.Default.copy(
            keyboardType = KeyboardType.Text, // Указываем тип клавиатуры    
    ),    
    ...
)

Помимо типа клавиатуры класс KeyboardOptions позволяет настраивать такие параметры как:

  • capitalization - сообщает клавиатуре, следует ли автоматически использовать заглавные буквы в словах и предложениях;
  • autoCorrect - сообщает клавиатуре, следует ли выполнять автоисправление для введёного текста;
  • imeAction - сообщает клавиатуре о необходимости изменить внешний вид вспомогательной кнопки на клавиатуре. Об этом поговорим чуть позднее, в рамках данной статьи.

Пароли и типы клавиатур

Если вы когда-нибудь видели поведение клавиатуры при записи экрана на iOS, во время ввода пароля, то для Android стоит снизить ожидания.

Скрытие содержимого клавиатуры и поля ввода при записи экрана на iOS

При использовании XML для полей ввода указывался параметр android:inputType="textPassword", который включал маску для вводимых символов.

Отсутствие скрытия содержимого клавиатуры и поля ввода при записи экрана на Android

При использовании Jetpack Compose это работает по другому. Для создания маски-пароля требуется передать PasswordVisualTransformation() в visualTransformation. Установка keyboardType в этом случае лишь подготовит клавиатуру к вводу пароля.

TextField(
    visualTransformation = PasswordVisualTransformation(), // Указываем маску для поля ввода пароля    
    keyboardOptions = KeyboardOptions.Default.copy(
            keyboardType = KeyboardType.Password, // Указываем тип клавиатуры    
    ),    
   ...
)

Однако, если требуется повысить уровень безопасности экрана ввода пароля, рекомендуется использовать SECURE_FLAG. Данный флаг предотвратит запись видео или создание скриншота на экране.

Типы действий на клавиатуре

На клавиатуре имеется кнопка действия (ImeAction).

ImeAction на клавиатуре в Android

Иногда использование данной кнопки является более удобным путём для пользователя, так как она находится в нижней части экрана. Например, не потребуется тянуться пальцем в верхнюю часть экрана.
Клавиатура поддерживает следующие действия:

Default

Система самостоятельно выберет действие для клавиатуры. Как правило это None или Done.

None

При клике на данное действие курсор перейдёт на новую строку.

Go

При клике на действие предполагается переход куда-либо. Например, переход по введенному URL.

Search

При клике на действие предполагается выполнение поискового запроса по введённому значению.

Send

При клике на действие предполагается отправка текста.

Previous

При клике на действие предполагается перевод фокуса на предыдущее поле ввода, если оно есть.

Next

При клике на действие предполагается перевод фокуса на следующее поле ввода, если оно есть.

Done

При клике на действие предполагается выполнение какого-либо завершающего действия (закрытие клавиатуры/отображение прогресса и отправка запроса).

Важно отметить, что иконки на ImeAction могут отличаться в зависимости от выбранной системной клавиатуры.

Указание ImeAction в Jetpack Compose

Указание ImeAction, как говорилось ранее, осуществляется с помощью класса KeyboardOptions.

TextField(
    keyboardOptions = KeyboardOptions.Default.copy(       
            imeAction = ImeAction.Search, // Указываем тип кнопки действия    
    ),    
    keyboardActions = KeyboardActions {
            // Указываем код, который требуется выполнить при клике на ImeAction    
    },    
    ...
)

Размещение контента над клавиатурой и другие проблемы, связанные с этим вопросом

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

Перекрытие контента прижатой к низу кнопкой при открытой клавиатуре

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

Пример перекрытия контента при открытии клавиатуры

В данном случае мы рекомендуем установить значение adjustResize для параметра windowSoftInputMode для Activity. Например, через Manifest.
Если в проекте используется BottomSheetDialogFragment, для него значение устанавливается отдельно через тему, так как данный класс имеет свой Window. Дополнительно, стоит указывать метод imePadding , который добавляет Spacer равный размеру клавиатуры для корректного подъема контента.

Верстка с использованием Jetpack Compose при этом может выглядеть следующим образом:

Box(modifier = Modifier.fillMaxSize()) {
    Column(modifier = Modifier.fillMaxSize()) {
            SomeHeader()        
            SomeBody()    
    }
        
    OverlayFooter()
}

Деформация контента при использовании adjustResize

Помните, что когда вы работаете с adjustResize, при недостатке места на экране вёрстка может начать деформироваться.

Деформация кнопки при недостаточном количестве места для контента при открытии клавиатуры

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

При необходимости добавляйте для контейнера возможность вертикального скролла. Это позволит избежать деформации верстки при недостатке экранного пространства.

Экран с возможностью скрола при недостаточном количестве места для контента при открытии клавиатуры

Поднятие контента над клавиатурой в BottomSheetDialogFragment

Достаточно часто в дизайне можно встретить боттомшит с полем ввода и кнопкой. Как правило, дизайнеры просят, чтобы при открытии клавиатуры весь контент включая кнопку поднимался над клавиатурой.

В этом случае, для BottomSheetDialogFragment следует указать adjustResize через тему. Это сработает на устройствах большинства производителей.

Однако для некоторых устройств Huawei для кнопки стоит указать параметр BringIntoViewRequester, который позволит проскролить контент до элемента, тем самым поднимет кнопку над клавиатурой.

val bringIntoViewRequester = remember { BringIntoViewRequester() }
val isFocused = remember { mutableStateOf(false) }
TextField(
    modifier = Modifier
            .fillMaxWidth()        
            .onFocusChanged{ isFocused.value = it.isFocused },
    ...        
)

Button(
    onClick = { 
           // Выполнить действие по клику    
    },    
    modifier = Modifier
            .fillMaxWidth()
            .bringIntoViewRequester(bringIntoViewRequester),
    ) {
        Text(text = "Отправить")
      }
)
      
LaunchedEffect(key1 = isFocused.value) {
    if (isFocused.value) {
            bringIntoViewRequester.bringIntoView()    
    }
}

Некорректная позиция handle в полях ввода внутри BottomSheetDialogFragment

Капелька, которая появляется при выделении или фокусе на тексте называется handle. Если вы встречались с поведением, как на скриншоте ниже, то у меня есть для вас крутая новость - это можно пофиксить.

Некорректная позиция handle при открытии клавиатуры на боттомшите

Данный баг связан с некорректной передачей координат местоположения поля ввода на экране в метод отрисовки handle.

Данное поведение можно исправить разместив контент боттомшита внутри Box:

Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.BottomCenter,
) {
    // Контент боттомшита
}

На этом наш пост подошёл к концу. Надеюсь, мой опыт работы с полями ввода поможет вам снизить количество граблей во время разработки функционала. Помните, что разработчик тоже влияет на UX приложения.

Good coding and happy day!🤘

Полезные ссылки:


Telegram-канал Теплица

Обратная связь