SoatDev IT Consulting
SoatDev IT Consulting
  • About us
  • Expertise
  • Services
  • How it works
  • Contact Us
  • News
  • July 26, 2023
  • Rss Fetcher
Photo by Anja Bauermann on Unsplash

Ever wonder, how to make the text inside TextInputLayout censored and/or partially censored? But the plot twist is, how can you censor the text without putting a “password” property on its inputType to accomplish this task?

How about setting it as a censored text first, then putting it in TextInputLayout? It could work, but it also needs extra steps to uncensor to its original when the validation process is executed. Then, how? Well, this is the story.

Long story short: there is a new feature designed to protect user-sensitive data. The feature will hide the user’s personal information so that when certain pages contain email addresses, id card numbers, full names, or phone numbers, the content of each will be censored. In this case, those pages are profile form page and profile view page. On the profile form page, the user fills in their personal information that will be submitted later.

The requirement is to censor the text inside the input field (TextInputLayout) with the asterisk symbol (∗) while/after submitting the data. Even when there is already existing data from response callback into that field, its data must also be censored. Each field type (email, name, id card, phone, etc.) has its own censoring type and rule.

To visualize it, imagine a user opening the profile form page. It has some fields of TextInputLayout that need to be filled. Let’s say, at that time, a user fills in the full name field. In that case, the field will be censored after finished. The password field inspires the whole project. It looks like we create a password but with some adjustments and behavior differences.

TransformationMethod; It is

Because we originally wanted to mimic how the password field works, we explored TextInputLayout source code. The source code can be found in the Android material module .jar or in this repository. Without wasting time, we immediately search “password” in that file. The findings lead us to the end icon toggle functionality, called END_ICON_PASSWORD_TOGGLE.

https://github.com/material-components/material-components-android/blob/174a57dabeb6e0481993b71af71e8aaa8afbb68d/lib/java/com/google/android/material/textfield/TextInputLayout.java#L3488C1-L3500C4

/**
* Set up the end icon mode. When set, a button is placed at the end of the EditText which enables
* the user to perform the specific icon's functionality.
*
* @param endIconMode the end icon mode to be set: {@link #END_ICON_PASSWORD_TOGGLE}, {@link
* #END_ICON_CLEAR_TEXT}, or {@link #END_ICON_CUSTOM}; or {@link #END_ICON_NONE} to clear the
* current icon if any
* @attr ref com.google.android.material.R.styleable#TextInputLayout_endIconMode
*/
public void setEndIconMode(@EndIconMode int endIconMode) {
endLayout.setEndIconMode(endIconMode);
}

TL;DR: The icon mode’s purpose is to show a password toggle if its EditText displays a password. It is set into endLayout (EndCompoundLayout) which has EndIconDelegates property inside. This delegate object contains a selection of other delegates, including PasswordToggleEndIconDelegate. We are getting close!

https://github.com/material-components/material-components-android/blob/174a57dabeb6e0481993b71af71e8aaa8afbb68d/lib/java/com/google/android/material/textfield/EndCompoundLayout.java#L349C1-L352C4

EndIconDelegate getEndIconDelegate() {
return endIconDelegates.get(endIconMode);
}
https://github.com/material-components/material-components-android/blob/174a57dabeb6e0481993b71af71e8aaa8afbb68d/lib/java/com/google/android/material/textfield/EndCompoundLayout.java#L358C1-L388C4

void setEndIconMode(@EndIconMode int endIconMode) {
if (this.endIconMode == endIconMode) {
return;
}
tearDownDelegate(getEndIconDelegate());
int previousEndIconMode = this.endIconMode;
this.endIconMode = endIconMode;
dispatchOnEndIconChanged(previousEndIconMode);
setEndIconVisible(endIconMode != END_ICON_NONE);
EndIconDelegate delegate = getEndIconDelegate();
setEndIconDrawable(getIconResId(delegate));
setEndIconContentDescription(delegate.getIconContentDescriptionResId());
setEndIconCheckable(delegate.isIconCheckable());
if (delegate.isBoxBackgroundModeSupported(textInputLayout.getBoxBackgroundMode())) {
setUpDelegate(delegate);
} else {
throw new IllegalStateException(
"The current box background mode "
+ textInputLayout.getBoxBackgroundMode()
+ " is not supported by the end icon mode "
+ endIconMode);
}
setEndIconOnClickListener(delegate.getOnIconClickListener());
if (editText != null) {
delegate.onEditTextAttached(editText);
setOnFocusChangeListenersIfNeeded(delegate);
}
applyIconTint(textInputLayout, endIconView, endIconTintList, endIconTintMode);
refreshIconState(/* force= */ true);
}
https://github.com/material-components/material-components-android/blob/174a57dabeb6e0481993b71af71e8aaa8afbb68d/lib/java/com/google/android/material/textfield/EndCompoundLayout.java#L826C1-L851C6

EndIconDelegate get(@EndIconMode int endIconMode) {
EndIconDelegate delegate = delegates.get(endIconMode);
if (delegate == null) {
delegate = create(endIconMode);
delegates.append(endIconMode, delegate);
}
return delegate;
}


private EndIconDelegate create(@EndIconMode int endIconMode) {
switch (endIconMode) {
case END_ICON_PASSWORD_TOGGLE:
return new PasswordToggleEndIconDelegate(endLayout, passwordIconDrawableId);
case END_ICON_CLEAR_TEXT:
return new ClearTextEndIconDelegate(endLayout);
case END_ICON_DROPDOWN_MENU:
return new DropdownMenuEndIconDelegate(endLayout);
case END_ICON_CUSTOM:
return new CustomEndIconDelegate(endLayout);
case END_ICON_NONE:
return new NoEndIconDelegate(endLayout);
default:
throw new IllegalArgumentException("Invalid end icon mode: " + endIconMode);
}
}

Now we know that END_ICON_PASSWORD_TOGGLE will return a delegate object, PasswordToggleEndIconDelegate. Thus, that object contains properties. For example, it contains OnClickListener. This listener is called to work within the toggle icon click listener, setEndIconOnClickListener(). This indicates that whenever the toggle is clicked, it will run the block codes inside.

https://github.com/material-components/material-components-android/blob/174a57dabeb6e0481993b71af71e8aaa8afbb68d/lib/java/com/google/android/material/textfield/PasswordToggleEndIconDelegate.java#L36C1-L53C5

private final OnClickListener onIconClickListener = view -> {
if (editText == null) {
return;
}
// Store the current cursor position
final int selection = editText.getSelectionEnd();
if (hasPasswordTransformation()) {
editText.setTransformationMethod(null);
} else {
editText.setTransformationMethod(PasswordTransformationMethod.getInstance());
}
// And restore the cursor position
if (selection >= 0) {
editText.setSelection(selection);
}
refreshIconState();
};

Finally, the block codes themselves set a TransformationMethod into EditText (correspondence with TextInputLayout, of course). Wait, what is that? PasswordTransformationMethod? Well, turns out it is a text transformer. It is used to do things like replace the characters of the source with other characters (quoted from this). In this case, it changes the password’s characters into dots. That is what we want to work with, the TransformationMethod.

Mimic PasswordTransformationMethod

We know for sure that it changes the characters of passwords into dots. Theoretically, it can change certain characters inside the input field into an asterisk symbol, as we planned. Let’s learn the structure of this PasswordTransformationMethod first. Again, the source can be accessed here.

https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/method/PasswordTransformationMethod.java

public class PasswordTransformationMethod
implements TransformationMethod, TextWatcher
{
public CharSequence getTransformation(CharSequence source, View view) {
if (source instanceof Spannable) {
Spannable sp = (Spannable) source;
/*
* Remove any references to other views that may still be
* attached. This will happen when you flip the screen
* while a password field is showing; there will still
* be references to the old EditText in the text.
*/
ViewReference[] vr = sp.getSpans(0, sp.length(),
ViewReference.class);
for (int i = 0; i < vr.length; i++) {
sp.removeSpan(vr[i]);
}
removeVisibleSpans(sp);
sp.setSpan(new ViewReference(view), 0, 0,
Spannable.SPAN_POINT_POINT);
}
return new PasswordCharSequence(source);
}

...

After reading it carefully, the part that has an important role is getTransformation(). Compared to other methods available there, it is the only method that actually does something with the text (source), though. This method returns a new CharSequence from the original source. In this case, it returns the PasswordCharSequence. This PasswordCharSequence is where the magic takes place.

https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/method/PasswordTransformationMethod.java

...

private static class PasswordCharSequence
implements CharSequence, GetChars
{
...

public char charAt(int i) {
if (mSource instanceof Spanned) {
Spanned sp = (Spanned) mSource;
int st = sp.getSpanStart(TextKeyListener.ACTIVE);
int en = sp.getSpanEnd(TextKeyListener.ACTIVE);
if (i >= st && i < en) {
return mSource.charAt(i);
}
Visible[] visible = sp.getSpans(0, sp.length(), Visible.class);
for (int a = 0; a < visible.length; a++) {
if (sp.getSpanStart(visible[a].mTransformer) >= 0) {
st = sp.getSpanStart(visible[a]);
en = sp.getSpanEnd(visible[a]);
if (i >= st && i < en) {
return mSource.charAt(i);
}
}
}
}
return DOT;
}

...

PasswordCharSequence and the implementation of GetChars are part of CharSequence, but let’s just focus on CharSequence. This class generates a sequence of chars (char type values). At the charAt() method, it returns a char at a specific index, in this case, a dot.

How about other methods? Are they not important at all? Other methods or block codes inside a PasswordTransformationMethod or PasswordCharSequence can be ignored because they are handling the spannable version of the source when copying or pasting from other input fields. It is just some “cleansing” procedure, IMO. All right, now we may start working on our own TransformationMethod. Let’s get started.

Our Own TransformationMethod

To start, let’s define all the censor types and their rules. To make it as simple as possible, the requirement only needs to censor certain characters in the middle of words.

For example, to censor the “name” field, it will only show two characters at the prefix and one character at the suffix. The remaining characters will become asterisks, so if the text is “Fatah,” then the result will be “Fa∗∗h.” If the text length does not fit the rule, it can be done by censoring it all the way or not at all. Now, we can make it like this, knowing it can be changed later. Let’s call it CensorType.

enum class CensorType(
val minLength: Int,
val startPeek: Int,
val endPeek: Int
) {
DEFAULT(minLength = 0, startPeek = 0, endPeek = 0),
NAME(minLength = 5, startPeek = 2, endPeek = 1),
PHONE(minLength = 6, startPeek = 3, endPeek = 1),
ID_NUMBER(minLength = 6, startPeek = 2, endPeek = 2),
EMAIL(minLength = 5, startPeek = 1, endPeek = 1)
}

Next is the main event, the TransformationMethod. Create a new file of TransformationMethod. It will only receive basic properties such as CensorType and its own properties — a Char type represents the symbol replacement and a Boolean flag to enable censoring. The flag will be used to activate or deactivate the censor later.

class DataCensorTransformationMethod(
private val censorType: CensorType,
private val minLengthRule: Int = censorType.minLength,
private val startPeek: Int = censorType.startPeek,
private val endPeek: Int = censorType.endPeek,
private val censorSymbol: Char = SYMBOL,
private val censored: Boolean = true
) : TransformationMethod {

...

Properties such as minLengthRule, startPeek and endPeek are declared alongside its CensorType. This is intended for any customization that may occur in later development. Then, implement all the rest methods. The first method is getTransformation(); its role is to return the censored version of CharSequence.

Important to note: it says, “… the returned text must mirror it dynamically instead of doing a one-time copy.” You can read more at this link.

class DataCensorTransformationMethod(
...
) : TransformationMethod {

override fun getTransformation(
source: CharSequence,
view: View?
): CharSequence {
return CensoredCharSequence(source)
}

override fun onFocusChanged(
view: View?,
sourceText: CharSequence?,
focused: Boolean,
direction: Int,
previouslyFocusedRect: Rect?
) {
...
}
...

private inner class CensoredCharSequence(
private val source: CharSequence
) : CharSequence {
override val length: Int
get() = source.length

override fun get(index: Int): Char {
when (censorType) {
...
}
}

override fun subSequence(startIndex: Int, endIndex: Int): CharSequence {
return source.subSequence(startIndex, endIndex)
}
}

...

The censoring process takes place in this class, CensoredCharSequence, specifically inside the get() method. It is similar to charAt in the Java version, though.

So, the code will work like this: read the censor type, validate the index if it complies with the rule return censorSymbol, and is also applicable when the length is not fit. Otherwise, return the original character at that index. Simple as that. First, let’s develop CensorType.NAME as an example. Here’s the code:

...

override fun get(index: Int): Char {
when (censorType) {
CensorType.NAME -> return if (length < minLengthRule || index in
startPeek until (length - endPeek)
) {
censorSymbol
} else {
source[index] // Original char
}
else -> return censorSymbol
}
}

...

The important part has been completed at this stage, and then our TransformationMethod can finally be used. We just need to set it into the input field to show the result.

textInputLayout?.editText?.transformationMethod = DataCensorTransformationMethod(CensorType.NAME)
Left: User input “Fatah” as a new value; Right: User edit the existing name into “Fadhi.”

It works, but anyone would be annoyed by not being able to see what they typed. So, now it’s time for the other TransformationMethod method to jump into action. Let’s get into onFocusChanged(). This method will be triggered when the input field loses or gains its focus (technically, when the user clicks another field or reselects the previous field). When the user selects the input field, it must switch to uncensored mode. This time, we will be using the censored property.

data class DataCensorTransformationMethod(
...
private val censored: Boolean = true
) : TransformationMethod {

override fun getTransformation(
source: CharSequence,
view: View?
): CharSequence {
return if (censored) {
CensoredCharSequence(source)
} else {
UncensoredCharSequence(source)
}
}

override fun onFocusChanged(
view: View?,
sourceText: CharSequence?,
focused: Boolean,
direction: Int,
previouslyFocusedRect: Rect?
) {
val editText = view as? EditText
val transformer = editText?.transformationMethod

if (transformer is DataCensorTransformationMethod) {
if (focused) {
editText.transformationMethod =
transformer.copy(censored = false)
} else {
editText.transformationMethod =
transformer.copy(censored = true)
}
}
}

...
...

private inner class UncensoredCharSequence(private val source: CharSequence) :
CharSequence {
...

override fun get(index: Int): Char {
return source[index]
}

...

So, now, let’s go inside onFocusChanged(). When focused is true, the uncensored mode will be activated by copying the current transformation method. This copying process is executed by calling copy(), a part of the data class in Kotlin. So, we need to change DataCensorTransformationMethod into a data class. Thus, censored property can be changed to false directly. Otherwise, it will be changed to true. By doing so, this input field will automatically change its mode.

Now, what is the UncensoredCharSequence? It is just a stupid class of CharSequence that is similar to CensoredCharSequence, but only returns the original text itself. Just look at the get() method above. IMO, we can return the source in getTransformation() first (in this case, I’m trying to dynamically mirror it as the best practice says. To be honest, I could be wrong, though). Still, the results could be great.

At first, the name field is censored. After selecting it, it becomes uncensored to show its content, making it easier to type in.

As you can see, when the input text field is selected, it will be uncensored. After that, when it changes to another field, it goes back to being censored again. Looks cool. With this basic technique, we can achieve more advanced censoring types and rules. For instance, look at this:

Another censoring type and rule. You can use your creativity to add variations later, like censors on the email that hide the username and domain name.

Conclusion

This tool, TransformationMethod, can be used in many scenarios that require content transformation. It has been a great journey to deep dive into the source code and find what we were looking for. Later, we can get a better understanding of how the password field works, especially how TextInputLayout also works.

Well, that’s it. I hope you all like it. Thanks for reading until the end. Bye 👋


TextInputLayout Censoring With Password-Like Method was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.

Previous Post
Next Post

Recent Posts

  • Week in Review: Why Anthropic cut access to Windsurf
  • Will Musk vs. Trump affect xAI’s $5 billion debt deal?
  • Superblocks CEO: How to find a unicorn idea by studying AI system prompts
  • Sage Unveils AI Trust Label to Empower SMB’s
  • How African Startups Are Attracting Global Fintech Funding

Categories

  • Industry News
  • Programming
  • RSS Fetched Articles
  • Uncategorized

Archives

  • June 2025
  • May 2025
  • April 2025
  • February 2025
  • January 2025
  • December 2024
  • November 2024
  • October 2024
  • September 2024
  • August 2024
  • July 2024
  • June 2024
  • May 2024
  • April 2024
  • March 2024
  • February 2024
  • January 2024
  • December 2023
  • November 2023
  • October 2023
  • September 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • April 2023

Tap into the power of Microservices, MVC Architecture, Cloud, Containers, UML, and Scrum methodologies to bolster your project planning, execution, and application development processes.

Solutions

  • IT Consultation
  • Agile Transformation
  • Software Development
  • DevOps & CI/CD

Regions Covered

  • Montreal
  • New York
  • Paris
  • Mauritius
  • Abidjan
  • Dakar

Subscribe to Newsletter

Join our monthly newsletter subscribers to get the latest news and insights.

© Copyright 2023. All Rights Reserved by Soatdev IT Consulting Inc.