Change the text color of a single ClickableSpan when pressed without affecting other ClickableSpans in the same TextView
I finally found a solution that does everything I wanted. It is based on this answer.
This is my modified LinkMovementMethod that marks a span as pressed on the start of a touch event (MotionEvent.ACTION_DOWN) and unmarks it when the touch ends or when the touch location moves out of the span.
public class LinkTouchMovementMethod extends LinkMovementMethod { private TouchableSpan mPressedSpan; @Override public boolean onTouchEvent(TextView textView, Spannable spannable, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { mPressedSpan = getPressedSpan(textView, spannable, event); if (mPressedSpan != null) { mPressedSpan.setPressed(true); Selection.setSelection(spannable, spannable.getSpanStart(mPressedSpan), spannable.getSpanEnd(mPressedSpan)); } } else if (event.getAction() == MotionEvent.ACTION_MOVE) { TouchableSpan touchedSpan = getPressedSpan(textView, spannable, event); if (mPressedSpan != null && touchedSpan != mPressedSpan) { mPressedSpan.setPressed(false); mPressedSpan = null; Selection.removeSelection(spannable); } } else { if (mPressedSpan != null) { mPressedSpan.setPressed(false); super.onTouchEvent(textView, spannable, event); } mPressedSpan = null; Selection.removeSelection(spannable); } return true; } private TouchableSpan getPressedSpan( TextView textView, Spannable spannable, MotionEvent event) { int x = (int) event.getX() - textView.getTotalPaddingLeft() + textView.getScrollX(); int y = (int) event.getY() - textView.getTotalPaddingTop() + textView.getScrollY(); Layout layout = textView.getLayout(); int position = layout.getOffsetForHorizontal(layout.getLineForVertical(y), x); TouchableSpan[] link = spannable.getSpans(position, position, TouchableSpan.class); TouchableSpan touchedSpan = null; if (link.length > 0 && positionWithinTag(position, spannable, link[0])) { touchedSpan = link[0]; } return touchedSpan; } private boolean positionWithinTag(int position, Spannable spannable, Object tag) { return position >= spannable.getSpanStart(tag) && position <= spannable.getSpanEnd(tag); } }
This needs to be applied to the TextView like so:
yourTextView.setMovementMethod(new LinkTouchMovementMethod());
And this is the modified ClickableSpan that edits the draw state based on the pressed state set by the LinkTouchMovementMethod: (it also removes the underline from the links)
public abstract class TouchableSpan extends ClickableSpan { private boolean mIsPressed; private int mPressedBackgroundColor; private int mNormalTextColor; private int mPressedTextColor; public TouchableSpan(int normalTextColor, int pressedTextColor, int pressedBackgroundColor) { mNormalTextColor = normalTextColor; mPressedTextColor = pressedTextColor; mPressedBackgroundColor = pressedBackgroundColor; } public void setPressed(boolean isSelected) { mIsPressed = isSelected; } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setColor(mIsPressed ? mPressedTextColor : mNormalTextColor); ds.bgColor = mIsPressed ? mPressedBackgroundColor : 0xffeeeeee; ds.setUnderlineText(false); }}
Much simpler solution, IMO:
final int colorForThisClickableSpan = Color.RED; //Set your own conditional logic here.final ClickableSpan link = new ClickableSpan() { @Override public void onClick(final View view) { //Do something here! } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setColor(colorForThisClickableSpan); }};
All these solutions are too much work.
Just set android:textColorLink
in your TextView
to some selector. Then create a clickableSpan with no need to override updateDrawState(...). All done.
here a quick example:
In your strings.xml
have a declared string like this:
<string name="mystring">This is my message%1$s these words are highlighted%2$s and awesome. </string>
then in your activity:
private void createMySpan(){ final String token = "#"; String myString = getString(R.string.mystring,token,token); int start = myString.toString().indexOf(token); //we do -1 since we are about to remove the tokens afterwards so it shifts int finish = myString.toString().indexOf(token, start+1)-1; myString = myString.replaceAll(token, ""); //create your spannable final SpannableString spannable = new SpannableString(myString); final ClickableSpan clickableSpan = new ClickableSpan() { @Override public void onClick(final View view) { doSomethingOnClick(); } }; spannable.setSpan(clickableSpan, start, finish, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); mTextView.setMovementMethod(LinkMovementMethod.getInstance()); mTextView.setText(spannable);}
and heres the important parts ..declare a selector like this calling it myselector.xml
:
<?xml version="1.0" encoding="utf-8"?><selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true" android:color="@color/gold"/> <item android:color="@color/pink"/></selector>
And last in your TextView
in xml do this:
<TextView android:id="@+id/mytextview" android:background="@android:color/transparent" android:text="@string/mystring" android:textColorLink="@drawable/myselector" />
Now you can have a pressed state on your clickableSpan.