Enter the text beautifully

  • Tutorial
Raw, but important data like phone numbers or credit cards is exactly what users most often enter in our applications. And there is a huge problem with this . Re-checking 16 digits of your MasterCard or 11 digits of a phone number is a hell for any user. Naturally, the developers have to solve this problem, on behalf of whom I am writing this post.

Since modern Android does not provide tools for automatically formatting arbitrary text, everyone solves this problem with his crutchesforces. At first, in our projects, this task was solved locally: the need arose - write your TextWatcher and format as needed. But we quickly realized that this should not be done - the number of local crutches and specific bugs grew exponentially. In addition, the task is very general, so it must be solved systematically.

For starters, I wanted the following:

  1. Pointed a mask like +7 (___) ___-__-__
  2. Hung it on EditText
  3. ...
  4. PROFIT

Over time, our tastes, as well as the requirements for the instrument, increased, and the options from the github could not fully satisfy them. So we decided in all seriousness to create our cozy module for solving the task.

Having started working on this area, we realized that creating a full-fledged format description language is akin to writing our RegEx engine, which, frankly, was not part of our plans. As a result, we came to the option where, if necessary, such a language can be added at any time (even in the client code) or use the simple DSL, available out of the box (which in our practice solved 90% of the tasks).

After looking at what happened, we decided that it was cool, and we should share it with the community. So we had the Decoro Android development library. 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 the series and passport number. The task is trivial - you just need to add a space and limit the length of the input:

Slot[] slots = new UnderscoreDigitSlotsParser().parseSlots("____ ______");
FormatWatcher formatWatcher = new MaskFormatWatcher( // форматировать текст будет вот он
    MaskImpl.createTerminated(slots)
); 
formatWatcher.installOn(passportEditText); // устанавливаем форматтер на любой TextView

In the example above, we did three important things:

  1. We described the input mask using an arbitrary string.
  2. We created our FormatWatcher and initialized it with this mask.
  3. Hung FormatWatcher on EditText.


Enter the series and passport number.

Honestly, the problem about the passport could be solved a little easier, for it we already have a blank:

FormatWatcher formatWatcher = new MaskFormatWatcher(
    MaskImpl.createTerminated(PredefinedSlots.RUS_PASSPORT) // маска для серии и номера
);
formatWatcher.installOn(passportEditText); // тут аргументом может быть любой TextView



Now that we have looked at Decoro in action, let's say a few words about the entities that she operates with.

  • Mask . The input mask is the heart of our library. It is she who determines how to decorate our raw data. The mask operates with slots and can be used both independently and inside FormatWatcher .
  • The Slot . Inside a mask, a slot is a position where you can insert a single character . It determines which characters can be inserted, and how it will affect neighboring slots. We'll talk more about masks and slots below.
  • PredefinedSlots contains predefined sets of slots (for phone number, passport, and so on)
  • FormatWatcher or formatter is an abstract implementation of TextWatcher. It holds the mask inside and synchronizes its contents with the contents of the TextView. This guy is used to format text “on the fly” while the user enters it. There are implementations of MaskFormatWatcher and DescriptorFormatWatcher in the box , you can read about the differences between them 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 (sort of +1 (___) ___-__-__). SlotsParser is designed to help us do this. Normal Stringit leads to an array of slots , which our mask can operate on.

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 linked list of slots , each of which is responsible for one character. So in the passport example, after entering, we got the following structure:


Each slot holds one character and pointers to neighbors. I marked red with a hardcoded slot; its value cannot be changed.

To create a mask, we need an array of slots. You can create it manually, you can take it 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 only numbers can be written (after all, for each slot you can also limit the set of valid characters). And for all other characters it will create hardcoded slots, and they will go into 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 two hardcoded / non-hardcoded behaviors would be enough for the slot. It seemed that it would be impossible to add to the little red characters, but to the little white ones. But it was an illusion.

First it turned out that you still need to allow the symbol to be inserted into the hardcoded slot. But only the symbol that already lies there. Otherwise, copy-paste functionality does not work. Suppose I try to insert +79991112233 (in the sense of paste) in the mask about the Russian phone number, and I get +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.

Slots are organized in a doubly linked list, and each of them knows about their neighbors. Inserting or deleting a character in one of the slots may lead to modification of its neighbors. Will it lead or not - it depends on the rules of this slot. The rule options are as follows:

  1. Insert mode. Unless you specify a specific rule, the slot behaves like a character in your text editor in normal mode. Let's try to insert another symbol in its place, and the current one will go to the next position and shift the whole text. The new symbol will take its place. By default, slots behave the same way.


    All slots in insert mode.

  2. Replacement mode. This is the same as entering text with the INSERT key pressed on the keyboard. The new value of the slot replaces the current, but does not affect the neighbors.


    All slots are in replacement mode.

  3. Hardcoded mode. The new symbol is “pushed” into the next slot, but the current value does not change. This mode is convenient to combine with the replacement mode. In this case, you can insert the same value into the hardcoded slot that is already written in it, and this will not affect the neighbors.


    When you try to insert at the beginning of the "telephone" mask, characters are pushed through a chain of hardcoded slots +43 (.

As it turned out, these simple rules allow you to describe masks for almost any purpose. In this way we describe telephone numbers (with arbitrary country codes), dates and document numbers.

Interesting fact
Initially, we described only 2 rules: “insert” and “hardcoded”. And when the rule about "replacement" was required, it turned out that it was implemented by itself - it was enough not to indicate either the first or the second. We rejoiced as children and dreamed that all the laws of the Universe can be described by a set of such primitive rules.

Formatting in code


But let's forget for a while about the beauty of input in EditText. It also happens that you just need to format the string once. Creating an entire TextWatcher for this would be superfluous. We use the mask directly, without intermediaries.

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

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()); // Phone number: +86 (199) 111-2345
Log.d("RAW phone", inputMask.toUnformattedString()); // RAW phone: +861991112345

Decorative slots


In the examples above, you could pay attention to the method Mask#toUnformattedString(). It magically allows us to get a row without too much tinsel, with only data. Now I’ll tell you how it works.

Each slot, in addition to the insertion rules and, in fact, the values, also contains a set of tags . The tag is simple Integer, and the slot contains them Set. The slot itself does not know how to do anything with these tags, it can only store. They are needed for the outside world (just as View#mKeyedTagssoon as in a flat structure). Tags can be used as you wish. Out of the box, a tag is available Slot#TAG_DECORATIONthat allows you to mark slots as decorative .

When we pull Mask#toString(), the mask collects values from all slotsand forms a single line from them. The call Mask#toUnformattedString()misses decorative slots , which allows you to exclude insignificant characters from the final line (like spaces and brackets).

It remains only to mark the necessary slots as decorative. If you use the slot sets available from the box (from the class PredefinedSlots), the decorative ones are already marked there, so you just pick it up and use it. If the slots are created from a string, then this work rests on SlotsParser. He knows how to create decorative slots out of the box PhoneNumberUnderscoreSlotsParser. He will make decorative all positions except numbers and pluses. If you are writing your SlotsParser, then the Slot#getTags()and methods will help mark the slot as decorative Slot#withTags(Integer...).



And a few words about what else Decoro can do:

  • Endless masks using MaskImpl#createNonTerminated(). In them, the last slot is endlessly copied, and you can paste 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);
    



  • Hide / show the chain of hardcoded slots at the beginning of the mask, depending on the fullness of the mask ( 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); // default value
    FormatWatcher formatWatcher = new MaskFormatWatcher(mask);
    formatWatcher.installOn(phoneEditText);
    


  • Prohibition of entry into the filled mask. Mask#setForbidInputWhenFilled()allows you to prohibit the introduction of new characters, if all free spaces 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); // default value
    FormatWatcher formatWatcher = new MaskFormatWatcher(mask);
    formatWatcher.installOn(phoneEditText);
    


  • Display the entire mask regardless of occupancy (by default it Mask#toString()will return a string only up to the first empty character). Mask#setShowingEmptySlots()allows you to enable the display of empty slots. A placeholder will be displayed in their place (default _ ), you can set your placeholder with Mask#setPlaceholder(). This function works only when working with the 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()); // Mask: +7 (***) ***-**-**
    mask.insertFront("999");
    Log.d("Mask", mask.toString()); // Mask: +7 (999) ***-**-**
    



You can find the source of the library, ask a question and report bugs on the github . Comments, suggestions and suggestions are welcome.

Thank you for attention!

Also popular now: