Изменить цвет текста одного ClickableSpan при нажатии, не затрагивая другие ClickableSpans в том же TextView

У меня есть TextView с несколькими ClickableSpans в нем. Когда ClickableSpan нажата, я хочу, чтобы он изменил цвет своего текста.

Я попытался установить список цветов в качестве атрибута textColorLink TextView. Это не приводит к желаемому результату, потому что это заставляет все промежутки изменять цвет, когда пользователь щелкает в любом месте в TextView.

Интересно, что использование textColorHighlight для изменения цвета фона работает должным образом: щелчок по диапазону изменяет только цвет фона этого диапазона, а нажатие в любом месте в TextView ничего не делает.

Я также попытался установить ForegroundColorSpans с теми же границами, что и ClickableSpans, где я передаю тот же список состояний цвета, что и выше, что и ресурс цвета. Это тоже не работает. Диапазоны всегда сохраняют цвет состояния по умолчанию в списке состояний цвета и никогда не переходят в нажатое состояние.

Кто-нибудь знает как это сделать?

Это список состояний цвета, который я использовал:

<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>

9 ответов

Решение

Я наконец нашел решение, которое делает все, что я хотел. Это основано на этом ответе.

Это мой модифицированный метод LinkMovementMethod, который помечает диапазон как нажатый в начале события касания (MotionEvent.ACTION_DOWN) и снимает отметку, когда касание заканчивается или когда местоположение касания выходит за пределы промежутка.

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

Это должно быть применено к TextView следующим образом:

    yourTextView.setMovementMethod(new LinkTouchMovementMethod());

И это модифицированный ClickableSpan, который редактирует состояние рисования на основе нажатого состояния, установленного LinkTouchMovementMethod: (он также удаляет подчеркивание из ссылок)

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

Гораздо более простое решение, ИМО:

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

Все эти решения слишком много работы.

Просто установите android:textColorLink в вашем TextView к некоторому селектору. Затем создайте clickableSpan без необходимости переопределять updateDrawState(...). Все сделано.

вот быстрый пример:

В вашем strings.xml иметь объявленную строку как это:

<string name="mystring">This is my message%1$s these words are highlighted%2$s and awesome. </string>

тогда в вашей деятельности:

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

и вот важные части.. объявить селектор, как это называется 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>

И последний в вашем TextView в XML сделать это:

 <TextView
     android:id="@+id/mytextview"
     android:background="@android:color/transparent"
     android:text="@string/mystring"
     android:textColorLink="@drawable/myselector" />

Теперь вы можете иметь нажатое состояние на clickableSpan.

Ответ legr3c мне очень помог. И я хотел бы добавить несколько замечаний.

Замечание № 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);

Я применил LinkTouchMovementMethod к TextView с двумя пролетами. Пролеты были выделены синим цветом при нажатии на них. myTextView.setHighlightColor(getResources().getColor(android.R.color.transparent)); исправил ошибку.

Замечание № 2.

Не забудьте получить цвета из ресурсов при прохождении normalTextColor, pressedTextColor, а также pressedBackgroundColor,

Должен передать разрешенный цвет вместо идентификатора ресурса здесь

Попробуйте этот пользовательский 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);
    }
}

и этот 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) {
    }
}

проверить это в 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);

Это мое решение, если у вас есть много элементов click (нам нужен интерфейс): Интерфейс:

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

Класс, который управляет событием:

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

В основном классе:

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

Ответ Стивена М выше работает, но подумал, что поделюсь этим:https://github.com/saket/Better-Link-Movement-Method

Это просто работает. Приятно не иметь кучу сложного кода.

Король с уважением относится к ответу Стивена , если кому-то нужно, вот его реализация в Котлине:

          abstract class TouchableSpan(
        private val mNormalTextColor: Int,
        private val mPressedTextColor: Int
    ) : ClickableSpan() {
    
        private var isPressed = false
    
        fun setPressed(isSelected: Boolean) {
            isPressed = isSelected
        }
    
        override fun updateDrawState(ds: TextPaint) {
            super.updateDrawState(ds)
            ds.color = if (isPressed) mPressedTextColor else mNormalTextColor
            ds.isUnderlineText = false
        }
    }

    class LinkTouchMovementMethod : LinkMovementMethod() {
    
        private var pressedSpan: TouchableSpan? = null
    
        override fun onTouchEvent(
            textView: TextView,
            spannable: Spannable,
            event: MotionEvent
        ): Boolean {
            if (event.action == MotionEvent.ACTION_DOWN) {
                pressedSpan = getPressedSpan(textView, spannable, event)
                pressedSpan?.setPressed(true)
                Selection.setSelection(
                    spannable, spannable.getSpanStart(pressedSpan),
                    spannable.getSpanEnd(pressedSpan)
                )
            } else if (event.action == MotionEvent.ACTION_MOVE) {
                val touchedSpan = getPressedSpan(textView, spannable, event)
                if (touchedSpan !== pressedSpan) {
                    pressedSpan?.setPressed(false)
                    pressedSpan = null
                    Selection.removeSelection(spannable)
                }
            } else {
                pressedSpan?.setPressed(false)
                super.onTouchEvent(textView, spannable, event)
                pressedSpan = null
                Selection.removeSelection(spannable)
            }
            return true
        }
    
        private fun getPressedSpan(
            textView: TextView,
            spannable: Spannable,
            event: MotionEvent
        ): TouchableSpan? {
            val x = event.x.toInt() - textView.totalPaddingLeft + textView.scrollX
            val y = event.y.toInt() - textView.totalPaddingTop + textView.scrollY
            val layout = textView.layout
            val position = layout.getOffsetForHorizontal(layout.getLineForVertical(y), x.toFloat())
            val link = spannable.getSpans(position, position, TouchableSpan::class.java)
            if (link.isNotEmpty() && positionWithinTag(position, spannable, link[0])) {
                return link[0]
            }
            return null
        }
    
        private fun positionWithinTag(position: Int, spannable: Spannable, tag: Any) =
            position >= spannable.getSpanStart(tag)
                    && position <= spannable.getSpanEnd(tag)
    
        companion object {
            val instance by lazy {
                LinkTouchMovementMethod()
            }
        }
    }

Поместите код Java, как показано ниже:

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

В приведенном выше коде укажите желаемый цвет ват.

Измените стиль.xml, как вы хотите.

<?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>

Попробуйте и скажите, хотите ли вы этого или что-то еще. обнови меня чувак.

Другие вопросы по тегам