-
Notifications
You must be signed in to change notification settings - Fork 31
Home
В основе библиотеки лежат два понятия слот и маска. Слот определяет как будет отформатирован один символ. Маска же содержит список слотов и отвечает за форматирование последовательности символов.
Слот (Slot.java)
Слот - это позиция для вставки символа. Слоты организуются в двусвязный список с помощью ссылок Slot#nextSlot
и Slot#prevSlot
.
Слот работает с форматированием на уровне одного символа, определяя возможность этого символа находиться
в данной позиции.
Существует два способа вставки символа в слот: вставка сверху и вставка слева. Вставка сверху (вставка по умолчанию) происходит когда внешний источник (например, маска) устанавливает значение в слот (однако при помощи метода Slot#setValue(Character, boolean) внешние источники могу осуществлять вставку слева). Вставка слева происходит когда один слот "проталкивает" значение в следующий слот (см. ниже). Различие между этими методами существует только для hardcoded (неизменяемых) слотов и определяется правилом RULE_FORBID_LEFT_OVERWRITE (см. ниже).
Возможность вставки символа в слот определяется правилами вставки в слот и его валидаторами.
Для успешной вставки символа в слот:
- Слот должен иметь правило RULE_INPUT_MOVES_CURRENT;
- Символ должен удовлетворять требованиям всех валидаторов (при отсутствии валидаторов этот пункт пропускается).
Существует два возможных правила вставки символа в слот:
- RULE_INPUT_MOVES_CURRENT - новое значение сдвигает текущее значение в следующий слот. При удалении значения из слота на его место устанавливается значение из следущего. Это поведение по умолчанию и оно соответствует обычному режиму ввода текста в тектстовом редакторе.
- RULE_INPUT_MOVES_INPUT - при попытке вставить новое значение оно не заменяет текущее значение в слоте, а "проталкивается" в следующий. При попытке удаления значения из такого слота, ничего не проиходит. Данное правило является необходимым для создания hardcoded-слотов. Такой слот имеет предустановленное заничение и правило RULE_INPUT_MOVES_INPUT. Его значение нельзя изменить. По умлочанию в hardcoded слот можно вставить только его текущее значение, однако и эту вставку можно запретить (см. правило RULE_FORBID_LEFT_OVERWRITE).
- RULE_FORBID_LEFT_OVERWRITE - правило имеет смысл только вместе с правилом RULE_INPUT_MOVES_INPUT. Если в hardcoded слот с этим правилом приходит значение слева, то оно не "перезаписывает" иекущее значение, а "проталкивается" в следующий слот.
С помощью валидаторов можно ограничивать множество символов, доступных для вставки в слот.
SlotValidator
является интерфейсом:
public interface SlotValidator extends Serializable {
boolean validate(final char value);
}
Пример простейшего валидатора - SlotValidators.DigitValidator
. Если данный валидатор установлен на слот,
в слот можно будет вписать только цифру.
public static class DigitValidator implements Slot.SlotValidator {
@Override
public boolean validate(final char value) {
return Character.isDigit(value);
}
// <...>
}
Помимо правил и валидаторов каждый слот может иметь набор тегов. Тег - это некоторый Integer
,
ассоциированный со слтом. Теги внутри слота хранятся во множестве (Set<Integer>
). Класс Slot
не содержит никакого кода по работе с тегами (кроме их установки, получения и проверки наличия).
Однако, в классе объявлена константа Slot#TAG_DECORATION
. Слоты помеченные данным тегом
являются декоративными и по особому обрабатываются маской для получения неформатированной
строки (см. ниже).
Маска (Mask.java и MaskImpl.java)
Маска позволяет форматировать последовательности символов с помощью связного списка слотов.
Маска не занимается созданием слотов (за исключением случаев клонирования слота в нетерминированной
маске, (см. ниже)[#termination-flag]). Для получения форматированного текста, текст должен быть вставлен в маску с помощью методов
Mask#insertAt(int CharSequence)
или Mask#insertFront(CharSequence)
. Для получения форматированной
строки необходимо вызвать метод Mask#toString()
.
Для создания маски необходимы два параметра:
- Массив слотов. Маска преобразовывет слоты в двусвязый список и хранит в в нем введенный текст.
- Флаг терминированности. Определяет, возможна ли вставка символов когда все слоты заполнены.
Данный флаг определяет возможна ли вставка символов в маску, когда все слоты заполнены (см. листинги 5 и 6). Если маска нетерминирована, последний слот будет бесконено копироваться, удлинняя маску. Это позволяет делать маски "бесконечной" длины. Однако символы, вставленные "сверх нормы", форматрованы не будут.
В случае необходимости разового форматирования текста (форматирования НЕ "на лету") необходимо вручную
создать маску. Для форматирования "на лету" (форматирование текста в TextView
и EditText
) ручного
создания маски не требуется (см. ниже).
Создать маску можно тремя способами:
- Фабричными методами
MaskImpl#createTerminated(Slot[])
иMask#createNonTeminated(Slot[])
; - С помощью комструктора
MaskImpl#MaskImpl(Slot[], boolean)
; - С помощью реализации интерфейса
MaskFactory
, напримерMaskFactoryImpl
.
Кроме списка слотов и флага терминированности маска обладает некоторыми настраиваемыми параметрами:
-
forbidInputWhenFilled
- еслиtrue
, запрещает вставку в маску новых символов. Еслиfalse
, при вставке в середину заполненной маски, новые символы будут "выталкивать" символы из конца маски. По умолчанию -false
. -
hideHardcodedHead
- еслиtrue
скрывает hardcoded последовательность в начале строки при отсутствии пользовательсткого ввода (см. пример ниже) при вызовеtoString()
. По умолчанию -false
. -
showingEmptySlots
- еслиtrue
и в маске есть незаполенные слоты, то эти слоты будут выведены вtoString()
. ВАЖНО: когда данный флаг выставлен вtrue
, флагhideHardcodedHead
игнорируется. По умолчанию -false
-
placeholder
- символ, которым заменяются пустые пустые слоты при вызовеtoString()
когдаshowingEmptySlots = true
. По умолчанию_
(нижнее подчеркивание).
Пример 6:
Mask mask = MaskImpl.createTerminated(PredefinedSlots.RUS_PHONE_NUMBER); // +7 (___) ___-__-__
System.out.println(mask.toString()); // +7 (
Пример 7:
Mask mask = MaskImpl.createTerminated(PredefinedSlots.RUS_PHONE_NUMBER); // +7 (___) ___-__-__
mask.setHideHardcodedHead(true);
System.out.println(mask.toString()); // nothing
В листингах 7 и 8 произодится вывод пустой (без пользовательского ввода)
маски для ввода номера телефона. Данная маска имеет hardcoded-последовательность в начале - "+7 (".
В листинге 7 будет выведена эта последовательность, т.к. hideHardcodedHead = false
, в листинге 8
не будет выведено ничего, т.к. hideHardcodedHead = true
.
Пример 8:
Mask mask = MaskImpl.createTerminated(PredefinedSlots.RUS_PHONE_NUMBER);
mask.setShowingEmptySlots(true);
mask.setPlaceholder('*');
mask.insertFront("999");
System.out.println(mask.toString()); // +7 (999) ***-**-**
Для получения неформатированной строки используется метод Mask#getUnformattedString(boolean)
.
Данный метод возвращает строку из значений всех слотов, за исключением декоративных.
Пример 9: получение неформатированной строки
Mask mask = MaskImpl.createTerminated(new Slot[]{
PredefinedSlots.digit(), // слот для цифры
PredefinedSlots.digit(), // слот для цифры
PredefinedSlots.hardcodedSlot('-').withTags(Slot.TAG_DECORATION), // декоративный hardcoded слот
PredefinedSlots.digit(), // слот для цифры
PredefinedSlots.digit(), // слот для цифры
});
mask.insertFront("1234");
System.out.println(mask.toString()); // 12-34
System.out.println(mask.toUnformattedString()); // 1234
Пример 10: получение неформатированной строки
Mask mask = MaskImpl.createTerminated(PredefinedSlots.RUS_PHONE_NUMBER);
mask.insertFront("9995554433");
System.out.println(mask.toString()); // +7 (999) 555-44-33
System.out.println(mask.toUnformattedString()); // +79995554433
Для создания маски необходимо иметь массив слотов. Но часто приходится
создавать маску из некоторого строкового представления (raw-строки). Примером такого строкового представления
могут быть +7 (___) ___-__-__
и DD.MM.YYYY
. Чтобы иметь возможность создания маски из строки
используется интерфейс SlotsParser
. Он позволяет сконвертировать строковое представление маски в
набор слотов.
public interface SlotsParser {
Slot[] parseSlots(CharSequence rawMask);
}
В модуле предусмотрены две его реализации: UnderscoreDigitSlotsParser
и PhoneNumberUnderscoreSlotsParser
.
-
UnderscoreDigitSlotsParser
создает массив слотов, заменяя_
на слоты для ввода цифры, а остальные символы на hardcoded-слоты. -
PhoneNumberUnderscoreSlotsParser
работает также, как и предыдущий парсер, но с одной особенностью: все hardcoded цифры в маске кроме первой будут иметь флаг RULE_FORBID_LEFT_OVERWRITE.
Например, в маске+369 (___) __-__-__
цифры6
и9
будут иметь флаг RULE_FORBID_LEFT_OVERWRITE. Это необходимо для корректного форматирования, когда ввод производится в пустойEditText
. Когда пользователь введет цифру3
, будет автоматически отображение строки+369 (
и курсор будет установлен после(
. Если же пользователь вводит первым символом6
или9
- будет отображено+369 (6
или+369 (9
соответственно.
Поскольку маска хранит введенные в нее символы и позволяет получать форматированный или неформатрованный ввод,
пересоздание маски является частой операцией. Для облегчения процесса создания маски и созранения
параметров будущей маски (слоты, форматные строки и флаги) используется класс MaskDescriptor
.
Сущности данного класса можно использовать для задания маски в Format watcher'ах (см. ниже) или
создания маски с помощью MaskFactoryImpl
.
MaskDescriptor
может хранить как массив слотов для создания маски, так и raw-маску (строковое представление).
Во втором случае для создания маски из дескриптора понадобится также SlotsParser
. Если для дескриптора
указаны оба - массив слотов и raw-маска, то приоритет имеет массив слотов (для создания маски из
такого дескриптора не понадобится SlotsParser
).
Также MaskDescriptor
хранит все дополнительные параметры для маски.
final MaskDescriptor descriptor = MaskDescriptor.ofRawMask("+7 ___ ___-__-__")
.withShowingEmptySlots(true)
.withEmptySlotPalceholder('*')
.withForbiddingInputWhenFilled(true)
.withTermination(true)
.withHideHardcodedHead(true)
.withInitialValue("999"); // значение, которое будет предустановлено в маску, при ее создании
Кроме слотов (в виде массива или raw-строки) остальные параметры дескриптора являются необязательными.
В ситуациях, когда необходимо форматировать текст в TextView "на лету", например во время ввода пользователем
текста в EditText
, нам необходимо постоянно модифицировать текст внутри маски. Для этой цели используется
абстракный класс FormatWatcher
. Класс инкапсулирует создание маски и работу с ней. Создание маски не
определено в базовом классе и дожно быть реализовано в наследниках. Format watcher устанавливается на
TextView
и отслеживает изменения текста с помощью механизма TextWatcher
.
ВАЖНО: не рекомендуется ипользование format watcher'ов вместе с другими TextWatcher'ами. Для
получения оповещений об изменении текста в TextView
можно использовать FormattedTextChangeListener
.
Он устанавливается на watcher методом FormatWatcher#setCallback(FormattedTextChangeListener)
.
Модуль предоставляет реализацию format watcher'a - FormatWatcherImpl
. Этот watcher позволяет
менять маску уже после того как был установлен на TextView
. Маска создается на основе
MaskDescriptor
и (при необходимости) SlotsParser
.
Для корректной работы FormatWatcherImpl
требуется указать MaskDescriptor
. Сделать это
необходимо до установки watcher'a на TextView
- в конструкторе FormatWatcherImpl
или
с помощью метода FormatWatcherImpl#changeMask(MaskDescriptor)
(с помощью этого метода можно
также сменить маску после установки watcher'a).
Если указанный MaskDescriptor
содержит массив слотов, то его достаточно для создания маски. Если же
в декрипторе указана только raw-маска, то для создания маски потребуется предоставить SlotsParser
.
Сделать это можно либо в конструкторе FormatWatcherImpl
, либо с помощью метода
FormatWatcherImpl#setSlotsParser(SlotsParser)
.
final EditText editText = (EditText) findViewById(R.id.editCustom);
FormatWatcher formatWatcher = new FormatWatcherImpl(
new UnderscoreDigitSlotsParser(),
MaskDescriptor.ofRawMask("___ ___ ___", true).withShowingEmptySlots(true)
);
formatWatcher.installOnAndFill(editText);
editText.setText("123456");
System.out.println(editText.getText()); // 123 456 ___
editText.getText().insert(0, "789");
System.out.println(editText.getText()); // 789 123 456