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

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

相关推荐
apihz27 分钟前
域名WHOIS信息查询免费API使用指南
android·开发语言·数据库·网络协议·tcp/ip
问道飞鱼44 分钟前
【移动端知识】移动端多 WebView 互访方案:Android、iOS 与鸿蒙实现
android·ios·harmonyos·多webview互访
aningxiaoxixi1 小时前
Android 之 audiotrack
android
枷锁—sha1 小时前
【DVWA系列】——CSRF——Medium详细教程
android·服务器·前端·web安全·网络安全·csrf
Cao_Shixin攻城狮5 小时前
Flutter运行Android项目时显示java版本不兼容(Unsupported class file major version 65)的处理
android·java·flutter
呼啦啦呼啦啦啦啦啦啦8 小时前
利用pdfjs实现的pdf预览简单demo(包含翻页功能)
android·javascript·pdf
idjl10 小时前
Mysql测试题
android·adb
游戏开发爱好者812 小时前
iOS App 电池消耗管理与优化 提升用户体验的完整指南
android·ios·小程序·https·uni-app·iphone·webview
人生游戏牛马NPC1号13 小时前
学习 Flutter (四):玩安卓项目实战 - 中
android·学习·flutter
星辰也为你祝福h14 小时前
Android原生Dialog
android