Different colors at EditText in Android - android

I am trying to make the text of an EditText multiple colors. For example, if my text is, "It is a good day.", is it possible to make the "It is a" part of the sentence green and the rest red?

I use something like that to make some parts of my color green:
final String text = "Some Text";
Spannable modifiedText = new SpannableString(text);
modifiedText.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.green)), 0, lengthYouWant, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(modifiedText);

You could use spannables.
Spannable spannable = yourText.getText();
spannable .setSpan(new BackgroundColorSpan(Color.argb(a, r, g, b)), start, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE);

I'm have this trouble too. After several hours, I figured out how to handle it:
mYourTextView.addTextChangedListener(new TextWatcher() {
private String oldContent = "";
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
final String newContent = mYourTextView.getText().toString();
if (newContent.length() > 10 && !oldContent.equals(newContent)) {
oldContent = newContent;
mYourTextView.setText(Html.fromHtml(
String.format("%s", "<font color='#000000'>" + newContent.substring(0, 10) + "</font>")
+ "<font color='#ff0000'>" + newContent.substring(10, newContent.length()) + "</font>"));
Log.d("onTextChanged", "Start : " + start);
//move cursor after current character
mYourTextView.setSelection(start + 1 > mYourTextView.getText().toString().length()
? start : start + 1);
}
}
#Override
public void afterTextChanged(Editable s) {
}
});
This code make first 10 characters in black color and the followed characters in red color.
We need variable oldContent to prevent loop infinity because when EditText call setText() then onTextChanged

Yes. You will need to create a Spannable object (either a SpannedString or SpannedStringBuilder), then set spans upon it to apply the colors you seek.
For example, the following method from this sample project takes the contents of a TextView, searches for a user-entered string, and marks up all occurrences with a purple background color, removing all previous markers:
private void searchFor(String text) {
TextView prose=(TextView)findViewById(R.id.prose);
Spannable raw=new SpannableString(prose.getText());
BackgroundColorSpan[] spans=raw.getSpans(0,
raw.length(),
BackgroundColorSpan.class);
for (BackgroundColorSpan span : spans) {
raw.removeSpan(span);
}
int index=TextUtils.indexOf(raw, text);
while (index >= 0) {
raw.setSpan(new BackgroundColorSpan(0xFF8B008B), index, index
+ text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
index=TextUtils.indexOf(raw, text, index + text.length());
}
prose.setText(raw);
}
In your case, changing the foreground color would use a ForegroundColorSpan instead of a BackgroundColorSpan.
Things get a bit tricky with an EditText, in that the user can edit the text, and you will need to choose your flags to meet the rules you want. For example, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE would say that:
characters entered in the middle of the spanned area get the span's effect (e.g., foreground color)
characters entered immediately before or after the spanned area are considered outside the spanned area and therefore do not get the span's effect

Just to elaborate on the answer of WarrenFaith:
implement a TextWatcher
yourEditText.onFocusChangeListener = this
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
val newTextStyle: Spannable = SpannableString(s?.toString())
resources.getColor(R.color.color_w3w, requireActivity().theme)
val span = ForegroundColorSpan(resources.getColor(R.color.your_color_here, requireActivity().theme))
newTextStyle.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)//Will set the first three characters to 'R.color.your_color_here'
s?.set(0, 3, span)
}

Related

SpannableString looses spans when used multiple times

This is the code that I have.
val s = SpannableString("hello")
s.setSpan(StyleSpan(Typeface.BOLD), 0, s.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
textView.text = TextUtils.concat(s, s)
The text shown in textView is: hellohello
Why aren't both "hello" in bold?
Edit: I am actually trying to concatenate some SpannableStrings with some normal Strings, something like TextUtils.concat("A", s, "B", s, "C") (the result of which I expect to be AhelloBhelloC). Replacing SPAN_INCLUSIVE_EXCLUSIVE with SPAN_INCLUSIVE_INCLUSIVE solves the "hellohello" problem but not this one.
https://developer.android.com/reference/android/text/TextUtils#concat(java.lang.CharSequence...):
If there are paragraph spans in the source CharSequences that satisfy paragraph boundary requirements in the sources but would no longer satisfy them in the concatenated CharSequence, they may get extended in the resulting CharSequence or not retained.
While this isn't super clear (at least to me) I infer that paragraph boundaries are extended if they should be extended. They should be extended if they are INCLUSIVE boundaries meaning if you insert at the beginning you should use Spannable.SPAN_INCLUSIVE_EXCLUSIVE or Spannable.SPAN_INCLUSIVE_INCLUSIVE, if you append at the end, you should use Spannable.SPAN_EXCLUSIVE_INCLUSIVE or Spannable.SPAN_INCLUSIVE_INCLUSIVE.
So use Spannable.SPAN_INCLUSIVE_INCLUSIVE instead of Spannable.SPAN_INCLUSIVE_EXCLUSIVE (I tested it and it works).
With SPAN_EXCLUSIVE_INCLUSIVE:
With SPAN_INCLUSIVE_INCLUSIVE:
After some digging I found TextUtils is using SpannableStringBuilder.append which is using SpannableStringBuilder.replace (https://developer.android.com/reference/android/text/SpannableStringBuilder#replace(int,%20int,%20java.lang.CharSequence,%20int,%20int)) under the hood and the replace is considered an insert at the end. With SPAN_EXCLUSIVE_INCLUSIVE it would not expand the formatting while it would with SPAN_INCLUSIVE_INCLUSIVE:
If the source contains a span with Spanned#SPAN_PARAGRAPH flag, and it does not satisfy the paragraph boundary constraint, it is not retained.
I also checked the resulting span and printed the start/end of the StyleSpan. The first line with SPAN_EXCLUSIVE_INCLUSIVE the last line with SPAN_INCLUSIVE_INCLUSIVE:
01-14 12:44:11.218 5017 5017 E test : span start/end: 0/5
01-14 12:44:22.575 5079 5079 E test : span start/end: 0/10
If you append text to a Spannable it doesn't matter whether that text is just plain text or another Spannable. Spans in the original texts will either expand or not depending on the span's flags (Spannable.SPAN_xyz_INCLUSIVE vs Spannable.SPAN_xyz_EXCLUSIVE). That's why the bold formatting will either expand to the whole string with TextUtils.concat("A", s, "B", s, "C") (using Spannable.SPAN_xyz_INCLUSIVE) or not expand at all (using Spannable.SPAN_xyz_EXCLUSIVE).
Your attempt to append an already formatted Spannable won't work because a span can be used just once.
That's why this:
val s = SpannableStringBuilder("hellohello")
val span = StyleSpan(Typeface.BOLD)
s.setSpan(span, 0, 5, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
s.setSpan(span, 5, 10, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
will result in hellohello, while this:
val s = SpannableStringBuilder("hellohello")
val span1 = StyleSpan(Typeface.BOLD)
val span2 = StyleSpan(Typeface.BOLD)
s.setSpan(span1, 0, 5, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
s.setSpan(span2, 5, 10, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
will result in hellohello
This answers your question:
Why aren't both "hello" in bold?
Unfortunately your question isn't very specific on what you're actually trying to achieve. If it's simply to have a Spannable with AhelloBhelloC, then you now know how. If your goal is to have a generic solution that allows to append arbitrary Spannable with arbitrary spans and keep those spans (CharacterStyle and ParagraphStyle) then you'll have to find a way to clone arbitrary Spanned texts with arbitrary spans (note if s is a SpannableStringBuilder then s.subSequence(0, s.length) copies spans but doesn't clone them). So here's how I'd do it:
private fun Spannable.clone(): Spannable {
val clone = SpannableStringBuilder(toString())
for (span in getSpans(0, length, Any::class.java)) {
if (span is CharacterStyle || span is ParagraphStyle) {
val st = getSpanStart(span).coerceAtLeast(0)
val en = getSpanEnd(span).coerceAtMost(length)
val fl = getSpanFlags(span)
val clonedSpan = cloneSpan(span)
clone.setSpan(clonedSpan, st, en, fl)
}
}
return clone
}
private fun cloneSpan(span: Any): Any {
return when (span) {
is StyleSpan -> StyleSpan(span.style)
// more clone code to be written...
else -> span
}
}
Then you can do:
val s = SpannableString("hello")
s.setSpan(StyleSpan(Typeface.BOLD), 0, s.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = TextUtils.concat(s, " not bold ", s.clone())
I ended up creating a helper property to make strings bold.
val String.bold: CharSequence
get() = buildSpannedString {
append(this#bold, StyleSpan(Typeface.BOLD), Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
}
I still can't re-use Spannables but probably it's because they can't be re-used (as #Emanuel suggested). But with this new property, the calling code looks pretty clean.
TextUtils.concat("A", "hello".bold, "B", "hello".bold, "C")
prints AhelloBhelloC

Change color of two parts of string when using String.format()

I am using the following to show the remaining time in a word game app.
remainingTime.setText(String.format(Locale.getDefault(),"REMAINING TIME: %d MNS %d SECONDS ",(millisUntilFinished / 1000) / 60 ,(millisUntilFinished / 1000) % 60));
I want to change the color of the minute and second text. How can the colors be defined in their placeholders??
I want it look like this:
If you are using Kotlin you can do it with an Extension Function.
fun TextView.setColouredSpan(word: String, color: Int) {
val spannableString = SpannableString(text)
val start = text.indexOf(word)
val end = text.indexOf(word) + word.length
try {
spannableString.setSpan(ForegroundColorSpan(color), start, end,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
text = spannableString
} catch (e: IndexOutOfBoundsException) {
println("'$word' was not not found in TextView text")
}
}
Use it after you have set your text to the TextView like so
private val blueberry by lazy { getColor(R.color.blueberry) }
textViewTip.setColouredSpan("Warning", blueberry)
I did it. What I did was find the numbers using regex and used
Spannable in the while loop of the matches.find() to color the index of the matches. The start was the index and the end index + 2.
You can use the following method to get the Digits in a different color
public SpannableString getColoredString(String string, int color){
SpannableString spannableString = new SpannableString(string);
for(int i = 0; i < string.length(); i++){
if(Character.isDigit(string.charAt(i))){
spannableString.setSpan(new ForegroundColorSpan(color), i, i+1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
}
return spannableString;
}
In your Activity code use the above function and set the return value to the textview
String text = String.format(Locale.getDefault(),"REMAINING TIME: %d MNS %d SECONDS ",(millisUntilFinished / 1000) / 60 ,(millisUntilFinished / 1000) % 60);
SpannableString string = getColoredString(text, Color.YELLOW);
remainingTime.setText(string);
In my case I had some sort of "some text email#test.com some text".
Where email#test.com had to be with different color.
I tried:
<string name="myText">
some text
<font color="#FFFFFF">%1$s</font>
some text
</string>
But it looks like after getString, HtmlCompat ignores "font color", so that's why I used symbols "<" and ">" as usual symbols like that:
<string name="myText">
some text
<font color="#FFFFFF"> %1$s</font>
some text
</string>
After that everything showed up nicely as I needed:
val email = "email#test.com"
val text = getString(R.string.myText, email)
binding.textView.text = HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY)

How to have a SpannableStringBuilder append a span that's inside a formatted string?

Background
Suppose I use SpannableStringBuilder to append multiple stuff into it, and one of them is string that I format from the strings.xml file, which has a span inside:
SpannableStringBuilder stringBuilder = new SpannableStringBuilder ();
stringBuilder.append(...)...
final SpannableString span = new SpannableString(...);
span.setSpan(new BackgroundColorSpan(0xff990000), ...,...,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
stringBuilder.append(getString(R.string.string_to_format, span));
stringBuilder.append(...)...
textView.setText(stringBuilder);
The problem
Sadly, formatting such a string removes the span itself, so in my case, there won't be any text with a background color.
This happens on the line of the "getString".
What I've tried
If I just append the span alone (without "getString"), it works fine.
I also tried to investigate Html.fromHtml, but it doesn't seem to support a background color for text anyway.
The question
Is it possible to format a string that has a span, yet still have the span within?
More specifically, the input is a string A from the strings.xml file, which only has a placeholder (no special HTML tags), and another string B that is supposed to replace the placeholder at runtime. The string B should have a highlight for a partial text of itself.
In my case, the highlighted text is a something to search for within string B.
OK, I've found an answer to my special end case, but I'd still like to know if there are better ways.
Here's what I did:
String stringToSearchAt=...
String query=...
int queryIdx = stringToSearchAt.toLowerCase().indexOf(query);
stringToSearchAt= stringToSearchAt.substring(0, queryIdx + query.length()) + "<bc/>" + stringToSearchAt.substring(queryIdx + query.length());
final String formattedStr=getString(..., stringToSearchAt);
stringBuilder.append(Html.fromHtml(formattedStr, null, new TagHandler() {
int start;
#Override
public void handleTag(final boolean opening, final String tag, Editable output, final XMLReader xmlReader) {
switch (tag) {
case "bc":
if (!opening)
start = output.length() - query.length();
break;
case "html":
if (!opening)
output.setSpan(new BackgroundColorSpan(0xff00bbaa), start, start + query.length(), 0);
}
}
}));
This is only good for my case, but in the case of general formatting, this won't suffice.
format a spanned string may be impossible, because it still use String.format() to format a String finilly, it's a Java API, and Span is Android API.
But I think you can use html string instead. Look at this document Styling with HTML markup.
for example:
String str = "Hi <strong><font color=\"#00bbaa\">%s</font></strong>, Welcome to <em><font color=\"#FF4081\">%s</font></em>";
String text = String.format(str, "Lucy", "Android");
Spanned spanned = Html.fromHtml(text);
// after Html.fromHtml(), you can still change the Span
SpannableString spannableString = new SpannableString(spanned);
spannableString.setSpan(new BackgroundColorSpan(0xff990000), 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
textView.setText(spannableString);
the result
if you want to put the string in the string.xml, you may need to change '<' to '<', '%s' to '%1$s'.
<string name="from_offical">Hello <strong><font color="#00bbaa">%1$s</font></strong>, Welcome to <em><font color="#00bbaa">%2$s</font></em></string>
This is an old question, but I am posting a more general solution than the accepted answer.
Reference is made to the following string resources:
<string name="string_1"><b>Bolded</b> <abc>Not bolded</abc></string>
<string name="string_2"><font bgcolor="red">Background red</font> No background color.</string>
<string name="string_3">The <b>capital</b> of %1 is %2\n%2 is the capital of %1.</string>
Android stores string resource tags separately from strings. Tags will always be consumed when read into an app.
var s1 = getString(R.string.string_1)
var s2 = getText(R.string.string_1)
s1 placed in a TextView will show "Bolded Not bolded" while s2 in a TextView will show "Bolded Not bolded". The tag "<abc>" has no interpretation, so it is lost.
If the background color is known at compile time then we can do the following:
textView.text = getText(R.string.string_2)
will display:
Of some interest is that while the font tag is supported by the Android framework and the HTML translation class (Html .java), the Html.java implementation does not support the bgcolor attribute, so the following
var s4 = "<font bgcolor=\"red\">Background red</font> No background color."
textView.text = HtmlCompat.fromHtml(s4, FROM_HTML_MODE_LEGACY)
will not display the background color.
If the formatting is indeterminate at compile time, then we must do a little more work. Replacing string arguments with spanned text using getString(string_id, varargs) fails as the OP notes. What is an alternative?
One way is to read a string in with placeholders intact.
getString(R.string.string_3) will produce the string "The capital of %1 is %2\n%2 is the capital of %1.". We could then search for "%1", "%2", etc. and make the replacements with spanned text. In this case, the placeholder identifiers could be any unique set of characters.
It may be better, however, to use getText(R.string.string_3) which will interpret any HTML codes supported by the framework.
The following code shows hot to make substitutions of spanned text into string_3. The spanned text that will be substituted simply has the first letter highlighted.
textView.text = SpanFormatter.getText(this, R.string.string_3, { Int -> getArg(Int) })
private fun getArg(argNum: Int) =
when (argNum) {
1 -> { // Get the country with a highlighted first character.
SpannableString("France").apply {
setSpan(
BackgroundColorSpan(0x55FF0000),
0,
1,
SpannedString.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
2 -> { // Get the capital city with a highlighted first character.
SpannableString("Paris").apply {
setSpan(
BackgroundColorSpan(0x550000FF),
0,
1,
SpannedString.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
else -> throw IllegalArgumentException("$argNum is a bad argument number.")
}
SpanFormatter.kt
object SpanFormatter {
private const val END_OF_STRING = -1
private const val SPAN_FLAGS = SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
fun getText(
context: Context,
#StringRes stringId: Int,
argFactory: (Int) -> CharSequence,
argStartChar: Char = '%'
) = getText(context.getText(stringId), argFactory, argStartChar)
fun getText(
cs: CharSequence,
argFactory: (Int) -> CharSequence,
argStartChar: Char = '%'
): CharSequence {
// Mark all areas of substitution with an ArgNum span.
val sb = SpannableStringBuilder(cs)
var pos = sb.indexOf(argStartChar, 0)
while (pos != END_OF_STRING) {
var argEnd = pos + 1
while (argEnd < sb.length && sb[argEnd].isDigit()) ++argEnd
if (argEnd - pos > 1) {
val argnum = sb.substring(pos + 1, argEnd).toInt()
check(argnum > 0) {
"Incorrect argument number (%d) which must greater than zero.\nString: %s".format(
argnum
)
}
sb.setSpan(ArgMark(argnum), pos, argEnd, SPAN_FLAGS)
}
pos = sb.indexOf(argStartChar, argEnd)
}
// Replace all ArgMark spans with the appropriate substitution text.
val argMarkSpans = sb.getSpans<ArgMark>(0, sb.length)
argMarkSpans.forEach { argMarkSpan ->
val start = sb.getSpanStart(argMarkSpan)
val end = sb.getSpanEnd(argMarkSpan)
sb.replace(start, end, argFactory(argMarkSpan.argNum))
sb.removeSpan(argMarkSpan)
}
return sb
}
private data class ArgMark(val argNum: Int)
}
The foregoing displays:
And a simpler way without the use of the marking spans which aren't really needed:
SpanFormatter.kt
object SpanFormatter {
private const val END_OF_STRING = -1
fun getText(
context: Context,
#StringRes stringId: Int,
argFactory: (Int) -> CharSequence,
argStartChar: Char = '%'
) = getText(context.getText(stringId), argFactory, argStartChar)
fun getText(
cs: CharSequence,
argFactory: (Int) -> CharSequence,
argStartChar: Char = '%'
): CharSequence {
val sb = SpannableStringBuilder(cs)
var argStart = sb.indexOf(argStartChar, 0)
while (argStart != END_OF_STRING) {
var argEnd = argStart + 1
while (argEnd < sb.length && sb[argEnd].isDigit()) ++argEnd
if (argEnd - argStart > 1) {
val argNum = sb.substring(argStart + 1, argEnd).toInt()
argFactory(argNum).apply {
sb.replace(argStart, argEnd, this)
argEnd = argStart + length
}
}
argStart = sb.indexOf(argStartChar, argEnd)
}
return sb
}
}

Android - Html.fromHtml handle background color

I have html text that I need to display in TextView. The html may look like this -
<font color="#AFEEEE"><font style="background-color: rgb(255,140,0);">Text with background and color</font></font>
Html.fromHtml doesn't support any attribute other than color for font tag. But we absolutely must show the background. I could write a custom tag handler but the attributes are not passed in, only the tag is passed in.
What is the best way to achieve this ?
NOTE : Cant use Webview.
I tried the code below. If I set raw on the text, it works, but if i process it further and pass it to Html.fromHtml, it doesnt show the background.
public static final String sText =
"Background on <font style=\"background-color: rgb(255,255,0);\">pa</font>rt text only";
Pattern pattern = Pattern.compile(BACKGROUND_PATTERN);
Matcher matcher = pattern.matcher(sText);
SpannableString raw = new SpannableString(sText);
BackgroundColorSpan[] spans =
raw.getSpans(0, raw.length(), BackgroundColorSpan.class);
for (BackgroundColorSpan span : spans) {
raw.removeSpan(span);
}
while (matcher.find()) {
raw.setSpan(new BackgroundColorSpan(0xFF8B008B),
matcher.start(2), matcher.start(2) + matcher.group(2).length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
sText = raw.toString();
final Spanned convertedHtml =
Html.fromHtml(sText, ig, new myTagHandler());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
String str = "<span style=\"background-color:#f3f402;\">" + TEXT TO HIGHLIGHT + "</span>";
textView.setText(Html.fromHtml(str, Html.FROM_HTML_MODE_LEGACY));
} else {
String str = "<font color='#f3f402'>" + TEXT TO HIGHLIGHT + "</font>";
textView.setText(Html.fromHtml(str));
}
More - https://stackoverflow.com/a/46035856/3625510
Add your own BackgroundColorSpan as you see fit.
Here is some code that sets such a span on all occurrences of a search term within a TextView:
private void searchFor(String text) {
TextView prose=(TextView)findViewById(R.id.prose);
Spannable raw=new SpannableString(prose.getText());
BackgroundColorSpan[] spans=raw.getSpans(0,
raw.length(),
BackgroundColorSpan.class);
for (BackgroundColorSpan span : spans) {
raw.removeSpan(span);
}
int index=TextUtils.indexOf(raw, text);
while (index >= 0) {
raw.setSpan(new BackgroundColorSpan(0xFF8B008B), index, index
+ text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
index=TextUtils.indexOf(raw, text, index + text.length());
}
prose.setText(raw);
}
So, find your beginning and ending points, create a BackgroundSpan with your desired color, and use setSpan() to apply it.
Note that this assumes that only part of your text needs the background color. If the entire TextView needs the color, go with njzk2's suggestion, and just apply the color to the whole TextView.

Detect changes in EditText (TextWatcher ineffective)

I need to detect text changes in an EditText. I've tried TextWatcher, but it doesn't work in a way I would expect it to. Take the onTextChanged method:
public void onTextChanged( CharSequence s, int start, int before, int count )
Say I have the text "John" in already in the EditText. If press another key, "e", s will be "Johne", start will be 0, before will be 4, and count will be 5. The way I would expect this method to work would be the difference between what the EditText previously was, and what it's about to become.
So I would expect:
s = "Johne"
start = 4 // inserting character at index = 4
before = 0 // adding a character, so there was nothing there before
count = 1 // inserting one character
I need to be able to detect individual changes every time a key is pressed. So if I have text "John", I need to know "e" was added at index 4. If I backspace "e", I need to know "e" was removed from index 4. If I put the cursor after "J" and backspace, I need to know "J" was removed from index 0. If I put a "G" where "J" was, I want to know "G" replaced "J" at index 0.
How can I achieve this? I can't think of a reliable way to do this.
Use a textwatcher and do the diff yourself. store the previous text inside the watcher, and then compare the previous text to whatever sequence you get onTextChanged. Since onTextChanged is fired after every character, you know your previous text and the given text will differ by at most one letter, which should make it simple to figure out what letter was added or removed where. ie:
new TextWatcher(){
String previousText = theEditText.getText();
#Override
onTextChanged(CharSequence s, int start, int before, int count){
compare(s, previousText); //compare and do whatever you need to do
previousText = s;
}
...
}
The best approach you can follow to identify text changes.
var previousText = ""
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
previousText = s.toString()
}
override fun onTextChanged(newText: CharSequence?, start: Int, before: Int, count: Int) {
val position = start + before ;
if(newText!!.length > previousText.length){ //Character Added
Log.i("Added Character", " ${newText[position]} ")
Log.i("At Position", " $position ")
} else { //Character Removed
Log.i("Removed Character", " ${previousText[position-1]} ")
Log.i("From Position", " ${position-1} ")
}
}
override fun afterTextChanged(finalText: Editable?) { }
You need to store and update the previous CharSequence every time the text is changed. You can do so by implementing the TextWatcher.
Example:
final CharSequence[] previousText = {""};
editText.addTextChangedListener(new TextWatcher()
{
#Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2)
{
}
#Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2)
{
if(i1 > 0)
{
System.out.println("Removed Chars Positions & Text:");
for(int index = 0; index < i1; index++)
{
System.out.print((i + index) + " : " + previousText[0].charAt(i + index)+", ");
}
}
if(i2 > 0)
{
System.out.println("Inserted Chars Positions & Text:");
for(int index = 0; index < i2; index++)
{
System.out.print((index + i) + " : " + charSequence.charAt(i + index)+", ");
}
System.out.print("\n");
}
previousText[0] = charSequence.toString();//update reference
}
#Override public void afterTextChanged(Editable editable)
{
}
});
I faced the exact same problem recently and I wrote my own custom algorithm to detect the diff from the TextWatcher output.
Algorithm -
We store 4 things -
Old selection size
Old text
Old text sequence before the cursor/selection.
Old text sequence after the cursor/selection.
Above 4 things are updated during the beforeTextChanged() callback.
Now during the onTextChanged() callback, we compute following two things -
New text sequence before the cursor/selection.
New text sequence after the cursor/selection.
Now following cases are possible -
Case 1
New text sequence before the cursor/selection == Old text sequence before the cursor/selection AND New text sequence after the cursor/selection isASuffixOf Old text sequence after the cursor/selection
This is a delete forward case. The number of deleted characters can be calculated by the oldText length minus the newText length.
Example -
Old text = Hello wo|rld (| represents the cursor)
Old text sequence before the cursor/selection = Hello wo
Old text sequence after the cursor/selection = rld
Old selection size = 0
New text = Hello wo|ld (| represents the cursor)
New text sequence before the cursor/selection = Hello wo
New text sequence after the cursor/selection = ld
Clearly, this is a case of delete in forward direction by 1 character.
Case 2
New text sequence after the cursor/selection == Old text sequence after the cursor/selection AND New text sequence before the cursor/selection isAPrefixOf Old text sequence before the cursor/selection
This is a delete backward case. The number of deleted characters can be calculated by the oldText length minus the newText length.
Example -
Old text = Hello wo|rld (| represents the cursor)
Old text sequence before the cursor/selection = Hello wo
Old text sequence after the cursor/selection = rld
Old selection size = 0
New text = Hello w|rld (| represents the cursor)
New text sequence before the cursor/selection = Hello w
New text sequence after the cursor/selection = rld
Clearly, this is a case of delete in backward direction by 1 character.
Case 3
New text sequence after the cursor/selection == Old text sequence after the cursor/selection AND Old text sequence before the cursor/selection isAPrefixOf New text sequence before the cursor/selection
This is an insert case. The exact insertion string can be calculated by removing the old text sequence from cursor + old text sequence after cursor from the new text string.
Example -
Old text = Hello wo|rld (| represents the cursor)
Old text sequence before the cursor/selection = Hello wo
Old text sequence after the cursor/selection = rld
Old selection size = 0
New text = Hello wo123|rld (| represents the cursor)
New text sequence before the cursor/selection = Hello wo123
New text sequence after the cursor/selection = rld
Clearly, this is a case of insert and inserted string is 123.
Case 4
If none of the above cases are satisfied, then we can say that it is a replace case. And the replace data is already provided by TextWatcher in the onTextChanged callback.
Here is the code for above algorithm -
class MyTextWatcher : android.text.TextWatcher {
var oldSelectionSize = 0
var oldText: String = ""
var oldSequenceBeforeCursor: String = ""
var oldSequenceAfterCursor: String = ""
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
oldSelectionSize = editText.selectionEnd - editText.selectionStart
oldText = s.toString()
oldSequenceBeforeCursor = s?.subSequence(0, editText.selectionStart).toString()
oldSequenceAfterCursor = s?.subSequence(editText.selectionEnd, s.length).toString()
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
s?.toString()?.let { newText ->
val newSequenceBeforeCursor = newText.subSequence(0, selectionStart).toString()
val newSequenceAfterCursor = newText.subSequence(selectionEnd, newText.length)
.toString()
if (newSequenceBeforeCursor == oldSequenceBeforeCursor &&
oldSequenceAfterCursor.endsWith(newSequenceAfterCursor))
// handle delete forward
// number of characters to delete ==>
// if(oldSelectionSize > 0) then deleted number of characters = oldSelectionSize
// else number of characters to delete = oldText.length - newText.length
else if (newSequenceAfterCursor == oldSequenceAfterCursor &&
oldSequenceBeforeCursor.startsWith(newSequenceBeforeCursor))
// handle delete backward
// number of characters to delete ==>
// if(oldSelectionSize > 0) then deleted number of characters = oldSelectionSize
// else number of characters to delete = oldText.length - newText.length
else if (newSequenceAfterCursor == oldSequenceAfterCursor &&
newSequenceBeforeCursor.startsWith(oldSequenceBeforeCursor))
// handle insert
// inserted string = (newText - oldSequenceBeforeCursor) - oldSequenceAfterCursor
else
// handle replace
// replace info already provided in `onTextChanged()` arguments.
}
}
override fun afterTextChanged(s: Editable?) {
}
}

Categories

Resources