
Raw but important data like phone numbers or credit cards is exactly what users most often enter in our applications. And with this there is a
huge problem. Recheck 16 digits of your Mastercard or 11 digits of a phone number is a hell for any user. Naturally, developers have to solve this problem, on whose behalf I am writing this post.
Since modern Android does not provide tools for automatic formatting of arbitrary text, everyone solves this problem with his
crutches . First, in our projects, this problem was solved by the place: the need arose - write your TextWatcher and format it as it should. But we quickly realized that this was not worth doing - the number of local crutches and specific bugs grew exponentially. In addition, the task is very general, so that it must be solved systemically.
For starters, I wanted the following:
- I specified a mask like
+7 (___) ___-__-__
- Hung it on EditText
- ...
- PROFIT
Over time, our tastes, as well as the requirements for the tool, increased, and the options from the githaba could not fully satisfy them. So we decided to seriously create our cozy module to solve the problem.
')
Having started working on this direction, we realized that creating a full-fledged language for describing the format is akin to writing our own RegEx engine, which, frankly, was not in our plans. As a result, we have come to the option when, if necessary, such a language can be added at any time (even in the client code) or use simple DSL available from the box (which in our practice solved 90% of the tasks).
Looking at what happened, we decided that it was cool, and we should share it with the community. This is how the
Decoro Android development library was born. And now I will show a couple of tricks from her arsenal.
We connect:
dependencies { compile "ru.tinkoff.decoro:decoro:1.1.1" }
Suppose we need to ask the user to enter a series and passport number. The task is trivial - you just need to add a space bar and limit the length of the input:
Slot[] slots = new UnderscoreDigitSlotsParser().parseSlots("____ ______"); FormatWatcher formatWatcher = new MaskFormatWatcher(
In the example above, we did three important things:
- Described the input mask using an arbitrary string.
- We created our own FormatWatcher and initialized it with this mask.
- Hang FormatWatcher on EditText.
Enter the series and passport number.Honestly, the problem of a passport could be solved a little easier, for it we already have a blank:
FormatWatcher formatWatcher = new MaskFormatWatcher( MaskImpl.createTerminated(PredefinedSlots.RUS_PASSPORT)
Now, when we looked at Decoro in action, let's say a few words about the entities with which she operates.
- Mask . The input mask is the heart of our library. It is what determines how to decorate our raw data. The mask operates on slots and can be used both independently and inside FormatWatcher .
- Slot Inside the mask slot is a position into which you can insert a single character . It determines exactly which characters can be inserted, and how this will affect adjacent slots. We will talk more about masks and slots below.
- PredefinedSlots contains preset slot sets (for phone number, passport, and so on)
- FormatWatcher or formatter is an abstract implementation of TextWatcher. He holds the mask inside and synchronizes its contents with the contents of the TextView. It is this guy who is used to format the text "on the fly" while the user enters it. In the box there are implementations of MaskFormatWatcher and DescriptorFormatWatcher , the difference between them can be found in our wiki . In the same article, we will only operate on MaskFormatWatcher , because it provides a cleaner and more understandable API.
- Sometimes we want to create a mask based on some DSL (like
+1 (___) ___-__-__
). SlotsParser is designed to help us do this. Regular String
it leads to an array of slots that our mask can handle.
What is a slot
Now a little more about how Decoro works. Our
input mask determines how the custom text will be formatted. And the main attribute of this mask is a coherent list of
slots , each of which is responsible for one character. So, in the example with the passport, after entering, we got the following structure:
Each slot holds one character and pointers to the neighbors. I marked the red hardcoded slot, its value can not be changed.To create a mask, we need an array of slots. You can create it manually, you can take it ready from the
PredefinedSlots class, or you can use some implementation of the
SlotsParser interface (for example, the
UnderscoreDigitSlotsParser mentioned above) and get this array from a simple string.
UnderscoreDigitSlotsParser works simply - for each character
_ it will create a slot in which you can only write numbers (after all, for each slot you can also limit the number of valid characters). And for all other characters will create
hardcoded slots, and they will enter the mask as is (this happened with our space). Similarly, you can write your own unique SlotsParser and get the opportunity to describe masks on your own DSL.
When we first started working on the library, we thought that there would be two hardcoded / non-hardcoded behaviors for the slot. It seemed that it would be impossible to put in the little red symbols, but in the little white ones it was possible. But it was an illusion.
At first it turned out that after all it was necessary to allow the symbol to be inserted into the hardcoded slot. But only the symbol that is already there. Otherwise, the copy-paste functionality does not work. Suppose I’m trying to insert a mask about a Russian phone number +79991112233 (in the sense of paste), and I’ve got +7 (+799) 911-12-23. Added this feature. However, it soon became clear that this behavior is not always correct. As a result, we came to the so-called
insertion rules , which are superimposed on each slot separately.
The slots are organized in a doubly linked list, and each of them knows about its neighbors. Inserting or deleting a character in one of the slots may result in modification of its neighbors. Will lead or not - depends on the
rules of this slot. Variants of the rules are:
- Insert mode If you do not specify a specific rule, the slot behaves like a character in your text editor in the usual way. We will try to insert another symbol in its place, and the current one will go to the next position and move all the text. The new character will take its place. By default, slots behave exactly the same.

All slots are in insert mode.
- Replacement mode. This is the same as entering text while holding the INSERT button on the keyboard. The new value of the slot replaces the current one, but does not affect the neighbors.

All slots are in replacement mode.
- Hardcoded mode. The new character is “pushed” into the next slot, and the current value does not change. This mode is convenient to combine with the replacement mode. In this case, the same value that is already written in it can be inserted into the hardcoded slot, and this will not affect the neighbors.

When trying to insert a “phone” mask at the beginning, the characters are pushed through the chain of hardcoded slots +43 (
.
As it turned out, these simple rules allow you to describe masks for almost any purpose. We thus describe phone numbers (with arbitrary country codes), dates and document numbers.
Interesting factInitially, we described only 2 rules: "insert" and "hardcoded". And when the rule about “replacement” was required, it turned out that it was realized by itself - it was enough not to specify either the first or the second. We were happy as children and dreamed that all the laws of the Universe could be described by a set of such primitive rules.
We format in the code
But let's forget for a while about the beauty of the input to EditText. It also happens that you just need to format the string just once. Creating an entire TextWatcher for this would be redundant. We use the mask directly, without intermediaries.
Mask inputMask = MaskImpl.createTerminated(PredefinedSlots.CARD_NUMBER_STANDART); inputMask.insertFront("5213100000000021"); Log.d("Card number", inputMask.toString());
And now for an arbitrary mask:
Slot[] slots = new PhoneNumberUnderscoreSlotsParser().parseSlots("+86 (1__) ___-____"); Mask inputMask = MaskImpl.createTerminated(slots); inputMask.insertFront("991112345"); Log.d("Phone number", inputMask.toString());
Decorative Slots
In the examples above, you could pay attention to the
Mask#toUnformattedString()
method. He magically allows us to get a string without too much tinsel, with only data. Now I will tell how it works.
Each slot, in addition to the rules of insertion and, in fact, values, also contains a set of
tags . The tag is just an
Integer
, and the slot contains their
Set
. The slot itself cannot do anything with these tags, it can only store. They are needed for the outside world (just like
View#mKeyedTags
only in a flat structure). Tags can be used on your own. Out of the box, the
Slot#TAG_DECORATION
tag is available, which allows you to mark slots as
decorative .
When we pull
Mask#toString()
, the mask collects values
from all slots and forms one single string from them. Calling
Mask#toUnformattedString()
skips the decorative slots , which allows to exclude insignificant characters from the final string (such as spaces and brackets).
It remains only to mark the necessary slots as decorative. If you use the out-of-the-box slot sets (from the
PredefinedSlots
class), the decorative ones are already marked there, so you just take and use them. If the slots are created from the string, then this work falls on the
SlotsParser
. Out of the box,
PhoneNumberUnderscoreSlotsParser
can create decorative slots. Decorative, he will make all positions, except for numbers and plus. If you are writing your SlotsParser, then
Slot#getTags()
and
Slot#withTags(Integer...)
will help to mark the slot as decorative.
And a few words about what Decoro can do:
- Infinite masks using
MaskImpl#createNonTerminated()
. In them, the last slot is infinitely copied, and you can insert as much text as you like into the mask.
Non-terminated mask FormatWatcher formatWatcher = new MaskFormatWatcher( MaskImpl.createNonTerminated(PredefinedSlots.RUS_PHONE_NUMBER) ); formatWatcher.installOn(phoneEditText);

- Hiding / showing the hardcoded slots chain at the beginning of the mask depending on the mask fullness (
Mask#setHideHardcodedHead()
). This is useful for phone number entry fields.
Hide hardcoded head
MaskImpl mask = MaskImpl.createTerminated(PredefinedSlots.RUS_PHONE_NUMBER); mask.setHideHardcodedHead(true); FormatWatcher formatWatcher = new MaskFormatWatcher(mask); formatWatcher.installOn(phoneEditText);

MaskImpl mask = MaskImpl.createTerminated(PredefinedSlots.RUS_PHONE_NUMBER); mask.setHideHardcodedHead(false);
- Prohibition of entry into the completed mask.
Mask#setForbidInputWhenFilled()
allows you to prevent new characters from being entered if all free places are already taken.
Forbid input when filled
MaskImpl mask = MaskImpl.createTerminated(PredefinedSlots.RUS_PHONE_NUMBER); mask.setForbidInputWhenFilled(true); FormatWatcher formatWatcher = new MaskFormatWatcher(mask); formatWatcher.installOn(phoneEditText);

MaskImpl mask = MaskImpl.createTerminated(PredefinedSlots.RUS_PHONE_NUMBER); mask.setForbidInputWhenFilled(false);
- Displays the entire mask regardless of its fullness (by default,
Mask#toString()
returns a string only up to the first empty character). Mask#setShowingEmptySlots()
allows you to enable the display of empty slots. In their place will be displayed placeholder (default _ ), your placeholder can be set using Mask#setPlaceholder()
. This function only works when working with a mask directly and is not available for use inside FormatWatcher.
Set showing empty slots final Mask mask = MaskImpl.createTerminated(PredefinedSlots.RUS_PHONE_NUMBER); mask.setPlaceholder('*'); mask.setShowingEmptySlots(true); Log.d("Mask", mask.toString());
You can find the library sources, ask a question and report bugs
on a githaba . Comments, suggestions and suggestions are welcome.
Thank you for attention!