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?) {
}
}
Related
Sorry for the big amount of code guys. Im at a loss lol. I needed a way in an EditText to overwrite chars, but skip the two spaces when the curser comes to them. So that spaces will be "Permanent" in a sense. This is for a basic hexadecimal editor style text box. While this somewhat does work, an when it does its slick. But it seems to have some flaw I am missing, Sometimes when you re typing it will start inserting characters, specifically when the curser is in between two chars ex this is before : "01 02 0|3 04 05" if you type 5 it should replace the 3, then skip over the spaces and end up at the next 0 But it ends up either one of two things, either inserting "01 02 05|3 04 05" or overwriting the 3, and removing one of the two space while jumping "01 02 05 |04 05". lastly it used to sometimes replace a space when the curser was next to a pace but didn't jump over the two spaces, I believe I have worked this out but I've been working on the other problems so I may have been blinded a bit and not noticed it. I'm guessing my text watcher is not being invoked by either formating var not returning to false, or some other thing I've overlooked. Because once curser is moved IE you touch somewhere else in the text, it begins to work briefly till it ends up inserting in between digits again. Anyone see anything I may have missed?
Here is the code so far:
class CustomEditText : AppCompatEditText {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private var isFormatting = false
private var mStart = -1 // Start position of the Text being modified
private var mEnd = -1 // End position of the Text being modified
private val watcher = object : TextWatcher {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
mStart = -1
// Keep track of the start and end position of the text change
if (before == 0) {
// The start and end variables have not been set yet, and there are no characters being deleted.
// Set the start position to the current cursor position.
mStart = start + count
// Set the end position to either the end of the string or the current cursor position + count.
mEnd = min(mStart + count, s!!.length)
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// No action needed before text change
}
override fun afterTextChanged(s: Editable?) {
// Avoid infinite recursion if the TextWatcher is modifying the text
if (!isFormatting) {
if (mStart >= 0) {
// Set `isFormatting` to true to indicate that we're currently formatting the text.
isFormatting = true
// Check if the current position is a digit
if (Character.isDigit(s!![mStart - 1])) {
// Check if the next position is a space or the end of the string
if (mStart < s.length && s[mStart] == ' ' || mStart == s.length) {
// If the next position is a space or the end of the string, insert the digit at the next position
s.insert(mStart, s[mStart - 1].toString())
mStart++
} else {
// Overwrite the text at the current position
s.replace(mStart, mEnd, "")
}
} else if (s[mStart - 1] == ' ') {
// Check if the next position is a digit
if (mStart + 1 < s.length && Character.isDigit(s[mStart + 1])) {
// Jump over the spaces and overwrite the first character in the next set
mStart = s.indexOf(" ", mStart) + 2
s.replace(mStart, mStart + 1, "")
} else {
// Overwrite the text at the current position
s.replace(mStart, mEnd, "")
}
} else {
// Overwrite the text at the current position
s.replace(mStart, mEnd, "")
}
isFormatting = false
}
}
}
}
init {
// Initiate and add the text change listener "watcher"
addTextChangedListener(watcher)
}
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
// Get the current text in the EditText
val text = text
if (text != null) {
val len = text.length
// If the selection start and end are equal, meaning the cursor is at a certain position
if (selStart == selEnd) {
// Check if the cursor is positioned at a space between two hexadecimal digits
// And if the character after the space is also a space
if (selStart != 0 && selStart < len && text[selStart - 1] == ' ' && text[selStart + 1] == ' ') {
// Move the cursor one position to the right to position it at the start of the next hexadecimal digit
setSelection(selStart + 1)
return
}
// Check if the cursor is positioned at a space and the character after the space is not a space
if (selStart < len && text[selStart] == ' ' && (selStart + 1 >= len || text[selStart + 1] != ' ')) {
// Move the cursor one position to the right to position it at the start of the next hexadecimal digit
setSelection(selStart + 1)
}
}
}
// Call the superclass implementation of onSelectionChanged
super.onSelectionChanged(selStart, selEnd)
}
}
I've also toyed with using drawables for the spaces, I even thought that maybe if I make a custom drawable similar to a tictactoe board if you will, and have the digits in between the drawable to achieve the same result. I know either way I still have to handle backspaces and the arrow key movement in the even the user is using a keyboard, but that's a 3 day debug session for another time. If anyone has any ideas or see anything I missed that would be awesome, Or if you think this approach is not the best. I tired many different ways to approach this but this got the closest result to working. I do feel as though a drawable may be much more resource intensive than a text watcher, albeit this would be as well with larger files, but that can all be solved down the road. This is allot, and I don't expect a magical fix. But more eyes on a project might be able to spot what I've missed, thank you for your time =)
EDIT-----------
So it seems this is a buggy way to go about this type of use case, and very unreliable. Ive started working on instead overriding the onDraw method in EditText to simply draw the text in the positions. Hoping this isn't too resource intensive as I haven't ran the code on hardware yet to see but at any rate I'm assuming it will stay in place when edited and be pretty resource efficient as compared to other methods I've tried(Some even an S22 ultra had a hard time with). Then it's simply implementing overwrite mode. Which i have already done. Hopefully this pans out. If anyone has a better idea or if the above code can be made more reliable I would still love to see it! For now my efforts are going towards onDraw.
In my Android App I've created 8 TextViews stacked on top of each other. Now I want to load in some plain text into those TextView-Lines. At the moment my Strings have a ";" as delimiter to indicate a line break, however it would be much more convenient if I would detect a linebreak automatically instead of using the hardcoded semicolon approach.
This is my String at the moment:
myString = "" +
"This seems to be some sort of spaceship,;" +
"the designs on the walls appear to be of;" +
"earth origin. It looks very clean here.;"
And in my other class I load in this string into the 8 TextViews, which I've loaded into an ArrayList, using the ";" as a delimiter.
public fun fillLines(myString: String) {
// How To Make Line Breaks Automatic??
for(i: Int in str until myString.split(";").size) {
if(i > textViewArray.size - 1) {
break
}
textViewArray[i].text = myString.split(";")[i]
textViewArray[i].alpha = 1.0f
}
}
Is there any way I can get the same result as shown above but without hardcoding the delimiter as ";" but instead somehow automatically detect the line break which would occur inside the TextView and then use this as a delimiter to advance through all 8 TextView "Lines".
The reason I need 8 TextViews Stacked On top of each other as individual "text lines" is because of an animation technique I want to use.
Line-breaking gets fairly complicated, so my recommendation would be that you allow a TextView to perform the measuring and layout to determine the line breaks. You could have an invisible TextView with the same style as your other views, and attach it to the layout so that it has the same width as your individual TextView instances. From there, add a layout change listener, and you can then retrieve the individual lines from the TextView Layout:
myTextView.text = // your text string here
myTextView.addOnLayoutChangeListener { view, _, _, _, _, _, _, _, _ ->
(view as? TextView)?.layout?.let { layout ->
// Here you'll have the individual broken lines:
val lines = (0 until layout.lineCount).map {
layout.text.subSequence(layout.getLineStart(it), layout.getLineVisibleEnd(it)
}
}
}
That said, this comes with the caveat that you'll lose out on hyphenation provided by the TextView, so you may wish to disable hyphenation entirely in your case.
You could fill text view with html. Below example.
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
tvDocument.setText(Html.fromHtml(bodyData,Html.FROM_HTML_MODE_LEGACY));
} else {
tvDocument.setText(Html.fromHtml(bodyData));
}
If your delimiter ; it is possible call method replaceAll(";", "<br>");
Ok I got it working now:
First you must add these properties for the textviews:
android:singleLine="true"
android:ellipsize="none"
Then you can do this:
public fun fillStorylines() {
val linecap = 46
var finalLine: String
var restChars = ""
val index = 9999
val text1: String = "" +
"This seems to be some sort of spaceship, " +
"the designs on the walls appear to be of " +
"earth origin. It looks very clean here. "
for(j: Int in 0..index) {
try {
finalLine = ""
val lines: List<String> = (restChars + text1.chunked(linecap)[j]).split(" ")
for (i: Int in 0 until lines.size - 1) {
finalLine += lines[i] + " "
}
textViewArray[j].text = finalLine
textViewArray[j].alpha = 1.0f
restChars = lines[lines.size - 1]
} catch (ex: Exception) {
break
}
}
}
If anyone knows a more elegant way to solve this please go ahead, your feedback is appreciated :)
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
}
}
I am new to Android. I use an iPhone, so I am not just new to the programming, but also to the OS completely. I just started this week and have written a basic notes application.
Now, when I go to the search view, search say "cats", if a result appears and I click to go to that note, I want all instances of "cats" to be highlighted. Then, when I tap in the EditText, I want the highlighting to go away.
It would also be awesome if I could highlight the text within the search view.
Apple's Notes.app does this and I think it really adds to the search functionality. Couldn't find any images to show you what I mean. Hopefully I explained it well enough.
I tried this:
//highlight searched text
//Get the text of the EditText
String text = editText.getText().toString();
//Get indexes of the query in the EditText
int firstIndex = text.indexOf(query);
int lastIndex = text.lastIndexOf(query);
//Highlight the selection
editText.setSelection(firstIndex, lastIndex);
But we run into problems if there are multiple of the same word. Any thoughts?
Selection and highlighting are not the same thing. Usually, selecting something also highlights it, but you don't highlight something by selecting it. Besides, Android does not support multiple selection in EditText.
To highlight, you need to apply a CharacterStyle to the range of text, such as a BackgroundColorSpan.
This sample project applies a BackgroundColorSpan to highlight search results in a TextView, using:
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);
}
The code shown first removes any existing BackgroundColorSpan instances, then applies new ones based on a search string.
Since EditText inherits from TextView, the same basic concept would apply here. However, IMHO, doing this sort of highlighting in an EditText will be foreign to users. I'd show the search results in a plain TextView, with an "edit" action bar item or something to move into editing mode.
I did some googling and looked searching "java" instead of "android" was helpful. Below is my working code:
private void highlightIndexes(String query){
String text = editText.getText().toString();
Map<Integer, Integer> indexMap = getIndexes(query);
Spannable spannable=new SpannableString(text);
Iterator<Integer> keySetIterator = indexMap.keySet().iterator();
while(keySetIterator.hasNext()){
Integer key = keySetIterator.next();
Integer value = indexMap.get(key);
spannable.setSpan(new ForegroundColorSpan(Color.BLUE), key, value, 0);
}
editText.setText(spannable);
}
private Map<Integer, Integer> getIndexes(String query){
Map<Integer, Integer> indexMap = new TreeMap<Integer, Integer>();
int queryLength = query.length();
query = query.substring(0, (queryLength -1)).toLowerCase(Locale.US);
String text = editText.getText().toString().toLowerCase(Locale.US);
int i, y;
i = text.indexOf(query);
y = i + queryLength - 1;
indexMap.put(i, y);
while(i >= 0) {
i = text.indexOf(query, i+1);
y = i + queryLength - 1;
if (i != -1 && y != -1){
indexMap.put(i, y);
}
}
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)
}