前言
前段时间看过一篇 实现类似新浪微博帖子显示(2)——话题、@好友、表情解析工具类类似这种富文本的显示我们一般首先就会想到SpannableString
,原作者也是这样实现的,就想着整理一下 相关知识.
SpannableString与SpannableStringBuilder
从名称来看一个是 builder
, 一个是 string
,很好理解.关于 SpannableString,SpannableStringBuilder及String
的区别可以看这里
SpannableString与SpannableStringBuilder,SpannableStringBuilder
可以通过append()
方法来动态改变其内部的值,与String
不同的是前两者都可以通过span
来添加额外的信息.
SetSpan方法
方法声明如下:
void setSpan (Object what, int start, int end, int flags);
参数说明:
- what : 文本格式,可以设置成前景色,背景色,下划线,中划线,模糊等
- start : 字符串设置格式的起始下标
- end : 字符串设置格式结束下标
- flags : 标识
flags: 包含四种情况,用四个常量控制
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
从起始下标到结束下标,包括起始下标不包含结束坐标Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
从起始下标到结束下标,但都不包括起始下标和结束下标Spanned.SPAN_INCLUSIVE_INCLUSIVE
从起始下标到终了下标,同时包括起始下标和结束下标Spanned.SPAN_EXCLUSIVE_INCLUSIVE
从起始下标到终了下标,包括结束下标不包含起始坐标
Spans框架
关于Android 平台上的Spans
架构,可以查看Spans, a Powerful Concept.
, Spans
主要分为四个层次
- 如果一个Span影响字符级的文本格式,则继承CharacterStyle.包括我们常用的
ForegroundColorSpan,BackgroundColorSpan
. - 如果一个Span影响段落层次的文本格式,则实现ParagraphStyle.包括了
BulletSpan
,继承自LeadingMarginSpan
- 如果一个Span修改字符级别的文本外观,则实现UpdateAppearance.包括了
ClickableSpan
- 如果一个Span修改字符级文本度量|大小,则实现UpdateLayout.包括了
AbsoluteSizeSpan
具体其继承结构可通过查看其Hierarchy
扩展阅读:
-
上面的中文译文: Spans,一个强大的概念
-
Android Span 架构介绍:介绍了多种
Spans
的用法.
工作原理
当你给一个TextView
设置文本时,它使用Layout去管理文本的渲染。Layout
包含有三个子类.分别是
BoringLayout
:负责显示单行文本,isBoring
用于判断是否是单行文本.DynamicLayout
:负责渲染Spannable
,且内部会设置SpanWatcher
,有soan
的时候会reflow
,进而重新计算布局.StaticLayout
: 单行文本,且非Spannable
的时候,不会监听span
变化,效率较DynamicLayout
高.里面处理了换行.
Layout.draw()
会负责文本的绘制,其中drawBackground
会递归LineBackgroundSpan
并调用lineBackgroundSpan.drawBackground
来进行背景的绘制.drawText
则会首先通过TextLine.obtain()
生成TextLine
,如果是文本,则调用canvas.drawText
绘制,如果包含了Spannble,emoji
,则交给TextLine
绘制.TextLine#draw()
会调用drawRun
,进而调用handleRun()
进行文本的渲染.TextLayoutCache
为了提高效率,在4.0之后加入.- 如果想要提升
TextView
的渲染效率,可以使用StaticLayout
.
扩展阅读:
自定义Span
除了上述四类 Span
,我们还可以自定义Span
,自定义Span
如同自定义View
一样.可以继承已有的Span
(扩展),也可以通过继承抽象了或者接口完全自定义.
- 扩展已有的Span
类似demo中扩展ForegroundColorSpan
来实现的ActionBar
的淡入效果.这里需要借助属性动画的相关知识.
一般需要复写updateDrawState
,getSize
或者draw
方法.
public void updateDrawState(TextPaint ds){}
updateDrawState
方法最终会被TextLine#handleRun()
方法调用
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {}
getSize()
方法,返回新的更换Span后的size
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
//draw something
canvas.drawText(text, start, end, x, y, paint);
}
draw
使用Canvas
绘制一些文本之外的东西.可以是 背景
…
- 完全自定义
通过源码我们发现CharacterStyle,ParagraphStyle.UpdateAppearance和UpdateLayout
都是一个空接口.我们要实现自定义Span
,则只有继承自其子类,一般是MetricAffectingSpan,ReplacementSpan,LineBackgroundSpan
比如,如果需要自定义背景则可以继承LineBackgroundSpan
,就像Demo中的LetterLineBackgroundSpan
学习demo:flavienlaurent/spans,里面列举了各种
Span
的用法.
FontMetrics
就是所谓的字体规格,如下图所示
FontMetrics
是Paint
的内部类,里面包含了一些关于字体的常量.
其中Baseline(基线)
,ascent(上坡度)
,descent(下坡度)
,leading(行间距)
,这些常量集合canvas
使得我们的绘制工作变得更加的自由
而我们绘制文字的时候一般使用的是TextPaint
这个继承自Paint
的类.
//获取文本宽度
TextPaint textPaint = new TextPaint();
paint.setTextSize(size);//设置字体大小
paint.setTypeface(Typeface.xx);//设置字体
float width = Layout.getDesiredWidth(str,textPaint);
图文混排居中
比如,我们在使用ImageSpan
的时候,并且使用了lineSpacingExtra
来设置行间距后,会出现图片下沉,即图片和文字不再一条线上,这时候就可以通过FontMetrics
来设置改变.如,评论中插入 图像,将span
上移即可
- 解决方法
public class EmojiSpan extends ImageSpan {
public EmojiSpan(Bitmap drawable) {
super(drawable);
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text,
int start, int end, float x,
int top, int y, int bottom, @NonNull Paint paint) {
// image to draw
Drawable b = getDrawable();
// font metrics of text to be replaced
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
int transY = (y + fm.descent + y + fm.ascent) / 2
- b.getBounds().bottom / 2;
canvas.save();
canvas.translate(x, transY);
b.draw(canvas);
canvas.restore();
}
}
扩展阅读