📜 ⬆️ ⬇️

Enter text beautifully

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:

  1. I specified a mask like +7 (___) ___-__-__
  2. Hung it on EditText
  3. ...
  4. 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( //      MaskImpl.createTerminated(slots) ); formatWatcher.installOn(passportEditText); //     TextView 

In the example above, we did three important things:

  1. Described the input mask using an arbitrary string.
  2. We created our own FormatWatcher and initialized it with this mask.
  3. 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) //      ); formatWatcher.installOn(passportEditText); //      TextView 



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


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:

  1. 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.

  2. 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.

  3. 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 fact
Initially, 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()); // 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 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:




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!

Source: https://habr.com/ru/post/312968/


All Articles