天天看點

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

擴充閱讀:

  • 上面的中文譯文: 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

      .

擴充閱讀:

  • 關于TextView的一些初步講解
  • android staticlayout使用講解
  • [譯]Instagram是如何提升TextView渲染性能的
  • TextView預渲染研究

自定義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

就是所謂的字型規格,如下圖所示

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

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

上移即可

Android中的SpannableString,Spans以及TextView繪制原理
  • 解決方法
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) / 
        - b.getBounds().bottom / ;

    canvas.save();
    canvas.translate(x, transY);
    b.draw(canvas);
    canvas.restore();
  }
}
           

擴充閱讀

  • Android中測量Text的寬度和高度
  • 用TextPaint來繪制文字
  • Android自定義View使用canvas繪制文字實作居中、自動換行

繼續閱讀