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

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

相关推荐
丘狸尾2 小时前
[cisco 模拟器] ftp服务器配置
android·运维·服务器
van叶~4 小时前
探索未来编程:仓颉语言的优雅设计与无限可能
android·java·数据库·仓颉
Crossoads8 小时前
【汇编语言】端口 —— 「从端口到时间:一文了解CMOS RAM与汇编指令的交汇」
android·java·汇编·深度学习·网络协议·机器学习·汇编语言
li_liuliu9 小时前
Android4.4 在系统中添加自己的System Service
android
C4rpeDime11 小时前
自建MD5解密平台-续
android
鲤籽鲲13 小时前
C# Random 随机数 全面解析
android·java·c#
m0_5485147717 小时前
2024.12.10——攻防世界Web_php_include
android·前端·php
凤邪摩羯17 小时前
Android-性能优化-03-启动优化-启动耗时
android
凤邪摩羯17 小时前
Android-性能优化-02-内存优化-LeakCanary原理解析
android
喀什酱豆腐18 小时前
Handle
android