i am making a chatting application in which i am using emoticons functionality.My Emoticons functionality is working properly for single image ,but when i am taking multiple emotive images it is not converting in to particular image..,at a time only single image is converting, My problem is
i am unable to separate the spanned object in edit text field..,for single value it is working but for multiple value its is not working..
Example.i am taking 4 different images in edit text field, like this here
now i want to seprate its spanned object.,how can i do this
here is code
public void keyClickedIndex( final String index)
{
ImageGetter imageGetter = new ImageGetter()
{
public Drawable getDrawable(String source)
{
StringTokenizer st = new StringTokenizer(index, ".");
Drawable d = new BitmapDrawable(getResources(),emoticons[Integer.parseInt(st.nextToken()) - 1]);
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
return d;
}
};
Spanned cs = Html.fromHtml("<img src ='"+ index +"'/>", imageGetter, null);
int cursorPosition = mSendText.getSelectionStart();
mSendText.getText().insert(cursorPosition, cs);
please help me..,Thanks in Advance.
you can use emoticon handler method
private static class EmoticonHandler implements TextWatcher {
private final EditText mEditor;
private final ArrayList<ImageSpan> mEmoticonsToRemove = new ArrayList<ImageSpan>();
//public String txt;
XMPPClient act;
public EmoticonHandler(EditText editor,XMPPClient act) {
// Attach the handler to listen for text changes.
mEditor = editor;
mEditor.addTextChangedListener(this);
this.act = act;
}
public void insert(String emoticon, int resource)
{
// Create the ImageSpan
Drawable drawable = mEditor.getResources().getDrawable(resource);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
ImageSpan span = new ImageSpan(drawable,emoticon,ImageSpan.ALIGN_BASELINE);
// Get the selected text.
int start = mEditor.getSelectionStart();
int end = mEditor.getSelectionEnd();
Editable message = mEditor.getEditableText();
// Insert the emoticon.
message.replace(start, end, emoticon);
message.setSpan(span, start, start + emoticon.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
#Override
public void beforeTextChanged(CharSequence text, int start, int count, int after) {
// Check if some text will be removed.
if (count > 0) {
int end = start + count;
Editable message = mEditor.getEditableText();
ImageSpan[] list = message.getSpans(start, end, ImageSpan.class);
boolean check = false;
for (ImageSpan span : list)
{
// Get only the emoticons that are inside of the changed
// region.
check = true;
int spanStart = message.getSpanStart(span);
int spanEnd = message.getSpanEnd(span);
//txt = text.toString();
act.emorTxt = text.toString();
if ((spanStart < end) && (spanEnd > start)) {
// Add to remove list
mEmoticonsToRemove.add(span);
}
}
if(!check)
{
act.emorTxt = text.toString();
}
}
}
#Override
public void afterTextChanged(Editable text) {
Editable message = mEditor.getEditableText();
// Commit the emoticons to be removed.
for (ImageSpan span : mEmoticonsToRemove)
{
int start = message.getSpanStart(span);
int end = message.getSpanEnd(span);
// Remove the span
message.removeSpan(span);
// Remove the remaining emoticon text.
if (start != end) {
message.delete(start, end);
}
}
mEmoticonsToRemove.clear();
}
#Override
public void onTextChanged(CharSequence text, int start, int before, int count) {
}
}
it will work perfectly....:)
Related
How to create custom input mask in Android.
For Example, need DFSU 618908 that means 4 digits in the beginning is alphabets, then space, then 7 digits.
In web development, I can use InputMaskRobinHerbots
I've been google, I found this RedMadRonbot but written in kotlin, which is I am not familiar.
How can I just create in a mask using TextWatcher for my case...?
Any help it so appreciated.
<EditText
android:id="#+id/editTextDialogNomorContainerDeliveryEkspor"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
</EditText>
In my Activity just
EditText nomorContainer = (EditText) promptsView.findViewById(R.id.editTextDialogNomorContainerDeliveryEkspor);
//some TextWatcher perhaps
Here is an example of using TextWatcher to create custom InputMask
Create this class that overrides TextWatcher
public class SsnMask implements TextWatcher {
private static final int MAX_LENGTH = 9;
private static final int MIN_LENGTH = 3;
private String updatedText;
private boolean editing;
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
#Override
public void onTextChanged(CharSequence text, int start, int before, int count) {
if (text.toString().equals(updatedText) || editing) return;
String digits = text.toString().replaceAll("\\D", "");
int length = digits.length();
if (length <= MIN_LENGTH) {
updatedText = digits;
return;
}
if (length > MAX_LENGTH) {
digits = digits.substring(0, MAX_LENGTH);
}
if (length <= 5) {
String firstPart = digits.substring(0, 3);
String secondPart = digits.substring(3);
updatedText = String.format(Locale.US, "%s-%s", firstPart, secondPart);
}
else {
String firstPart = digits.substring(0, 3);
String secondPart = digits.substring(3, 5);
String thirdPart = digits.substring(5);
updatedText = String.format(Locale.US, "%s-%s-%s", firstPart, secondPart, thirdPart);
}
}
#Override
public void afterTextChanged(Editable editable) {
if (editing) return;
editing = true;
editable.clear();
editable.insert(0, updatedText);
editing = false;
}
}
Then you just apply it like this
ssnET = view.findViewById(R.id.ssnET);
ssnET.addTextChangedListener(new SsnMask());
An input-mask-android author here.
The library is actually interoperable with Java. There's even a Quick Start page in our Wiki with Java sample code.
I have an edit text while typing inside edit text if the word starts with # that particular words color should change ,
i have implemented textwatcher and found that if text starts with # but do not know how to update the color dynamically ,
Have tried SpannableStringBuilder ssb = new SpannableStringBuilder(yourText) But its static , could anyone help me for dynamic implementation
Here is my code
myEditTxt.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence text, int start, int count, int after) {
}
#Override
public void onTextChanged(CharSequence text, int start, int before, int count) {
if (text.charAt(start) == '#') {
//here i needs to update the typing text color
}
}
#Override
public void afterTextChanged(Editable s) {
}
});
As implemented in this link How to change color of words with hashtags #Muhammed GÜNEŞ suggested
you can modify it according to follow
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
textView.removeTextChangedListener(this);
setTags(textView,s.toString());
textView.setSelection(textView.getText().toString().length());
textView.addTextChangedListener(this);
}
private void setTags(TextView pTextView, String pTagString) {
SpannableString string = new SpannableString(pTagString);
int start = -1;
for (int i = 0; i < pTagString.length(); i++) {
if (pTagString.charAt(i) == '#') {
start = i;
} else if (pTagString.charAt(i) == ' ' || (i == pTagString.length() - 1 && start != -1)) {
if (start != -1) {
if (i == pTagString.length() - 1) {
i++; // case for if hash is last word and there is no
// space after word
}
final String tag = pTagString.substring(start, i);
string.setSpan(new ClickableSpan() {
#Override
public void onClick(View widget) {
Toast.makeText(MainActivity.this,"Click",Toast.LENGTH_LONG).show();
}
#Override
public void updateDrawState(TextPaint ds) {
// link color
ds.setColor(Color.parseColor("#33b5e5"));
ds.setUnderlineText(false);
}
}, start, i, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
start = -1;
}
}
}
pTextView.setMovementMethod(LinkMovementMethod.getInstance());
pTextView.setText(string);
}
it will add color and click event too also u can modify it according o your further requirement
Maybe something like this
#Override
public void onTextChanged(CharSequence text, int start, int before, int count) {
if (text.charAt(start) == '#') {
SpannableString spannableString = new SpannableString(editText.getText().toString());
ForegroundColorSpan foregroundSpan = new ForegroundColorSpan(ContextCompat.getColor(getContext(), R.color.yourColor));
spannableString.setSpan(foregroundSpan, start,
spannableString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
editText.setText(spannableString);
}
}
If you only want to change "#" char. You can try it:
int index = myEditTxt.getText().toString().indexOf("#");
if (index != -1) {
SpannableString spannable = new
SpannableString(myEditTxt.getText().toString());
ForegroundColorSpan fcs = new ForegroundColorSpan(color);
// Set the text color
spannable.setSpan(fcs, index, index + 1,
Spannable.SPAN_INCLUSIVE_INCLUSIVE);
mEditText.setText(spannable);
}
If you only want to change after and before "#". You can try it in your listener:
if (text.charAt(start) == '#') {
SpannableString spannable = new
SpannableString(myEditTxt.getText().toString());
ForegroundColorSpan fcs = new ForegroundColorSpan(color);
// Set the text color
if (start > 0)
spannable.setSpan(fcs, 0, start -1,
Spannable.SPAN_INCLUSIVE_INCLUSIVE);
if (start < myEditTxt.getText().toString().length())
spannable.setSpan(fcs, start + 1, myEditTxt.getText().toString().length(),
Spannable.SPAN_INCLUSIVE_INCLUSIVE);
mEditText.setText(spannable);
}
Finally found an answer and works as expected .
private int intCount = 0, initialStringLength = 0;
private String strText = "";
on text changed
#Override
public void onTextChanged(CharSequence text, int start, int before, int count) {
String strET = editText.getText().toString();
String[] str = strET.split(" ");
int cnt = 0;
if (text.length() != initialStringLength && text.length() != 0) {
if (!strET.substring(strET.length() - 1).equals(" ")) {
initialStringLength = text.length();
cnt = intCount;
for (int i = 0; i < str.length; i++)
if (str[i].charAt(0) == '#')
strText = strText + " " + "<font color='#EE0000'>" + str[i] + "</font>";
else
strText = strText + " " + str[i];
}
if (intCount == cnt) {
intCount = str.length;
editText.setText(Html.fromHtml(strText));
editText.setSelection(textShareMemories.getText().toString().length());
}
} else {
strText = "";
}
}
Is there a way to prevent a user from deleting or modifying a spannable in an EditText? More specifically, I have an ImageSpan as the first character of the EditText. I want to ensure the user cannot delete that ImageSpan.
I realize I can use TextWatcher and replace the ImageSpan if the user deletes it. That's rather ugly and I'm hoping there's a way to prevent the deletion in the first place.
Here's a snip of code where I set the text value:
Bitmap bitmap = <bitmap from elsewhere>;
String text = <text to display after ImageSpan, from elsewhere>;
SpannableString ss = new SpannableString (" " + text);
ImageSpan image = new ImageSpan (getContext(), bitmap, ImageSpan.ALIGN_BOTTOM);
ss.setSpan (image, 0, 1, 0);
setText (ss);
OK, the solution is quite complicated but it's working. We need a custom InputFilter and a SpanWatcher. Let's start.
The first step is quite simple. We set a non-editable prefix with an image span and set a cursor after this prefix.
final String prefix = "?";
final ImageSpan image = new ImageSpan(this, R.drawable.image);
edit.setText(prefix);
edit.getText().setSpan(image, 0, prefix.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
edit.setSelection(prefix.length());
Then we set an InputFilter that will prevent the prefix with the span from editing. It can be done by moving a range being edited so it starts after the prefix.
edit.setFilters(new InputFilter[] {
new InputFilter() {
#Override
public CharSequence filter(final CharSequence source, final int start,
final int end, final Spanned dest, final int dstart, final int dend) {
final int newStart = Math.max(prefix.length(), dstart);
final int newEnd = Math.max(prefix.length(), dend);
if (newStart != dstart || newEnd != dend) {
final SpannableStringBuilder builder = new SpannableStringBuilder(dest);
builder.replace(newStart, newEnd, source);
if (source instanceof Spanned) {
TextUtils.copySpansFrom(
(Spanned) source, 0, source.length(), null, builder, newStart);
}
Selection.setSelection(builder, newStart + source.length());
return builder;
} else {
return null;
}
}
}
});
Then we create a SpanWatcher that will detect selection changes and move the selection out of the prefix range.
final SpanWatcher watcher = new SpanWatcher() {
#Override
public void onSpanAdded(final Spannable text, final Object what,
final int start, final int end) {
// Nothing here.
}
#Override
public void onSpanRemoved(final Spannable text, final Object what,
final int start, final int end) {
// Nothing here.
}
#Override
public void onSpanChanged(final Spannable text, final Object what,
final int ostart, final int oend, final int nstart, final int nend) {
if (what == Selection.SELECTION_START) {
if (nstart < prefix.length()) {
final int end = Math.max(prefix.length(), Selection.getSelectionEnd(text));
Selection.setSelection(text, prefix.length(), end);
}
} else if (what == Selection.SELECTION_END) {
final int start = Math.max(prefix.length(), Selection.getSelectionEnd(text));
final int end = Math.max(start, nstart);
if (end != nstart) {
Selection.setSelection(text, start, end);
}
}
}
};
And finally we just add the SpanWatcher to the text.
edit.getText().setSpan(watcher, 0, 0, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
And that's all. So comparing this solution to just adding a TextWatcher I would prefer the latter approach.
I'm trying to implement an EditText that limits input to alpha chars only [A-Za-z].
I started with the InputFilter method from this post. When I type "a%" the text disappears then if I hit backspace the text is "a". I've tried other variations on the filter function like using a regex to match only [A-Za-z] and sometimes see crazy behavior like repeating chars, I'll type "a" then "b" and get "aab" then type "c" and get "aabaabc" then hit backspace and get "aabaabcaabaabc"!
Here's the code I'm working with so far with the different approaches I've tried.
EditText input = (EditText)findViewById( R.id.inputText );
InputFilter filter = new InputFilter() {
#Override
public CharSequence filter( CharSequence source, int start, int end, Spanned dest, int dstart, int dend ) {
//String data = source.toString();
//String ret = null;
/*
boolean isValid = data.matches( "[A-Za-z]" );
if( isValid ) {
ret = null;
}
else {
ret = data.replaceAll( "[##$%^&*]", "" );
}
*/
/*
dest = new SpannableStringBuilder();
ret = data.replaceAll( "[##$%^&*]", "" );
return ret;
*/
for( int i = start; i < end; i++ ) {
if( !Character.isLetter( source.charAt( i ) ) ) {
return "";
}
}
return null;
}
};
input.setFilters( new InputFilter[]{ filter } );
I'm totally stumped on this one so any help here would be greatly appreciated.
EDIT:
Ok, I've done quite a lot of experimenting with InputFilter and have drawn some conclusions, albeit no solution to the problem. See the comments in my code below. I'm going to try Imran Rana's solution now.
EditText input = (EditText)findViewById( R.id.inputText );
InputFilter filter = new InputFilter() {
// It is not clear what this function should return!
// Docs say return null to allow the new char(s) and return "" to disallow
// but the behavior when returning "" is inconsistent.
//
// The source parameter is a SpannableStringBuilder if 1 char is entered but it
// equals the whole string from the EditText.
// If more than one char is entered (as is the case with some keyboards that auto insert
// a space after certain chars) then the source param is a CharSequence and equals only
// the new chars.
#Override
public CharSequence filter( CharSequence source, int start, int end, Spanned dest, int dstart, int dend ) {
String data = source.toString().substring( start, end );
String retData = null;
boolean isValid = data.matches( "[A-Za-z]+" );
if( !isValid ) {
if( source instanceof SpannableStringBuilder ) {
// This works until the next char is evaluated then you get repeats
// (Enter "a" then "^" gives "a". Then enter "b" gives "aab")
retData = data.replaceAll( "[##$%^&*']", "" );
// If I instead always returns an empty string here then the EditText is blanked.
// (Enter "a" then "^" gives "")
//retData = "";
}
else { // source is instanceof CharSequence
// We only get here if more than 1 char was entered (like "& ").
// And again, this works until the next char is evaluated then you get repeats
// (Enter "a" then "& " gives "a". Then enter "b" gives "aab")
retData = "";
}
}
return retData;
}
};
input.setFilters( new InputFilter[]{ filter } );
Use the following code:
EditText input = (EditText) findViewById(R.id.inputText);
input.addTextChangedListener(new TextWatcher() {
public void onTextChanged(CharSequence s, int start, int before, int count) {
// TODO Auto-generated method stub
for( int i = start;i<s.toString().length(); i++ ) {
if( !Character.isLetter(s.charAt( i ) ) ) {
input.setText("");
}
}
}
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// TODO Auto-generated method stub
}
public void afterTextChanged(Editable s) {
// TODO Auto-generated method stub
}
});
If you want the valid text to remain in the EditText:
input.addTextChangedListener(new TextWatcher() {
public void onTextChanged(CharSequence s, int start, int before, int count) {
// TODO Auto-generated method stub
}
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// TODO Auto-generated method stub
}
public void afterTextChanged(Editable s) {
// TODO Auto-generated method stub
for( int i = 0;i<s.toString().length(); i++ ) {
if( !Character.isLetter(s.charAt( i ) ) ) {
s.replace(i, i+1,"");
}
}
}
});
Fix for repeating text, work on all Android Versions:
public static InputFilter getOnlyCharactersFilter() {
return getCustomInputFilter(true, false, false);
}
public static InputFilter getCharactersAndDigitsFilter() {
return getCustomInputFilter(true, true, false);
}
public static InputFilter getCustomInputFilter(final boolean allowCharacters, final boolean allowDigits, final boolean allowSpaceChar) {
return new InputFilter() {
#Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
boolean keepOriginal = true;
StringBuilder sb = new StringBuilder(end - start);
for (int i = start; i < end; i++) {
char c = source.charAt(i);
if (isCharAllowed(c)) {
sb.append(c);
} else {
keepOriginal = false;
}
}
if (keepOriginal) {
return null;
} else {
if (source instanceof Spanned) {
SpannableString sp = new SpannableString(sb);
TextUtils.copySpansFrom((Spanned) source, start, sb.length(), null, sp, 0);
return sp;
} else {
return sb;
}
}
}
private boolean isCharAllowed(char c) {
if (Character.isLetter(c) && allowCharacters) {
return true;
}
if (Character.isDigit(c) && allowDigits) {
return true;
}
if (Character.isSpaceChar(c) && allowSpaceChar) {
return true;
}
return false;
}
};
}
Now you can use this filer like:
//Accept Characters Only
edit_text.setFilters(new InputFilter[]{getOnlyCharactersFilter()});
//Accept Digits and Characters
edit_text.setFilters(new InputFilter[]{getCharactersAndDigitsFilter()});
//Accept Digits and Characters and SpaceBar
edit_text.setFilters(new InputFilter[]{getCustomInputFilter(true,true,true)});
Bingo, I found the problem!
When I use android:cursorVisible="false" on the EditText the start and dstart parameters don't match up correctly.
The start parameter is still always 0 for me, but the dstart parameter is also always 0 so it works out as long as I use .replaceAll(). This is contrary to what this post says so I don't quite understand why but at least I can build something that works now!
We had a similar problem and I believe a solution[0] that would work for you as well. Our requirements were to implement an EditText that stripped rich text input. For example, if the user copied bold text to their clipboard and pasted it into the EditText, the EditText should remove the bold emphasis styling and preserve only the plain text.
The solution class looks something like this:
public class PlainEditText extends EditText {
public PlainEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
addFilter(this, new PlainTextInputFilter());
}
private void addFilter(TextView textView, InputFilter filter) {
InputFilter[] filters = textView.getFilters();
InputFilter[] newFilters = Arrays.copyOf(filters, filters.length + 1);
newFilters[filters.length] = filter;
textView.setFilters(newFilters);
}
private static class PlainTextInputFilter implements InputFilter {
#Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
int dstart, int dend) {
return stripRichText(source, start, end);
}
private CharSequence stripRichText(CharSequence str, int start, int end) {
// ...
}
}
}
Our original implementation for stripRichText() was simple:
// -- BROKEN. DO NOT USE --
String plainText = str.subSequence(start, end).toString();
return plainText;
The Java base String class doesn't retain any styling information so converting the CharSequence interface to a concrete String copies only plain text.
What we didn't realize was that some Android soft keyboards add and depend on temporary compositional hints for typos and other things. The problem manifests by removing the hints as well as repeating characters in an unexpected way (usually doubling the entire EditText field's input). The documentation[1] for InputFilter.filter() communicates the requirement this way:
* Note: If <var>source</var> is an instance of {#link Spanned} or
* {#link Spannable}, the span objects in the <var>source</var> should be
* copied into the filtered result (i.e. the non-null return value).
I believe the proper solution is to preserve temporary spans:
/** Strips all rich text except spans used to provide compositional hints. */
private CharSequence stripRichText(CharSequence str, int start, int end) {
String plainText = str.subSequence(start, end).toString();
SpannableString ret = new SpannableString(plainText);
if (str instanceof Spanned) {
List<Object> keyboardHintSpans = getComposingSpans((Spanned) str, start, end);
copySpans((Spanned) str, ret, keyboardHintSpans);
}
return ret;
}
/**
* #return Temporary spans, often applied by the keyboard to provide hints such as typos.
*
* #see {#link android.view.inputmethod.BaseInputConnection#removeComposingSpans}
* #see {#link android.inputmethod.latin.inputlogic.InputLogic#setComposingTextInternalWithBackgroundColor}
*/
#NonNull private List<Object> getComposingSpans(#NonNull Spanned spanned,
int start,
int end) {
// TODO: replace with Apache CollectionUtils.filter().
List<Object> ret = new ArrayList<>();
for (Object span : getSpans(spanned, start, end)) {
if (isComposingSpan(spanned, span)) {
ret.add(span);
}
}
return ret;
}
private Object[] getSpans(#NonNull Spanned spanned, int start, int end) {
Class<Object> anyType = Object.class;
return spanned.getSpans(start, end, anyType);
}
private boolean isComposingSpan(#NonNull Spanned spanned, Object span) {
return isFlaggedSpan(spanned, span, Spanned.SPAN_COMPOSING);
}
private boolean isFlaggedSpan(#NonNull Spanned spanned, Object span, int flags) {
return (spanned.getSpanFlags(span) & flags) == flags;
}
/**
* Apply only the spans from src to dst specific by spans.
*
* #see {#link android.text.TextUtils#copySpansFrom}
*/
public static void copySpans(#NonNull Spanned src,
#NonNull Spannable dst,
#NonNull Collection<Object> spans) {
for (Object span : spans) {
int start = src.getSpanStart(span);
int end = src.getSpanEnd(span);
int flags = src.getSpanFlags(span);
dst.setSpan(span, start, end, flags);
}
}
[0] Actual implementation available here: https://github.com/wikimedia/apps-android-wikipedia/blob/e9ddd8854ff15cde791a2e6fb7754a5450d6f7cf/app/src/main/java/org/wikipedia/richtext/RichTextUtil.java
[1] https://android.googlesource.com/platform/frameworks/base/+/029942f77d05ed3d20256403652b220c83dad6e1/core/java/android/text/InputFilter.java#37
I would just like to add my solution to the problem(as late as it is). I found that if you add
yourEditText.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
Then the backspace problems stop
Original String:
Lorem ##ipsum## dolar ##sit## atem. Lorem ipsum dolar sit ##atem##.
After formating:
Lorem #ipsum dolar #sit atem. Lorem ipsum dolar sit #atem.
But only the last one has the Formating i want. See image below.
CODE
private void format() {
CharSequence text = editContent.getText();
MovementMethod movementMethod = editContent.getMovementMethod();
if ((movementMethod == null) || !(movementMethod instanceof LinkMovementMethod))
{
editContent.setMovementMethod(LinkMovementMethod.getInstance());
}
text = setSpanBetweenTokens(text, "##", new ForegroundColorSpan(0xFF0099FF), new UnderlineSpan(), new ClickableSpan() {
#Override
public void onClick(View widget) {
Toast.makeText(getApplicationContext(), "click", Toast.LENGTH_SHORT).show();
}
});
editContent.setText(text);
}
private static CharSequence setSpanBetweenTokens(CharSequence text, String token, CharacterStyle... characterStyle) {
int tokenLen = token.length();
int start = text.toString().indexOf(token) + 1;
int end = text.toString().indexOf(token, start);
while (start > -1 && end > -1)
{
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
for (CharacterStyle c : characterStyle) {
spannableStringBuilder.setSpan(c, start, end, 0);
}
spannableStringBuilder.delete(end, end + tokenLen);
spannableStringBuilder.delete(start - 1, start);
text = spannableStringBuilder;
start = text.toString().indexOf(token) + 1;
end = text.toString().indexOf(token, start);
}
return text;
}
EDIT
My final Solution
private void format() {
CharSequence text = editContent.getText();
MovementMethod movementMethod = editContent.getMovementMethod();
if ((movementMethod == null) || !(movementMethod instanceof LinkMovementMethod))
{
editContent.setMovementMethod(LinkMovementMethod.getInstance());
}
text = setSpanBetweenTokens(text, "##");
editContent.setText(text);
}
private static CharSequence setSpanBetweenTokens(CharSequence text, String token) {
int tokenLen = token.length();
int start = text.toString().indexOf(token) + 1;
int end = text.toString().indexOf(token, start);
while (start > -1 && end > -1)
{
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
spannableStringBuilder.setSpan(new ForegroundColorSpan(0xFF0099FF), start, end, 0);
spannableStringBuilder.setSpan(new UnderlineSpan(), start, end, 0);
spannableStringBuilder.setSpan(new ClickableSpan() {
#Override
public void onClick(View widget) {
Log.d("DEBUG", "Click");
}
}, start, end, 0);
spannableStringBuilder.delete(end, end + tokenLen);
spannableStringBuilder.delete(start - 1, start);
text = spannableStringBuilder;
start = text.toString().indexOf(token) + 1;
end = text.toString().indexOf(token, start);
}
return text;
}
Pass different object for each span:
spannableStringBuilder.setSpan(c, start, end, 0);
You're passing the same object for each span:
new ForegroundColorSpan(0xFF0099FF)
When span object exists in spannableStringBuilder then it changes bounds only, not a new span is added.
I would suggest a simpler way. If your formatting needs are basic, a simple regex + Html.fromHtml() should do the trick:
private void format() {
String mText = editContent.getText();
Spanned mSpannedText = Html.fromHtml(mText.replaceAll("##(.*?)##)","<font color=\"0xFF0099\">#$1</font>"),
editContent.setText(mSpannedText);
}
The final solution correctly loops however your first token will not be correctly deleted as you have used
int start = text.toString().indexOf(token) + 1;
which would only work if your token was 1 character in length. Since your chosen token is ## change the above code to utilise the already created variable tokenLen
int start = text.toString().indexOf(token) + tokenLen;
this will ensure your text is correctly edited and all trace of your tokens are removed.