Настройка полей ввода в Android: маска, проверка и IME
Чтобы поле ввода в Android работало “как в нормальной форме”, достаточно разделить задачи: клавиатура и кнопки IME задаются в XML, ограничения делаются фильтрами, маска форматирует отображение, а валидация отвечает на вопрос “данные корректны?”.
Оглавление
Клавиатура: inputType и imeOptions
android:inputType отвечает за раскладку, подсказки и поведение ввода, а android:imeOptions — за кнопку на клавиатуре (Next/Done).
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress"
android:imeOptions="actionNext"/>
Для последнего поля формы обычно ставят actionDone и обрабатывают действие:
etPassword.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
submitForm()
true
} else false
}
Если поле многострочное, не ставьте actionNext: для комментариев чаще нужен textMultiLine, а “Enter” должен вставлять перенос строки.
Ограничения: длина и допустимые символы
Клавиатура не гарантирует чистый ввод (пользователь может вставить что угодно). Поэтому критичные поля ограничивают фильтрами.
Длина:
etCard.filters = arrayOf(InputFilter.LengthFilter(19 + 3)) // 19 цифр + пробелы
Только нужные символы (например, цифры и разделитель):
etAmount.keyListener =
DigitsKeyListener.getInstance(Locale.getDefault(), /*sign*/ false, /*decimal*/ true)
inputType="numberDecimal" не защищает от вставки букв/пробелов. Для надёжности комбинируйте keyListener/InputFilter и нормализацию текста перед отправкой.
Маска ввода через TextWatcher (без боли с рекурсией)
Маска — это форматирование “как выглядит текст”, а не проверка корректности. Базовый паттерн: защита от рекурсии + форматирование из “сырого” значения.
class MaskWatcher(
private val editText: EditText,
private val rawTransform: (String) -> String = { it.filter(Char::isDigit) },
private val format: (String) -> String
) : TextWatcher {
private var selfChange = false
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
override fun afterTextChanged(s: Editable) {
if (selfChange) return
val raw = rawTransform(s.toString())
val formatted = format(raw)
if (formatted == s.toString()) return
selfChange = true
s.replace(0, s.length, formatted)
editText.setSelection(formatted.length.coerceAtMost(s.length))
selfChange = false
}
}
Примеры форматирования:
fun formatCard(raw: String) =
raw.take(19).chunked(4).joinToString(" ") // 1234 5678 9012 3456
fun formatMmyy(raw: String): String {
val d = raw.take(4)
return if (d.length <= 2) d else d.substring(0, 2) + "/" + d.substring(2)
}
etCard.addTextChangedListener(MaskWatcher(etCard, format = ::formatCard))
etExpiry.addTextChangedListener(MaskWatcher(etExpiry, format = ::formatMmyy))
Если важно, чтобы курсор “не прыгал”, восстанавливайте позицию не “в конец”, а по количеству цифр слева от курсора до форматирования. Это особенно заметно в телефонах и картах.
Валидация и ошибки через TextInputLayout
Для форм удобнее связка TextInputLayout + TextInputEditText: контейнер показывает ошибку, helper-текст и счётчик.
private fun validateEmail(showError: Boolean): Boolean {
val v = etEmail.text?.toString().orEmpty().trim()
val ok = v.contains("@") && v.length <= 64
tilEmail.error = if (showError && !ok) "Введите корректный email" else null
return ok
}
etEmail.doAfterTextChanged { validateEmail(showError = false) }
btnSubmit.setOnClickListener {
val ok = validateEmail(showError = true)
if (ok) submitForm()
}
Лучший UX — не “краснить” поле при каждом символе: показывайте ошибку после потери фокуса или после первой попытки отправки.
Частые ошибки
- Маска и валидация смешаны: в итоге маска мешает редактированию, а “корректность” не проверяется.
- Нет защиты от рекурсивного
TextWatcher: получаются зависания или “мигание” текста. - Рассчитывают на
inputTypeкак на защиту от мусора — но вставка обходит ограничения. - Ставят
actionDoneна промежуточные поля и ломают переход по форме (лучшеactionNextиfocusSearch/requestFocus). - Делают маску, но забывают хранить/отправлять “raw” (например, номер карты без пробелов).
FAQ
Можно ли сделать маску только через regex?
Regex хорош для проверки, но для пошагового форматирования удобнее процедурный форматтер + TextWatcher.
Что выбрать: EditText или TextInputEditText?
Для форм — TextInputEditText внутри TextInputLayout: проще показывать ошибки, подсказки и счётчики.
Где валидировать “по-настоящему”?
В UI — мягко (подсказки/ошибки), перед отправкой — строго: нормализуйте строку (trim, убрать пробелы) и проверьте бизнес-правила.