Изменение цвета текста одного ClickableSpan при нажатии, не затрагивая другие ClickableSpan в том же 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>

person Steven Meliopoulos    schedule 31.12.2013    source источник
comment
Здесь вам нужно будет использовать объект Spannable для текста, есть пример. API: developer.android.com/reference/android/text/Spannable.html stackoverflow.com/ вопросы / 3282940 /.   -  person Kanak Sony    schedule 31.12.2013
comment
Я, конечно, делаю все вышеперечисленное на Spannable. И связанный пример показывает только, как установить цвет диапазона, что здесь не проблема. Я хочу, чтобы цвет диапазона менялся при нажатии.   -  person Steven Meliopoulos    schedule 31.12.2013
comment
afaik ClickableSpan не поддерживает его напрямую   -  person pskink    schedule 31.12.2013
comment
Что ж, тогда обидно. Похоже, что это такая простая вещь, исходя из CSS. В этом случае я, вероятно, просто воспользуюсь textColorHighlight.   -  person Steven Meliopoulos    schedule 31.12.2013
comment
К счастью, я ошибся, более того, вы можете изменить не только цвет ссылки (updateDrawState), но и цвет фона (SpanWatcher)   -  person pskink    schedule 31.12.2013
comment
Не могли бы вы рассказать, как использовать updateDrawState и SpanWatcher для этой цели? Например, как я могу определить, нажат ли диапазон в updateDrawState?   -  person Steven Meliopoulos    schedule 31.12.2013


Ответы (7)


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

Это мой модифицированный 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);
    }
}
person Steven Meliopoulos    schedule 03.01.2014
comment
приятно, одна вещь: установка ds.bgColor на 0xffffffff полностью скрывает выбор по умолчанию (полезно при использовании стрелок dpad для навигации по ссылкам) - person pskink; 03.01.2014
comment
Спасибо за подсказку. Я изменил его на 0xffeeeeee, как в вашем примере. Но почему это имеет значение? Разве они оба не полностью прозрачны? - person Steven Meliopoulos; 03.01.2014
comment
ну, они полностью непрозрачны, так как 0xff ******, это должно быть что-то вроде 0xaa ****** - person pskink; 03.01.2014
comment
Отличное решение! Чтобы избежать создания бесчисленных LinkTouchMovementMethod объектов, я бы переопределил статический метод LinkMovementMethod.getInstance(), чтобы в любой момент возвращать единственный экземпляр. - person jenzz; 18.08.2014
comment
Я последовал этому примеру, и он сработал очень хорошо. Однако цветная область сенсорной обратной связи находится не по центру текста. Вроде бы по вертикали ниже середины текста. Когда я выбираю отображение границ макета в настройках разработчика, вокруг каждого диапазона нет рамки. Кто-нибудь знает, как это можно изменить? - person Russ Wheeler; 09.04.2015
comment
@steven Meliopoulos, спасибо за ваш красивый код, но он не работает на android M, пожалуйста, изучите проблему, я задал вопрос о stackoverflow, ссылка на который выглядит следующим образом: stackoverflow.com/questions/34737514/ - person Reprator; 12.01.2016
comment
@Steven Meliopoulos, я решил свою проблему, преобразовав текстовое представление в кнопку, но, пожалуйста, помогите мне, так как я не могу знать, почему теперь он работает с текстовым просмотром на android M. - person Reprator; 13.01.2016
comment
@VikramSingh Я тестировал его на M, и у меня он отлично работает. - person wrozwad; 04.02.2016
comment
Примите мой голос, сэр. Черт возьми, такая простая вещь, как желание, чтобы ссылка работала как ссылка. Протестировано при работе на Android O. - person Jeffrey Blattman; 03.07.2020

Гораздо более простое решение, 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);
    }
};
person zundi    schedule 17.03.2016
comment
Это не только устанавливает цвет для нажатого состояния, но и все - person lilienberg; 18.05.2018
comment
Это НЕ решает рассматриваемую проблему. - person Gustavo Baiocchi Costa; 26.02.2020

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

Просто установите 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.

person j2emanue    schedule 20.10.2014
comment
selector цвет работает хорошо, только если у нас есть только 1 ссылка, если у нас есть 2 ссылки, обе ссылки мы выделяем в одно и то же время, когда мы нажимали - person Linh; 02.04.2019

Ответ 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.

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

person Maksim Dmitriev    schedule 03.12.2014

попробуйте этот настраиваемый 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);
person pskink    schedule 31.12.2013
comment
Спасибо за ответ. Я попробовал, но это не сработало так, как я ожидал. В частности, ссылки не возвращались в исходное состояние, когда они были выпущены или когда позиция касания переместилась за пределы ссылки, поэтому в конце концов я выбрал версию, которую опубликовал выше. - person Steven Meliopoulos; 03.01.2014

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

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);
  }
}
person Carlos Gómez    schedule 07.02.2016

Поместите 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>

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

person kathir    schedule 31.12.2013
comment
Разве это не изменило бы цвет всего TextView? - person Steven Meliopoulos; 31.12.2013
comment
Цель состоит в том, чтобы изменить только цвет нажатого ClickableSpan. Остальная часть TextView должна остаться без изменений. - person Steven Meliopoulos; 31.12.2013