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

后端 未结 8 1267
小鲜肉
小鲜肉 2020-11-29 21:19

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

相关标签:
8条回答
  • 2020-11-29 21:58

    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);
        }
    }
    
    0 讨论(0)
  • 2020-11-29 22:00

    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);
        }
    };
    
    0 讨论(0)
  • 2020-11-29 22:04

    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);
    
    0 讨论(0)
  • 2020-11-29 22:11

    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);
      }
    }
    
    0 讨论(0)
  • 2020-11-29 22:11

    The answer by Steven M above works, but thought I'd share this: https://github.com/saket/Better-Link-Movement-Method

    It just works. Nice to not have to own a bunch of complex code.

    0 讨论(0)
  • 2020-11-29 22:19

    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.

    0 讨论(0)
提交回复
热议问题