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.


legr3c's answer helped me a lot. And I'd like to add a few remarks.

Remark #1.

TextView myTextView = (TextView) findViewById(R.id.my_textview);
myTextView.setMovementMethod(new LinkTouchMovementMethod());
myTextView.setHighlightColor(getResources().getColor(android.R.color.transparent));
SpannableString mySpannable = new SpannableString(text);
mySpannable.setSpan(new TouchableSpan(), 0, 7, 0);
mySpannable.setSpan(new TouchableSpan(), 15, 18, 0);
myTextView.setText(mySpannable, BufferType.SPANNABLE);

I applied LinkTouchMovementMethod to a TextView with two spans. The spans were highlighted with blue when clicked them. myTextView.setHighlightColor(getResources().getColor(android.R.color.transparent)); fixed the bug.

Remark #2.

Don't forget to get colors from resources when passing normalTextColor, pressedTextColor, and pressedBackgroundColor.

Should pass resolved color instead of resource id here