Настройка полей ввода в 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, убрать пробелы) и проверьте бизнес-правила.