技术解读
项目背景与痛点
在 Android 体系里,Spannable 是一个典型的“能力很强、但用起来很痛”的 API。setSpan(obj, start, end, flags) 采用绝对字符索引,只要文本内容发生任何改动——追加一个字符、插入一个表情、过滤一个关键词——之前所有 span 的区间都会错位。于是大多数业务代码最终退化成这样:手动维护一张区间表,一边拼接文本,一边计算偏移量,再一边调用 setSpan,稍微复杂一点就会失控。
更麻烦的是三个高频需求原生几乎不提供支持:
- 正则高亮:你想把一段文本里所有
#话题#、@某人、URL 都染色并加点击事件,得自己跑Matcher、自己遍历、自己处理重叠区间。 - 图文混排:
ImageSpan只认Drawable,不接受 GIF,不接受异步加载的网络图,表情包替换更是要自己做占位符和尺寸测量。 - 动态表情:原生
ImageSpan绘制的是静态帧,动图必须自定义Drawable,还要处理invalidate的触发节奏。
liangjingkanji/spannable 的切入点就在这。它并不试图重造轮子,而是一个把 Spannable 命令式 API 收敛成声明式 DSL 的封装层,让你先描述“我要什么效果”,再由库去算索引、装 Span、处理占位与绘制。
核心技术揭秘
这套库的设计思路可以概括为“两段式”:先确定最终文本,再统一装配 Span。这与原生“边拼边 set”的流式写法是根本区别,也是它能安全支持正则替换的前提。
几个值得拆开讲的点:
索引计算被集中到一处。 正则匹配阶段产出的是“区间集合”,图片与表情阶段产出的是“占位字符区间”,两者最终汇入同一个 SpannableStringBuilder。因为文本在装配前已经定型,索引不再有“越算越乱”的问题。这是它敢把正则作为核心特性的底气。
Span 工厂与配置分离。 样式声明(颜色、字号、点击回调、图片资源)是纯数据,Span 实例是运行时对象。分离之后,同一份样式声明复用于不同文本成为可能,也便于做缓存。
图片走占位符方案。 图文混排本质是把一个字符位替换成一块绘制区域,因此必须解决两件事:占位字符的宽度与 TextView 的基线对齐。这类库通常通过自定义 ReplacementSpan 或精细控制 ImageSpan 的对齐方式来收敛这个差异。
GIF 依赖绘制驱动。 动画不是「设一次就完」,而是要在 Drawable 的帧回调里请求重绘。工程上的关键不是让它动,而是让它只在可见时动——否则列表滚动时几十个动图同时跑,功耗和帧率都会失控。
功能亮点与差异
和 Spanny、RichText、Markwon 这类选手比,它的定位很清晰:
| 维度 | 典型同类库 | spannable |
|---|---|---|
| API 风格 | 链式调用、偏 Java | Kotlin DSL + 扩展函数 |
| 正则匹配 | 需自行处理 | 一等公民,直接声明 |
| 动图 / GIF | 基本不支持 | 原生支持 |
| 自定义表情包 | 需自行实现 | 内置路径 |
| 图文混排 | 支持但需调尺寸 | 封装了对齐与测量 |
真正的差异点是正则与表情被当成“一等需求”而非“扩展能力”。大多数富文本库假设你先有 HTML 或 Markdown,再做渲染;而它假设你先有一段纯文本和一组规则——这更贴近 IM、评论区、弹幕这类真实业务形态。
应用场景与落地建议
它最适合的场景:IM 消息气泡、评论/帖子正文、话题标签高亮、带表情的输入框、商品详情里的卖点标红。
生产环境要注意几件事:
- 不要在 RecyclerView 里跨 item 缓存
CharSequence。 带 Span 的对象持有Drawable、ClickableSpan甚至Context引用,缓存不当直接内存泄漏。 - 正则要防灾难性回溯。 用户输入不可信,带嵌套量词的模式在长文本上可能卡住主线程,务必限定长度或预编译。
- 长文本分批装配。 几千字的正文一次性算区间 +
setSpan,会有可感知的掉帧,建议分段构建。 - GIF 的生命周期管控。 屏幕外的动图必须停帧,
onDetachedFromWindow时要真正释放,而不只是暂停。 - 注意复制粘贴的语义退化。 图片、表情在纯文本剪贴板里会还原成占位字符,如果业务涉及分享导出,需要单独做文本降级。
综合评价
优点:API 现代、贴合 Kotlin 生态;正则、动图、表情这三块原生最难啃的骨头被系统性解决了;作者 liangjingkanji 在 Android 圈有持续输出(BRV 等),中文文档和 issue 响应是可预期的。
缺点:本质仍是 Spannable 的封装,没有绕开 TextView 的固有性能天花板;能力覆盖面广意味着抽象层更厚,出问题时的调试成本高于直接写原生 span;GIF 支持的收益与功耗代价需要业务自己权衡。
一句话总结:如果你正在为“正则高亮 + 表情混排”写第三遍重复的索引计算代码,这个库值得直接换掉你的手写方案;但如果你只想要几处简单的文字染色,引入它反而是过度设计。

评论