【Android】SpannableStringBuilder setSpan 小坑分享

最近开发时遇到个有趣的问题,分享一下以作记录:

问题提出

如果我们需要给一段文本中的一部分加入点击监听,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);

再次运行,可以发现两个地方都可以正常触发点击了

相关推荐
simplepeng8 小时前
我的天,我真是和androidx的字体加载杠上了
android
小猫猫猫◍˃ᵕ˂◍9 小时前
备忘录模式:快速恢复原始数据
android·java·备忘录模式
CYRUS_STUDIO11 小时前
使用 AndroidNativeEmu 调用 JNI 函数
android·逆向·汇编语言
梦否11 小时前
【Android】类加载器&热修复-随记
android
徒步青云12 小时前
Java内存模型
android
今阳12 小时前
鸿蒙开发笔记-6-装饰器之@Require装饰器,@Reusable装饰器
android·app·harmonyos
-优势在我17 小时前
Android TabLayout 实现随意控制item之间的间距
android·java·ui
hedalei17 小时前
android13修改系统Launcher不跟随重力感应旋转
android·launcher
Indoraptor18 小时前
Android Fence 同步框架
android
峥嵘life18 小时前
DeepSeek本地搭建 和 Android
android