Hot questions for Using Android EditText in spannablestring

Question:

I am trying to get All Spans applied to text as below;

public String getTextWithTags(Editable e)
{

    StyleSpan[] ss = e.getSpans(0,e.length(),StyleSpan.class);
    ss[0].getSpanStart <--- ? This is the problem, no such function

    return "";
}

But there is no index find function to replace tags to store them on database so i can retrieve all spans back when i reopen text. How can i get all span positions from editable object?


Answer:

StyleSpan[] ss = e.getSpans(0,e.length(),StyleSpan.class);

for(StyleSpan span : ss){
    int start = e.getSpanStart(span);
    int end = e.getSpanEnd(span);
}

Question:

Currently I'm developing Spelling & Grammar checking app. It has EditText where user can input text & one button called "Check Text " upon click on button app will call LanguageTool API to check text & returns JSON response with result .

Here is screenshot of app :

Here is code which I have tried so far for highlighting multiple words but this code only highlights last word from array which I have created:

for (int i = 0; i < errorStrings.size(); i++) {

// Here textToCheck is EditText & errorStrings is ArrayList of type WrongString class which i have created to hold Error string , offset & length.

Spannable wordtoSpan = new SpannableString(texttoSend);
wordtoSpan.setSpan(new BackgroundColorSpan(Color.BLUE),errorStrings.get(i).getOffset(),
                                        (errorStrings.get(i).getOffset()+errorStrings.get(i).getLength()), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                            textToCheck.setText(wordtoSpan);
}

Answer:

I have wrote a simple method that allow you pass TextView (or child classes Button, Edittext etc.).

1. If you want to highlight a text like in find word in paragraph. You can use below method like.

setHighLightedText(yourTextView_Edittext_Button, "a");

Which gives you result like this.

    /**
     * use this method to highlight a text in TextView
     * @param tv TextView or Edittext or Button or child of TextView class
     * @param textToHighlight Text to highlight
     */
    public void setHighLightedText(TextView tv, String textToHighlight) {
        String tvt = tv.getText().toString();
        int ofe = tvt.indexOf(textToHighlight, 0);
        Spannable wordToSpan = new SpannableString(tv.getText());
        for (int ofs = 0; ofs < tvt.length() && ofe != -1; ofs = ofe + 1) {
            ofe = tvt.indexOf(textToHighlight, ofs);
            if (ofe == -1)
                break;
            else {
                wordToSpan.setSpan(new BackgroundColorSpan(0xFFFFFF00), ofe, ofe + textToHighlight.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                tv.setText(wordToSpan, TextView.BufferType.SPANNABLE);
            }
        }
    }

2. If you want make clickable highlighted text (like click on terms & condition text) then use this code as below:

 setClickableHighLightedText(yourTextView_Edittext_Button, "go to settings", new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // TODO: do your stuff here 
        }
    });

Which gives you result like

/**
 * use this method to set clickable highlighted a text in TextView
 *
 * @param tv              TextView or Edittext or Button or child of TextView class
 * @param textToHighlight Text to highlight
 */
public void setClickableHighLightedText(TextView tv, String textToHighlight, View.OnClickListener onClickListener) {
    String tvt = tv.getText().toString();
    int ofe = tvt.indexOf(textToHighlight, 0);
    ClickableSpan clickableSpan = new ClickableSpan() {
        @Override
        public void onClick(View textView) {
            if (onClickListener != null) onClickListener.onClick(textView);
        }

        @Override
        public void updateDrawState(TextPaint ds) {
            super.updateDrawState(ds);
            ds.setColor(0xff0000ff);
            ds.setUnderlineText(true);
        }
    };
    SpannableString wordToSpan = new SpannableString(tv.getText());
    for (int ofs = 0; ofs < tvt.length() && ofe != -1; ofs = ofe + 1) {
        ofe = tvt.indexOf(textToHighlight, ofs);
        if (ofe == -1)
            break;
        else {
            wordToSpan.setSpan(clickableSpan, ofe, ofe + textToHighlight.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            tv.setText(wordToSpan, TextView.BufferType.SPANNABLE);
            tv.setMovementMethod(LinkMovementMethod.getInstance());
        }
    }
}

This is a workaround, you can customise spans according to your need. Some good tutorials Android text styles and one other

Question:

note: I've created a GitHub repo containing the repro for this bug here. Feel free to clone and try the app out yourself to see the bug. The relevant code is here: with the commented part kept in a comment it works fine, uncomment it and you will experience the bug.


I'm building a source code editor app for Android. I have a custom Editable type that wraps SpannableStringBuilder (which will henceforth be referred to as SSB). Here is its code:

package com.bluejay.myapplication;

import android.text.Editable;
import android.text.InputFilter;
import android.text.SpannableStringBuilder;

public class ColoredText implements Editable {
    private final SpannableStringBuilder builder;

    public ColoredText(String rawText) {
        assert rawText != null;
        this.builder = new SpannableStringBuilder(rawText);
    }

    @Override
    public Editable replace(int st, int en, CharSequence source, int start, int end) {
        this.builder.replace(st, en, source, start, end);
        return this;
    }

    @Override
    public Editable replace(int st, int en, CharSequence text) {
        this.builder.replace(st, en, text);
        return this;
    }

    @Override
    public Editable insert(int where, CharSequence text, int start, int end) {
        this.builder.insert(where, text, start, end);
        return this;
    }

    @Override
    public Editable insert(int where, CharSequence text) {
        this.builder.insert(where, text);
        return this;
    }

    @Override
    public Editable delete(int st, int en) {
        this.builder.delete(st, en);
        return this;
    }

    @Override
    public Editable append(CharSequence text) {
        this.builder.append(text);
        return this;
    }

    @Override
    public Editable append(CharSequence text, int start, int end) {
        this.builder.append(text, start, end);
        return this;
    }

    @Override
    public Editable append(char text) {
        this.builder.append(text);
        return this;
    }

    @Override
    public void clear() {
        this.builder.clear();
    }

    @Override
    public void clearSpans() {
        this.builder.clearSpans();
    }

    @Override
    public void setFilters(InputFilter[] filters) {
        this.builder.setFilters(filters);
    }

    @Override
    public InputFilter[] getFilters() {
        return this.builder.getFilters();
    }

    @Override
    public void getChars(int start, int end, char[] dest, int destoff) {
        this.builder.getChars(start, end, dest, destoff);
    }

    @Override
    public void setSpan(Object what, int start, int end, int flags) {
        this.builder.setSpan(what, start, end, flags);
    }

    @Override
    public void removeSpan(Object what) {
        this.builder.removeSpan(what);
    }

    @Override
    public <T> T[] getSpans(int start, int end, Class<T> type) {
        return this.builder.getSpans(start, end, type);
    }

    @Override
    public int getSpanStart(Object tag) {
        return this.builder.getSpanStart(tag);
    }

    @Override
    public int getSpanEnd(Object tag) {
        return this.builder.getSpanEnd(tag);
    }

    @Override
    public int getSpanFlags(Object tag) {
        return this.builder.getSpanFlags(tag);
    }

    @Override
    public int nextSpanTransition(int start, int limit, Class type) {
        return this.builder.nextSpanTransition(start, limit, type);
    }

    @Override
    public int length() {
        return this.builder.length();
    }

    @Override
    public char charAt(int index) {
        return this.builder.charAt(index);
    }

    @Override
    public CharSequence subSequence(int start, int end) {
        return this.builder.subSequence(start, end);
    }
}

As you can see, this type is a simple wrapper for SSB. new ColoredText(str) creates the underlying SSB from str, and all of its method calls (with the exception of append, delete, etc. which return this instead of the SSB) simply forward to the SSB.

Now when I have an EditText and I try to set the ColoredText as the underlying text of the EditText, like so

EditText editText = (EditText) findViewById(R.id.editText);
// By default, setText() will attempt to copy the passed CharSequence into a new SSB.
// See https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/TextView.java#L4396
// and https://github.com/android/platform_frameworks_base/blob/master/core/java/android/text/Editable.java#L143
// I want to prevent this and have the ColoredText instead of an SSB be the EditText's
// underlying text, that is, I want the mText member to be of type ColoredText.
editText.setEditableFactory(new Editable.Factory() {
    @Override
    public Editable newEditable(CharSequence source) {
        return (Editable) source; // source is ColoredText
    }
});

ColoredText text = new ColoredText("Hello world!\nHello world again!");
editText.setText(text, TextView.BufferType.EDITABLE);

The EditText will behave quite glitchy when edited. In the above example, tap anywhere on the first line with Hello world! and start typing random characters. The second line will be affected, and somehow (even if you don't touch a newline or arrow keys) the cursor will eventually spill over into the second line. And some of the chars you type may not get displayed, even though the cursor will move.

Now if you comment out the setEditableFactory part, so the text is copied into an SSB during setText(), and you run the app again, you will see there are no glitches.

It even works if you leave the setEditableFactory part intact, but replace the variable initialization of text with

SpannableStringBuilder text = new SpannableStringBuilder("Hello world!\nHello world again!");

Clearly, although setText() says it'll accept any Editable, it doesn't work well when dealing with anything other than an SSB. Why does this happen and how can I fix it? Thanks.


Answer:

By digging the source code of SpannableStringBuilder I figured out that it not only fulfils the responsibilities defined by the Interfaces Editable, etc. but also reports the span change by calling SpanWatcher.onSpanChanged() by passing this. DynamicLayout (the real workhorse of EditText) responds to onSpanChanged() by checking the equality of passed in reference with it's member (which is our actual ColoredSpan instance). Obviously they are different and I suspect that this is a problem.

Actually SpannableStringBuilder is not just Editable, but more than that. If you need a custom Editable subclassing SpannableStringBuilder may work.

Question:

I'm creating an EditText subclass and I want to make an Editable variable to pass to the super class.

When I originally tried

private Editable unicodeText = new Editable();

I got the error

'Editable' is abstract; cannot be instantiated

Searching for this error in Google did not return any helpful results, so now that I have found the answer I am adding this question with an answer below.


Answer:

Editable is an interface, not a class, and as such cannot be instantiated.

Interfaces cannot be instantiated—they can only be implemented by classes or extended by other interfaces. (docs)

However, the class SpannableStringBuilder implements Editable so you can do the following:

private Editable unicodeText = new SpannableStringBuilder();

Thanks to this answer for setting me on the right track.