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

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

相关推荐
Danny_姜7 小时前
Android Studio Quail 4 稳定版发布:本地 Gemma 4 + Android Skills
android·ide·android studio
平头哥AI16 小时前
Day 22 _ 包装错误别丢链_%w、errors.Is 与 errors.As
android·服务器·学习·golang·go
聚美智数17 小时前
图片广告检测-图片审核-图片广告识别-图像广告检测
android
早睡早起身体好12320 小时前
用 XGrammar 约束大模型工具调用:解决参数为空的问题
android·人工智能·神经网络·机器学习·自然语言处理·vllm
大佐不会说日语~21 小时前
Android 16 下 uni-app APP 提示“网络连接失败”的排障与修复
android·uni-app
2501_915106321 天前
苹果App Store上架费用及流程全面解析
android·ios·小程序·https·uni-app·iphone·webview
YF02111 天前
Android 16系统App开机自启动方案
android
hm宋1 天前
安卓手机卡顿优化 —— 背景与使用指南
android
OxYGC1 天前
[Android] [WatchOS]智能手表第一篇: 实用工具与应用推荐
android·智能手表
嘟哩DuliDuli1 天前
创作 Agent 的任务状态该如何保存
android·java·javascript