not fairly Jetpack Compose: VisualTransformation Made Simpler | by Carter Hudson | Mar, 2023 will cowl the newest and most present counsel practically the world. proper of entry slowly consequently you comprehend with ease and appropriately. will development your information cleverly and reliably
In the event you do not care how the sausage is made, skip to the tip for the implementation.
Jetpack Compose has launched the VisualTransformation
API for formatting and reworking TextField
enter. I not too long ago had a must format a telephone quantity because it was written, so I looked for an idiomatic answer.
Not eager to reinvent the wheel, I went on the lookout for an answer. Absolutely somebody had already fastened this, proper? Properly, not a lot, it appears.
Each proposed implementation I may discover appeared overly designed or utterly guide in its calculations. My mind simply nops. Strive it your self, give it a search.
Taking the time to know it, not to mention keep it up sooner or later, looks as if an even bigger time funding than I am keen to make. There needs to be a greater manner, proper?
Let’s attempt to implement ours.
This class needs you to implement the filter
operate, which is to format the incoming uncooked string to the illustration for show. I want that they had known as this operate one thing else, like format
However it’s what it’s.
The return worth of this operate needs an occasion of TransformedText
. Let’s examine what it is all about
Right here we’ve two parameters to the constructor: AnnotatedString
and one thing known as OffsetMapping
. He AnnotatedString
it is fairly widespread, so I will not go into an excessive amount of element right here. It will simply hold your string formatted usually, so you will find yourself with one thing like AnnotatedString(textual content = SomeFormatter.format(textual content), ...)
. Fairly easy. Now, what concerning the second parameter of TransformedText
? one thing known as OffsetMapping
.
Oh boy let me inform you concerning the vampire of time that’s OffsetMapping
. This factor is the actual meat and potatoes of transformation. That is the place the magic occurs.
Okay, a minor rant right here – skip a number of paragraphs if you happen to do not thoughts.
To begin with, the parameter identify actually bothers me: offset
. It appears innocuous sufficient at first look, however what precisely are we compensating for? Let’s examine the paperwork:
Convert the unique textual content offset to the remodeled textual content offset.
This operate should be a monotonically non-decreasing operate. In different phrases, if a cursor advances within the unique textual content, the cursor within the remodeled textual content should advance or keep there.
Monotonically nondecreasing. Understood. I am glad they simplified it for me within the subsequent sentence.
Okay, if we use some context clues right here, we are able to assume that it is concerning the cursor offset for the TextField
. It is smart, proper? In the event you begin including symbols and so forth to a telephone quantity, the cursor place for the unique textual content won’t be the identical place within the remodeled textual content.
From what I can inform, the bidirectionality of the factor helps when the consumer highlights segments of textual content for copy/paste. Both manner, he needs each, so we’ll give him each.
Finish of the diatribe.
In the event you search on-line, you will see folks recommending a extra guide process for offering compensation allowances. When implementing originalToTransformed
it might think about the present cursor offset within the unique string and take into consideration the place that may be within the remodeled string.
A zero offset is to the left of the place the primary character would seem: the remaining place of a void TextField
.
An offset with the worth 1 can be to the correct of the primary character, and so forth.
That is fairly easy for one thing like a zipper code or bank card. Let’s take a look at a really fast instance. Be at liberty to skip this if you have already got the thought.
I observed that there appear to be two approaches to formatting these strings: what I name “anxious” and “lazy”, for lack of higher phrases. I simply made this up. For instance, let’s check out a zipper code.
Fundamental ZIP codes are 5 digits, however generally the types provide the possibility, and even require, a ZIP+4 format. ZIP+4 has 5 digits, a hyphen, after which 4 digits: xxxxx-xxxx
.
Anxious towards Lazy refers to when the spacers are inserted.
Observe: |
refers back to the cursor
Anxious
What I name “keen” right here, within the case of a zipper code, signifies that the hyphen would seem when the fifth digit is entered: 1234| -> 12345-|
Lazy
In contrast, what I name “lazy” refers to inserting the hyphen when the sixth character is entered: 12345| -> 12345-6|
Which one ought to I take advantage of?
Actually, it relies upon, and it varies. In case you have a formatter that you don’t have any management over, like Google’s telephone quantity formatter, you owe it to them. In case you have management over the formatter, you possibly can dictate which technique to make use of. For our zip instance, if ZIP+4 is required, possibly it should eagerly show the script. If it is non-obligatory, possibly solely present it if the consumer begins typing it.
In the event you combine methods between formatters and offset mappings, you may get misplaced cursors or, in some instances, crash altogether.
Now that that is out of the way in which, let’s think about one thing extra sophisticated than a zipper code.
Bank card: lazy formatter, lazy offsets
Let’s examine how we are able to visually remodel a bank card quantity because it’s entered. We shall be utilizing a lazy formatter and lazy offsets for this instance. In contrast to the zip instance, we’ve multiple separator that we should respect. First, our easy formatter:
object LazyCreditCardFormatter : Formatter
override enjoyable format(enter: CharSequence): CharSequence =
enter
.chunked(4)
.joinToString(" ")
Fairly easy and lazy. Now we’ve to consider our OffsetMapping
Unique to Remodeled
They provide us the unique cursor offset, and we’re compelled to compute what the offset can be for the remodeled string. You may discover that I’ve used concrete ranges right here fairly than making an attempt to assemble them abstractly by making higher than/lower than statements.
override enjoyable originalToTransformed(offset: Int): Int =
when (offset)
in (1..4) -> offset
in (5..8) -> offset + 1
in (9..12) -> offset + 2
in (13..16) -> offset + 3
else -> offset
If we’re within the first vary, no separators have been inserted into the remodeled string, so nothing adjustments with our offsets.
If we’re within the second vary, which means a fifth character has been entered. The string has been formatted to have an area between characters 4 and 5. That is the purpose at which the formatter and offset allocation ought to observe the identical keen or lazy technique. If the cursor had been within the unique string, it might be after the fifth character. Since an additional house has been inserted, we have to transfer it after the sixth character, so offset + 1
and so forth for every rank dice.
Remodeled to Unique
This one breaks my mind generally, for no matter cause. On this instance, we go in the other way for the offset transformations. Our vary cubes are barely completely different, relying on how we would like the cursor location to behave when a consumer highlights textual content. You might have to mess around with this a bit for every particular case.
override enjoyable transformedToOriginal(offset: Int): Int =
when (offset)
in (1..4) -> offset
in (5..9) -> offset - 1
in (11..14) -> offset - 2
in (15..19) -> offset - 3
else -> offset
that doesn’t appear additionally dangerous. Our when
assertion has grown a bit, however that is to be anticipated with extra separators within the combine, proper? At worst, you will in all probability spend an hour or two soaking in it, making an attempt to get it to behave correctly. Chances are you’ll must tweak and tweak it a bit for every completely different use case.
However what if you must do one thing that has a extra dynamic format, like a telephone quantity?
On no account do I need to discover out all of the doable compensation assignments for telephone numbers. The format can change drastically as you kind, to not point out the assorted codecs for worldwide calls. That looks as if a nightmare. Let’s search for a extra dynamic answer.
originaltransformed
In originalToTransformed
, we had been making some additions relying on which vary group the unique offset was in. Some concepts come to thoughts, however they rapidly show naive below check.
Can we depend what number of separators are within the formatted string after which add it to our unique offset? That finally ends up breaking the cursor path via all the string. In case you have 3 separators, you possibly can solely return to the third offset.
What about one thing like this?
override enjoyable originalToTransformed(offset: Int): Int
if (offset == 0)
return 0
return formatted
.substring(0, offset)
.depend isSeparator(it) + offset
That will not work both. The cursor location is delayed after the primary separator is inserted. If we substrict to offset
we fall behind after the primary separator and have bizarre cursor placement each time.
I cannot insist on the purpose. I simply know that I attempted numerous issues. Really feel slightly dangerous for me, okay? I am working with a tiny mind, right here.
after which clicked
What we actually need to know is the place to place the cursor within the remodeled string, when given a place within the unique string, proper?
Contemplate the uncooked string 5551234
and the formatted string 555–1234
.
We all know that every index within the string is one lower than the correct shift of that character. If we have a look at the formatted string, we all know all of the indices of all of the characters. If we all know that, we are able to derive every offset for every character, proper? It is only a matter of filtering out the indices we’re all for and changing the indices into offsets.
if we circled 555–1234
in an inventory of indices for all non-separator characters, we might find yourself with [0,1,2,4,5,6,7]
. Discover that the script index is lacking.
If we add one to every of these indices, we might find yourself with offsets: [1,2,3,5,6,7,8]
. Nonetheless, if we run this, we’ll by no means be capable to transfer the cursor to the left of the primary character, every shift shall be offset by one, and we’ll in all probability crash with empty strings. That is simple sufficient to repair: simply prepend a 0 firstly and we’ll remedy all these issues: [0,1,2,3,5,6,7,8]
.
Now we all the time know every remodeled offset. Since we filter out all separators, we are able to entry utilizing the unique offset with out going out of bounds.
Let’s attempt. If we’re given an unique offset of 4, we all know we might place the cursor right here: 555-1|234
within the remodeled string. If we entry our record of offsets remodeled at index 4, we get 5
. The fifth offset within the remodeled string shall be to the correct of the 1
simply as we would like.
Right here is the related implementation:
override enjoyable originalToTransformed(offset: Int): Int
val transformedOffsets = formatted
.mapIndexedNotNull index, c ->
index
.takeIf PhoneNumberUtils.isNonSeparator(c)
// convert index to an offset
?.plus(1)
// We need to assist an offset of 0 and shift every thing to the correct,
// so we prepend that index by default
.let offsetList ->
listOf(0) + offsetList
return transformedOffsets[offset]
remodeled to unique
Let’s examine one other instance:
(555) 123-4567
If we’re given a remodeled offset of 11, which is to the correct of 4
, we are able to have a look at our unique string and see that it might correspond to an offset of seven. The important thing right here is to note the sample. The distinction between the values of the 2 offsets is the same as the variety of separators previous the remodeled offset.
As soon as we discover that, issues change into fairly easy:
override enjoyable transformedToOriginal(offset: Int): Int =
formatted
// This creates an inventory of all separator offsets
.mapIndexedNotNull index, c ->
index.takeIf !PhoneNumberUtils.isNonSeparator(c)
// We need to depend what number of separators precede the remodeled offset
.depend separatorIndex ->
separatorIndex < offset
// We discover the unique offset by subtracting the variety of separators
.let separatorCount ->
offset - separatorCount
There we go, that is so significantly better than having to write down an excellent tedious when
assertion. However taking a look at this, would not this answer appear fairly generic already?
The one factor right here that refers to telephone numbers is PhoneNumberUtils.isNonSeparator(c)
. With that in thoughts, we may in all probability make a generic implementation fairly simply:
summary class GenericSeparatorVisualTransformation : VisualTransformation {summary enjoyable remodel(enter: CharSequence): CharSequence
summary enjoyable isSeparator(char: Char): Boolean
override enjoyable filter(textual content: AnnotatedString): TransformedText {
val formatted = remodel(textual content)
return TransformedText(
textual content = AnnotatedString(textual content = formatted.toString()),
object : OffsetMapping
override enjoyable originalToTransformed(offset: Int): Int
val transformedOffsets = formatted
.mapIndexedNotNull index, c ->
index
.takeIf !isSeparator(c)
// convert index to an offset
?.plus(1)
// We need to assist an offset of 0 and shift every thing to the correct,
// so we prepend that index by default
.let offsetList ->
listOf(0) + offsetList
return transformedOffsets[offset]
override enjoyable transformedToOriginal(offset: Int): Int =
formatted
// This creates an inventory of all separator offsets
.mapIndexedNotNull index, c ->
index.takeIf isSeparator(c)
// We need to depend what number of separators precede the remodeled offset
.depend separatorIndex ->
separatorIndex < offset
// We discover the unique offset by subtracting the variety of separators
.let separatorCount ->
offset - separatorCount
)
}
}
I ended up utilizing this to format telephone numbers. As a bonus, right here is a straightforward telephone quantity formatter I ready that helps worldwide format. Press 0 as the primary entry to prepend a +
which prompts the worldwide format.
class SimplePhoneNumberFormatter(defaultCountry: String = Locale.getDefault().nation) personal val formatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(defaultCountry)
personal enjoyable String.replaceIndexIf(
index: Int,
worth: Char,
predicate: (Char) -> Boolean
): String =
if (predicate(get(index)))
toCharArray()
.apply set(index, worth)
.let(::String)
else this
enjoyable format(quantity: String): String =
quantity
.takeIf it.isNotBlank()
?.replaceIndexIf(0, '+') c ->
c == '0'
?.filter
?.let sanitized ->
formatter.clear()
var formatted = ""
sanitized.forEach
formatted = formatter.inputDigit(it)
formatted
?: quantity
Use these two courses collectively like this:
class PhoneNumberVisualTransformation(
defaultLocale: Locale = Locale.getDefault(),
) : GenericSeparatorVisualTransformation() personal val phoneNumberFormatter = SimplePhoneNumberFormatter(defaultLocale.nation)
override enjoyable remodel(enter: CharSequence): CharSequence =
phoneNumberFormatter.format(enter.toString())
override enjoyable isSeparator(char: Char): Boolean = !PhoneNumberUtils.isNonSeparator(char)
And bear in mind to restrict the entries in your textual content discipline, otherwise you’ll have a tough time:
TextField(
worth = telephone,
onValueChange = worth -> telephone = worth.takeWhile it.isDigit() ,
visualTransformation = bear in mind PhoneNumberVisualTransformation()
)
I hope you could have realized one thing. I do know I did. And I do not need to need to study it ever once more, so I wrote this text as a dev journal. Phew.
P.S.
In the event you actually like all of the keen formatting and wish this strategy to assist enthusiastic offsets, you could possibly do one thing like:
personal enjoyable getOffsetFactor(enter: CharSequence, index: Int): Int
if (!useEagerOffsets)
return 1
val nextIndex = index + 1
val hasNext = nextIndex <= enter.lastIndex
val nextIsSeparator = hasNext && isSeparator(enter[nextIndex])
return if (nextIsSeparator) 2 else 1
go a flag within the generic displacement mapping:
summary class GenericSeparatorVisualTransformation(personal val useEagerOffsets: Boolean = false) :
VisualTransformation
and do one thing like:
index
.takeIf !isSeparator(c)
// convert index to an offset
?.plus(getOffsetFactor(formatted, index))
However I kinda hate it, and I did not check it a lot, so your mileage might range.
I want the article roughly Jetpack Compose: VisualTransformation Made Simpler | by Carter Hudson | Mar, 2023 provides keenness to you and is helpful for complement to your information
Jetpack Compose: VisualTransformation Made Easier | by Carter Hudson | Mar, 2023