Change the text color of a single ClickableSpan when pressed without affecting other ClickableSpans in the same TextView

AndroidLayoutTextviewTextcolor

Android Problem Overview


I have a TextView with multiple ClickableSpans in it. When a ClickableSpan is pressed, I want it to change the color of its text.

I have tried setting a color state list as the textColorLink attribute of the TextView. This does not yield the desired result because this causes all the spans to change color when the user clicks anywhere on the TextView.

Interestingly, using textColorHighlight to change the background color works as expected: Clicking on a span changes only the background color of that span and clicking anywhere else in the TextView does nothing.

I have also tried setting ForegroundColorSpans with the same boundaries as the ClickableSpans where I pass the same color state list as above as the color resource. This doesn't work either. The spans always keep the color of the default state in the color state list and never enter the pressed state.

Does anyone know how to do this?

This is the color state list I used:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_pressed="true" android:color="@color/pressed_color"/>
  <item android:color="@color/normal_color"/>
</selector>

Android Solutions


Solution 1 - Android

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);
	}
}

Solution 2 - Android

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);
    }
};

Solution 3 - Android

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.

Solution 4 - Android

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

Solution 5 - Android

try this custom ClickableSpan:

class MyClickableSpan extends ClickableSpan {
    private String action;
    private int fg;
    private int bg;
    private boolean selected;

    public MyClickableSpan(String action, int fg, int bg) {
        this.action = action;
        this.fg = fg;
        this.bg = bg;
    }

    @Override
    public void onClick(View widget) {
        Log.d(TAG, "onClick " + action);
    }
    
    @Override
    public void updateDrawState(TextPaint ds) {
        ds.linkColor = selected? fg : 0xffeeeeee;
        super.updateDrawState(ds);
    }
}

and this SpanWatcher:

class Watcher implements SpanWatcher {
    private TextView tv;
    private MyClickableSpan selectedSpan = null;

    public Watcher(TextView tv) {
        this.tv = tv;
    }
    
    private void changeColor(Spannable text, Object what, int start, int end) {
//        Log.d(TAG, "changeFgColor " + what);
        if (what == Selection.SELECTION_END) {
            MyClickableSpan[] spans = text.getSpans(start, end, MyClickableSpan.class);
            if (spans != null) {
                tv.setHighlightColor(spans[0].bg);
                if (selectedSpan != null) {
                    selectedSpan.selected = false;
                }
                selectedSpan = spans[0];
                selectedSpan.selected = true;
            }
        }
    }

    @Override
    public void onSpanAdded(Spannable text, Object what, int start, int end) {
        changeColor(text, what, start, end);
    }

    @Override
    public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart, int nend) {
        changeColor(text, what, nstart, nend);
    }

    @Override
    public void onSpanRemoved(Spannable text, Object what, int start, int end) {
    }
}

test it in onCreate:

    TextView tv = new TextView(this);
    tv.setTextSize(40);
    tv.setMovementMethod(LinkMovementMethod.getInstance());

    SpannableStringBuilder b = new SpannableStringBuilder();
    b.setSpan(new Watcher(tv), 0, 0, Spanned.SPAN_INCLUSIVE_INCLUSIVE);

    b.append("this is ");
    int start = b.length();
    MyClickableSpan link = new MyClickableSpan("link0 action", 0xffff0000, 0x88ff0000);
    b.append("link 0");
    b.setSpan(link, start, b.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    b.append("\nthis is ");
    start = b.length();
    b.append("link 1");
    link = new MyClickableSpan("link1 action", 0xff00ff00, 0x8800ff00);
    b.setSpan(link, start, b.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    b.append("\nthis is ");
    start = b.length();
    b.append("link 2");
    link = new MyClickableSpan("link2 action", 0xff0000ff, 0x880000ff);
    b.setSpan(link, start, b.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    
    tv.setText(b);
    setContentView(tv);

Solution 6 - Android

This is my solution if you got many click elements (we need an interface): The Interface:

public interface IClickSpannableListener{
  void onClickSpannText(String text,int starts,int ends);
}

The class who manage the event:

public class SpecialClickableSpan extends ClickableSpan{
  private IClickSpannableListener listener;
  private String text;
  private int starts, ends;
  
  public SpecialClickableSpan(String text,IClickSpannableListener who,int starts, int ends){
    super();
    this.text = text;
    this.starts=starts;
    this.ends=ends;
    listener = who;
  }
  
  @Override
  public void onClick(View widget) {
     listener.onClickSpannText(text,starts,ends);
  }
}

In main class:

class Main extends Activity  implements IClickSpannableListener{
  //Global
  SpannableString _spannableString;
  Object _backGroundColorSpan=new BackgroundColorSpan(Color.BLUE); 

  private void setTextViewSpannable(){
    _spannableString= new SpannableString("You can click «here» or click «in this position»");
    _spannableString.setSpan(new SpecialClickableSpan("here",this,15,18),15,19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 
    _spannableString.setSpan(new SpecialClickableSpan("in this position",this,70,86),70,86, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    TextView tv = (TextView)findViewBy(R.id.textView1);
    tv.setMovementMethod(LinkMovementMethod.getInstance());
    tv.setText(spannableString);
  }

  @Override
  public void onClickSpannText(String text, int inicio, int fin) {
    System.out.println("click on "+ text);
    _spannableString.removeSpan(_backGroundColorSpan);
    _spannableString.setSpan(_backGroundColorSpan, inicio, fin, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    ((TextView)findViewById(R.id.textView1)).setText(_spannableString);
  }
}

Solution 7 - Android

Place the java code as below :

package com.synamegames.orbs;

import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;

public class CustomTouchListener implements View.OnTouchListener {     
public boolean onTouch(View view, MotionEvent motionEvent) {

    switch(motionEvent.getAction()){            
        case MotionEvent.ACTION_DOWN:
         ((TextView) view).setTextColor(0x4F4F4F); 
            break;          
        case MotionEvent.ACTION_CANCEL:             
        case MotionEvent.ACTION_UP:
        ((TextView) view).setTextColor(0xCDCDCD);
            break;
    } 

    return false;   
} 
}

In the above code specify wat color you want .

Change the style .xml as you want.

<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="MenuFont">
    <item name="android:textSize">20sp</item>
    <item name="android:textColor">#CDCDCD</item>
    <item name="android:textStyle">normal</item>
    <item name="android:clickable">true</item>
    <item name="android:layout_weight">1</item>
    <item name="android:gravity">left|center</item>
    <item name="android:paddingLeft">35dp</item>
    <item name="android:layout_width">175dp</item> 
    <item name="android:layout_height">fill_parent</item>
</style>

Try it out and say is this you want or something else . update me dude.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSteven MeliopoulosView Question on Stackoverflow
Solution 1 - AndroidSteven MeliopoulosView Answer on Stackoverflow
Solution 2 - AndroidzundiView Answer on Stackoverflow
Solution 3 - Androidj2emanueView Answer on Stackoverflow
Solution 4 - AndroidMaksim DmitrievView Answer on Stackoverflow
Solution 5 - AndroidpskinkView Answer on Stackoverflow
Solution 6 - AndroidCarlos GómezView Answer on Stackoverflow
Solution 7 - AndroidkathirView Answer on Stackoverflow