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

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

相关推荐
安卓理事人41 分钟前
安卓LinkedBlockingQueue消息队列
android
万能的小裴同学2 小时前
Android M3U8视频播放器
android·音视频
q***57742 小时前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober3 小时前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿4 小时前
关于ObjectAnimator
android
zhangphil4 小时前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我5 小时前
从头写一个自己的app
android·前端·flutter
lichong9516 小时前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端
用户69371750013847 小时前
14.Kotlin 类:类的形态(一):抽象类 (Abstract Class)
android·后端·kotlin
火柴就是我7 小时前
NekoBoxForAndroid 编译libcore.aar
android