最近开发时遇到个有趣的问题,分享一下以作记录:
问题提出
如果我们需要给一段文本中的一部分加入点击监听,SpannableStringBuilder 结合 ClickableSpan 实现起来会非常顺畅. 比如我们给 水果,是指多汁且主要味觉为甜味和酸味,可食用的植物果实。水果不但含有丰富的维生素营养,而且能够促进消化。 这段文字中的甜味,增加一个点击监听,我们会有如下实现:
java
TextView tv = findViewById(R.id.tv_text);
SpannableStringBuilder stringBuilderDesc = new SpannableStringBuilder();
String textDesc = "水果,是指多汁且主要味觉为甜味和酸味,可食用的植物果实。水果不但含有丰富的维生素营养,而且能够促进消化。";
stringBuilderDesc.append(textDesc);
ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
Toast.makeText(MainActivity.this, "是一种味道", Toast.LENGTH_SHORT).show();
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
//取消下划线
ds.setUnderlineText(false);
}
};
int indexDesc = textDesc.indexOf("甜");
if(indexDesc != -1) {
stringBuilderDesc.setSpan(clickableSpan, indexDesc, indexDesc + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
运行后,一切正常,目的达到。
如果我们还需要给"酸味"也加入相同的点击事件,那也应当照葫芦画瓢即可。
java
TextView tv = findViewById(R.id.tv_text);
SpannableStringBuilder stringBuilderDesc = new SpannableStringBuilder();
String textDesc = "水果,是指多汁且主要味觉为甜味和酸味,可食用的植物果实。水果不但含有丰富的维生素营养,而且能够促进消化。";
stringBuilderDesc.append(textDesc);
ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
Toast.makeText(MainActivity.this, "是一种味道", Toast.LENGTH_SHORT).show();
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
//取消下划线
ds.setUnderlineText(false);
}
};
int indexDesc = textDesc.indexOf("甜");
if(indexDesc != -1) {
stringBuilderDesc.setSpan(clickableSpan, indexDesc, indexDesc + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
indexDesc = textDesc.indexOf("酸");
if(indexDesc != -1) {
stringBuilderDesc.setSpan(clickableSpan, indexDesc, indexDesc + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
tv.setHighlightColor(Color.TRANSPARENT);
tv.setMovementMethod(LinkMovementMethod.getInstance());
tv.setText(stringBuilderDesc);
运行后发现,酸味点击响应正常,但甜味点击却没了反应,什么原因?
分析原因
ClickableSpan 本身的原因?
那我们用其他类型的Span试一试,ForegroundColorSpan 试试呢?
java
TextView tv = findViewById(R.id.tv_text);
SpannableStringBuilder stringBuilderDesc = new SpannableStringBuilder();
String textDesc = "水果,是指多汁且主要味觉为甜味和酸味,可食用的植物果实。水果不但含有丰富的维生素营养,而且能够促进消化。";
stringBuilderDesc.append(textDesc);
ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(Color.parseColor("#ff507daf"));
int indexDesc = textDesc.indexOf("甜");
if(indexDesc != -1) {
stringBuilderDesc.setSpan(foregroundColorSpan, indexDesc, indexDesc + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
indexDesc = textDesc.indexOf("酸");
if(indexDesc != -1) {
stringBuilderDesc.setSpan(foregroundColorSpan, indexDesc, indexDesc + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
tv.setHighlightColor(Color.TRANSPARENT);
tv.setMovementMethod(LinkMovementMethod.getInstance());
tv.setText(stringBuilderDesc);
好嘛,甜味的颜色也失效了,那看来并不是ClickSpan的问题,而是SpannableStringBuilder本身的问题。
setSpan
我们看看setSpan()这个方法具体是怎么将传入的span设置下来的,
查看源码,我们发现有这么一段代码:
其中,what是传进来的Span对象,mIndexOfSpan 是存放设置进来的Span的Map,而send默认为true。 这里我们发现,如果传入的span对象已经存在于mIndexOfSpan,会取出原本设置的start与end位置,并将新传入的一并传到 sendSpanChanged 方法中。继续往下看,我们发现,如果不存在与map中,最终调用的是sendSpanAdded方法。
我们点进来看看sendSpanChanged干了什么:
该方法的名称和逻辑已经向我们基本透露了它的作用了------更改已存在的span的生效位置,我们接着查看这个监听做了什么:
可以看到我们最终会走回removeSpan方法,将老span"移除掉"。
解决方法
那解决方法也就随之浮出水面了,只要保证我们给同一builder中的不同文本设置的Span不要是同一个对象,在判断是不要走到"changed"的逻辑就好了,调整代码:
java
TextView tv = findViewById(R.id.tv_text);
SpannableStringBuilder stringBuilderDesc = new SpannableStringBuilder();
String textDesc = "水果,是指多汁且主要味觉为甜味和酸味,可食用的植物果实。水果不但含有丰富的维生素营养,而且能够促进消化。";
stringBuilderDesc.append(textDesc);
ClickableSpan clickableSpan1 = new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
Toast.makeText(MainActivity.this, "是一种味道", Toast.LENGTH_SHORT).show();
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
//取消下划线
ds.setUnderlineText(false);
}
};
ClickableSpan clickableSpan2 = new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
Toast.makeText(MainActivity.this, "是一种味道", Toast.LENGTH_SHORT).show();
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
//取消下划线
ds.setUnderlineText(false);
}
};
int indexDesc = textDesc.indexOf("甜");
if(indexDesc != -1) {
stringBuilderDesc.setSpan(clickableSpan1, indexDesc, indexDesc + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
indexDesc = textDesc.indexOf("酸");
if(indexDesc != -1) {
stringBuilderDesc.setSpan(clickableSpan2, indexDesc, indexDesc + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
tv.setHighlightColor(Color.TRANSPARENT);
tv.setMovementMethod(LinkMovementMethod.getInstance());
tv.setText(stringBuilderDesc);
再次运行,可以发现两个地方都可以正常触发点击了