Android中的SpannableString,Spans以及TextView绘制原理

前言

前段时间看过一篇 实现类似新浪微博帖子显示(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

扩展阅读:

工作原理

当你给一个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

FontMetricsPaint的内部类,里面包含了一些关于字体的常量.

其中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上移即可

emoji下沉效果

  • 解决方法
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();
  }
}

扩展阅读

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android SpannableString 可以解决英文混排导致的提前换行问题,其原理是通过设置 SpannableString 对象的 LineHeightSpan 来实现的。LineHeightSpan 是一个接口,用于设置文本的行高。当 TextView绘制文本时,会调用 LineHeightSpan 的 chooseHeight() 方法来获取文本的行高。通过设置 LineHeightSpan 来实现文本的行高设置,可以避免英文混排导致的提前换行问题。 以下是相关的系统源码和完整的解释说明: 在 Android TextView 绘制文本的过程,会调用静态方法 StaticLayout.Builder.obtain() 来创建 StaticLayout 对象,然后调用 StaticLayout.draw() 方法来绘制文本。在 StaticLayout.Builder.obtain() 方法,会通过传入 LineHeightSpan 来设置文本的行高。具体代码如下: ```java public static Builder obtain(CharSequence source, int start, int end, TextPaint paint, int width) { Builder b = sBuilderPool.acquire(); if (b == null) { b = new Builder(); } b.mSource = source; b.mStart = start; b.mEnd = end; b.mPaint = paint; b.mWidth = width; b.mAlignment = Alignment.ALIGN_NORMAL; b.mSpacingMult = 1; b.mSpacingAdd = 0; b.mIncludePad = true; b.mEllipsizedWidth = width; b.mEllipsize = null; b.mMaxLines = Integer.MAX_VALUE; b.mBreakStrategy = Layout.BREAK_STRATEGY_BALANCED; b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE; // 设置 LineHeightSpan 来设置文本的行高 if (source instanceof Spanned) { Spanned spanned = (Spanned) source; LineHeightSpan[] spans = spanned.getSpans(start, end, LineHeightSpan.class); if (spans.length > 0) { b.mLineHeight = new LineHeightSpan.LeadingMarginSpan2[spans.length]; for (int i = 0; i < spans.length; i++) { b.mLineHeight[i] = new LineHeightSpan.LeadingMarginSpan2.Standard(spans[i]); } } } return b; } ``` 在上述代码,我们可以看到,如果 SpannableString 设置了 LineHeightSpan,那么会在 StaticLayout.Builder.obtain() 方法将其转换为 LeadingMarginSpan2,并设置到 StaticLayout.Builder 对象。 接下来,我们来看一下 LineHeightSpan 的实现。LineHeightSpan 是一个接口,其定义如下: ```java public interface LineHeightSpan extends ParagraphStyle { void chooseHeight(CharSequence text, int start, int end, int istartv, int v, Paint.FontMetricsInt fm); } ``` 其,chooseHeight() 方法用于设置文本的行高。在 StaticLayout.draw() 方法,会调用 LineHeightSpan 的 chooseHeight() 方法来获取文本的行高。具体代码如下: ```java private void drawText(Canvas canvas, CharSequence buffer, int start, int end, float x, int top, int y, int bottom, Paint paint, boolean hasTab, Layout.TabStops tabStops) { ... int v = top; // 循环绘制每一行文本 for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) { int startLine = getLineStart(lineNum); int endLine = getLineEnd(lineNum); // 获取文本的行高 int above = getLineTop(lineNum); int below = getLineBottom(lineNum); // 调用 LineHeightSpan 的 chooseHeight() 方法来获取文本的行高 for (int i = 0; i < spans.length; i++) { if (spans[i] instanceof LineHeightSpan) { ((LineHeightSpan) spans[i]).chooseHeight(buffer, startLine, endLine, startLine, v, fm); } } // 绘制文本 ... } ... } ``` 在上述代码,我们可以看到,循环绘制每一行文本时,会调用 LineHeightSpan 的 chooseHeight() 方法来获取文本的行高,从而实现对文本的行高设置。 综上所述,通过设置 SpannableString 对象的 LineHeightSpan,可以实现对文本的行高设置,从而避免英文混排导致的提前换行问题。在 TextView 绘制文本的过程,会调用 LineHeightSpan 的 chooseHeight() 方法来获取文本的行高,从而实现对文本的行高设置。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值