- PVSM.RU - https://www.pvsm.ru -
Сырые, но важные данные вроде номеров телефонов или кредиток — это именно то, что пользователи чаще всего вводят в наших приложениях. И с этим есть огромная проблема. Перепроверять 16 цифр своего Мастеркарда или 11 цифр номера телефона — это сущий ад для любого юзера. Решать эту проблему, естественно, приходится разработчикам, от лица которых я и пишу этот пост.
Поскольку современный Андроид не предоставляет инструментов для автоматического форматирования произвольного текста, эту задачу каждый решает своими костылями силами. Сначала в наших проектах эта задача решалась по месту: возникла необходимость — напиши свой TextWatcher и форматируй как надо. Но мы быстро поняли, что так делать не стоит — количество локальных костылей и специфических багов росло экспоненциально. Кроме того, задача весьма общая, так что и решать её надо системно.
Для начала хотелось следующего:
+7 (___) ___-__-__
Со временем вкусы наши, как и требования к инструменту, возросли, а варианты с гитхаба не смогли в полной мере их удовлетворить. Так что мы решили со всей серьезностью создать свой уютненький модуль для решения поставленной задачи.
Начав работать над этим направлением, мы осознали, что создание полноценного языка описания формата — это сродни написанию своего RegEx-движка, что, честно говоря, в наши планы не входило. В итоге мы пришли к варианту, когда такой язык при необходимости можно добавить в любой момент (даже в клиентском коде) или пользоваться простеньким DSL, доступным из коробки (что в нашей практике решило 90% задач).
Посмотрев на то, что получилось, мы решили, что это круто, и надо бы поделиться с сообществом. Так у нас и родилась библиотека для Android-разработки Decoro. И сейчас я покажу пару фокусов из её арсенала.
Подключаем:
dependencies {
compile "ru.tinkoff.decoro:decoro:1.1.1"
}
Допустим, нам надо попросить пользователя ввести серию и номер паспорта.
Задача тривиальная — надо всего лишь добавить пробельчик и ограничить длину ввода:
Slot[] slots = new UnderscoreDigitSlotsParser().parseSlots("____ ______");
FormatWatcher formatWatcher = new MaskFormatWatcher( // форматировать текст будет вот он
MaskImpl.createTerminated(slots)
);
formatWatcher.installOn(passportEditText); // устанавливаем форматтер на любой TextView
В примере выше мы сделали три важных вещи:
Вводим серию и номер паспорта.
Честно говоря, задачу про паспорт можно было решить чуть проще, для нее у нас есть уже есть заготовка:
FormatWatcher formatWatcher = new MaskFormatWatcher(
MaskImpl.createTerminated(PredefinedSlots.RUS_PASSPORT) // маска для серии и номера
);
formatWatcher.installOn(passportEditText); // тут аргументом может быть любой TextView
Теперь, когда мы посмотрели на Decoro в действии, скажем пару слов о тех сущностях, которыми она оперирует.
+1 (___) ___-__-__
). SlotsParser как раз призван помочь нам это сделать. Обычный String
он приводит к массиву слотов, которым умеет оперировать наша маска.
Теперь чуть подробнее о том, как работает Decoro.
Наша маска ввода определяет, как именно будет отформатирован пользовательский текст. И главный атрибут этой маски — связный список слотов, каждый из которых отвечает за один символ. Так что в примере с паспортом у нас после ввода получилась такая структура:
Каждый слот держит один символ и указатели на соседей. Красным я обозначил hardcoded слот, его значение изменить нельзя.
Для создания маски нам нужен массив слотов. Его можно создать вручную, можно взять готовый из класса PredefinedSlots, а можно использовать какую-нибудь реализацию интерфейса SlotsParser (например, упомянутый выше UnderscoreDigitSlotsParser) и получить этот массив из простой строки. UnderscoreDigitSlotsParser работает просто — для каждого символа _ он создаст слот, в который можно записывать только цифры (ведь для каждого слота можно еще и ограничить множество допустимых символов). А для всех остальных символов создаст hardcoded слоты, и в маску они войдут как есть (это и произошло с нашим пробелом). Подобным образом можно написать свой уникальный SlotsParser и получить возможность описывать маски на своем собственном DSL.
Когда мы только начинали работать над библиотекой, мы думали, что для слота будет достаточно двух поведений hardcoded/non-hardcoded. Казалось, что в красненькие символы складывать будет нельзя, а в беленькие можно. Но это была иллюзия.
Сначала выяснилось, что всё-таки надо позволить вставлять символ в hardcoded-слот. Но только тот символ, который там уже лежит. Иначе не работает функционал копировать-вставить. Допустим, в маску про российский номер телефона я пытаюсь вставить +79991112233 (в смысле, сделать paste), а у меня получается +7 (+799) 911-12-23. Добавили такую возможность. Однако, вскоре выяснилось, что и это поведение не всегда корректно. В итоге мы пришли к так называемым правилам вставки, которые накладываются на каждый слот отдельно.
Слоты организованы в двусвязный список, и каждый из них знает про своих соседей. Вставка или удаление символа в одном из слотов может привести к модификации его соседей. Приведет или нет — зависит от правил этого слота. Варианты правил такие:
Все слоты в режиме вставки.
Все слоты в режиме замены.
При попытке вставки в начало «телефонной» маски символы проталкиваются через цепочку hardcoded-слотов +43 (
.
Как выяснилось, эти простые правила позволяют описать маски практически для любых целей. Мы таким образом описываем телефонные номера (с произвольными кодами страны), даты и номера документов.
Но забудем на время про красоту ввода в EditText. Бывает и такое, что надо всего лишь разово отформатировать строку. Создавать для этого целый TextWatcher было бы излишним. Воспользуемся маской напрямую, без посредников.
Mask inputMask = MaskImpl.createTerminated(PredefinedSlots.CARD_NUMBER_STANDART);
inputMask.insertFront("5213100000000021");
Log.d("Card number", inputMask.toString()); // Card number: 5213 1000 0000 0021
Log.d("RAW number", inputMask.toUnformattedString()); // RAW number: 5213100000000021
И теперь для произвольной маски:
Slot[] slots = new PhoneNumberUnderscoreSlotsParser().parseSlots("+86 (1__) ___-____");
Mask inputMask = MaskImpl.createTerminated(slots);
inputMask.insertFront("991112345");
Log.d("Phone number", inputMask.toString()); // Phone number: +86 (199) 111-2345
Log.d("RAW phone", inputMask.toUnformattedString()); // RAW phone: +861991112345
В примерах выше вы могли обратить внимание на метод Mask#toUnformattedString()
. Он волшебным образом позволяет нам получить строку без лишней мишуры, с одними только данными. Сейчас расскажу, как это работает.
Каждый слот, помимо правил вставки и, собственно, значения, содержит еще и набор тэгов. Тэг — это просто Integer
, и слот содержит их Set
. Сам слот ничего с этими тэгами делать не умеет, может только хранить. Нужны они для внешнего мира (прямо как View#mKeyedTags
только в плоской структуре). Тэгами можно пользоваться по своему усмотрению. Из коробки же доступен тэг Slot#TAG_DECORATION
, который позволяет помечать слоты как декоративные.
Когда мы дергаем Mask#toString()
, маска собирает значения со всех слотов и формирует из них единую строку. Вызов же Mask#toUnformattedString()
пропускает декоративные слоты, что позволяет исключить из финальной строки незначимые символы (вроде пробелов и скобок).
Остается только пометить нужные слоты как декоративные. Если вы используете доступные из коробки наборы слотов (из класса PredefinedSlots
), там декоративные уже помечены, так что вы просто берете и пользуетесь. Если же слоты создаются из строки, то эта работа ложится на SlotsParser
. Из коробки создавать декоративные слоты умеет PhoneNumberUnderscoreSlotsParser
. Декоративными он сделает все позиции, кроме цифр и плюса. Если же вы пишете свой SlotsParser, то пометить слот как декоративный помогут методы Slot#getTags()
и Slot#withTags(Integer...)
.
И несколько слов о том, что еще умеет Decoro:
MaskImpl#createNonTerminated()
. В них последний слот бесконечно копируется, и в маску можно вставить сколько угодно текста.
FormatWatcher formatWatcher = new MaskFormatWatcher(
MaskImpl.createNonTerminated(PredefinedSlots.RUS_PHONE_NUMBER)
);
formatWatcher.installOn(phoneEditText);
Mask#setHideHardcodedHead()
). Это полезно для полей ввода номера телефона.
Mask mask = MaskImpl.createTerminated(PredefinedSlots.RUS_PHONE_NUMBER);
mask.setHideHardcodedHead(true);
FormatWatcher formatWatcher = new MaskFormatWatcher(mask);
formatWatcher.installOn(phoneEditText);
Mask mask = MaskImpl.createTerminated(PredefinedSlots.RUS_PHONE_NUMBER);
mask.setHideHardcodedHead(false); // default value
FormatWatcher formatWatcher = new MaskFormatWatcher(mask);
formatWatcher.installOn(phoneEditText);
Mask#setForbidInputWhenFilled()
позволяет запретить вводить новые символы, если все свободные места уже заняты.
Mask mask = MaskImpl.createTerminated(PredefinedSlots.RUS_PHONE_NUMBER);
mask.setForbidInputWhenFilled(true);
FormatWatcher formatWatcher = new MaskFormatWatcher(mask);
formatWatcher.installOn(phoneEditText);
Mask mask = MaskImpl.createTerminated(PredefinedSlots.RUS_PHONE_NUMBER);
mask.setForbidInputWhenFilled(false); // default value
FormatWatcher formatWatcher = new MaskFormatWatcher(mask);
formatWatcher.installOn(phoneEditText);
Mask#toString()
вернет строку только до первого незаполненного символа). Mask#setShowingEmptySlots()
позволяет включить отображение пустых слотов. На их месте будет отображаться placeholder (по умолчанию _), свой placeholder можно задать с помощью Mask#setPlaceholder()
. Данная функция работает только при работе с маской напрямую и недоступна для использования внутри FormatWatcher’а.
final Mask mask = MaskImpl.createTerminated(PredefinedSlots.RUS_PHONE_NUMBER);
mask.setPlaceholder('*');
mask.setShowingEmptySlots(true);
Log.d("Mask", mask.toString()); // Mask: +7 (***) ***-**-**
mask.insertFront("999");
Log.d("Mask", mask.toString()); // Mask: +7 (999) ***-**-**
Благодарю за внимание!
Автор: Тинькофф Банк
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android/207134
Ссылки в тексте:
[1] нашей вики: https://github.com/TinkoffCreditSystems/decoro/wiki/FormatWatcher-%D0%B8-%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B2-EditText-%22%D0%BD%D0%B0-%D0%BB%D0%B5%D1%82%D1%83%22
[2] на гитхабе: https://github.com/TinkoffCreditSystems/decoro
[3] Источник: https://habrahabr.ru/post/312968/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.