在客戶端開發中,我們往往需要對一個TextView的文字的部分內容進行特殊化處理,比如加粗、改變顏色、加鏈接、下划線等。iOS為我們提供了AttributedString
,而Android則提供了SpannableString
。今天我們就來看看SpannableString
的主角span
。
在Android的android.text.style
包下為我們提供了各種各樣的span,如:ForegroundColorSpan
、ImageSpan
、ClickableSpan
等等。網上已經有着很多使用這些span的教程了,所以沒必要在這里繼續探討這些基礎使用了。今天主要分析的是一個可以高度自定義化的span:ReplacementSpan
。
為何要自定義化?這是因為在實際需求上,已存在的組件很多時候不能滿足設計的需求。比如我們需要把TextView中一些詞匯變為帶圓角標簽的tag;又比如我們要給一段文字分段,但是段間距要精確到像素而不能簡簡單單用\n
解決。遇到這種需求時,當然我們可以把它拆分成很多個子view去完成,但是我們也可以選擇優雅的用自定義span去完成。
為何ReplacementSpan
? ReplacementSpan
是系統提供給我們的一個抽象類。通過名字我們可以知道其實用於是用於替換。指示我們可以把文本的某一部分替換成我們想要的內容。這也許是我們想要的。
Relpacement
的定義很簡單:
public abstract class ReplacementSpan extends MetricAffectingSpan { public abstract int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm); public abstract void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint); public void updateMeasureState(TextPaint p) { } public void updateDrawState(TextPaint ds) { } }
我們在繼承它的時候,需要實現兩個方法getSize
和draw
。通過方法名,我們也許能夠知道其作用:getSize
用於確定span的大小,draw
用於繪制我們想要的內容。
但是問題來了,這些方法的傳參是什么?為何getSize只返回了一個int值?
了解了這兩個問題,就基本弄懂了自定義span。來回答這兩個問題前,我們首先要明確的一件事情是:span是用於SpannableString
中,並且最終被用於TextView
中。所以在定義span時,我們的大小、繪制內容都應該依賴於使用時的環境。我們假設自定義span使用的環境為A
,那么A將包換一些信息,例如:baseline、Paint、FontMetricsInt等信息。
那我們現在來看看getSize
方法。getSize
的返回值是int,其實這個值指的是自定義span的寬度,那它的高度呢?其實高度是已知的,那就是外界環境A帶來的字的高度。但我某些情況我們希望改變span的高度,我們該怎么做呢? 如果對Android上字體繪制有一定了解的同學會知道,一個字的高度取決於繪制這個子的Paint.FontMetricsInt,而Paint.FontMetricsInt又有asent
和desent
來表示字體baseline以上的高度和以下的高度。如果對字體繪制與測量不清楚的同學可以看看這篇博文:http://mikewang.blog.51cto.com/3826268/871765 。 而getSize
方法的參數中有Paint.FontMetricsInt,那我們是否就可以通過改變傳入的Paint.FontMetricsInt的asent
和desent
來達到改變高度的目的呢?答案是可行的。我們可以來看看官方ImageSpan
(ImageSpan繼承自DynamicDrawableSpan,而DynamicDrawableSpan繼承自Replacement,下面的方法由DynamicDrawableSpan實現的)的實現:
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { Drawable d = getCachedDrawable(); Rect rect = d.getBounds(); if (fm != null) { fm.ascent = -rect.bottom; fm.descent = 0; fm.top = fm.ascent; fm.bottom = 0; } return rect.right; }
我們可以看到ImageSpan
就是用這種做法改變高度,這也可以代表官方的做法了。
知道size之后那我們來看看如何繪制了。draw
方法有一堆的參數,但源碼並沒有給出各個參數的作用,這里我直接指出:
- CharSequence text, int start, int end: 這三個用來指示想要替換的原文本text、替換的起始位置start和結束為止end。
- float x: 替換文本的起始坐標位置。
- int y: 作用環境A的baseline。
- int top, int bottom: 這個與上一步getSize有關,指示span可繪制區域的top和bottom。
在知道這些參數之后,我們就可以根據業務需求實現想要的span,下面給出自己在業務上使用的圓角tag型的span:
public class RadiusBackgroundSpan extends ReplacementSpan { private int mBgColor; private int mRadius; private int mTextColor; private int mTextSize; private int mPaddingHorizontal; private String mText; private int mMarginLeft; private int mMarginRight; public RadiusBackgroundSpan(int bgColor, int radius, int textColor, int textSize, int paddingHorizontal, String text){ mBgColor = bgColor; mRadius = radius; mTextColor = textColor; mTextSize = textSize; mPaddingHorizontal = paddingHorizontal; mText = text; mMarginLeft = UIUtil.dpToPx(4); mMarginRight = UIUtil.dpToPx(4); } @Override public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { paint.setTextSize(mTextSize); return (int) paint.measureText(mText) + 2*mPaddingHorizontal + mMarginLeft + mMarginRight; } @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { final int offsetToTop = UIUtil.dpToPx(2); paint.setTextSize(mTextSize); paint.setAntiAlias(true); RectF rect = new RectF(); rect.left = (int) x + mMarginLeft; Paint.FontMetricsInt fontMetrics = paint.getFontMetricsInt(); int marginVertical = (bottom - top - fontMetrics.descent + fontMetrics.top)/2; rect.top = top + marginVertical - offsetToTop; //視覺感覺偏下了,往上一點點 rect.bottom = bottom - marginVertical; rect.right = rect.left + (int) paint.measureText(mText) + 2*mPaddingHorizontal; paint.setColor(mBgColor); canvas.drawRoundRect(rect, mRadius, mRadius, paint); paint.setColor(mTextColor); float fontShouldOffsetTop = ((fontMetrics.descent - fontMetrics.ascent)/2+fontMetrics.ascent)/2 - offsetToTop/2; canvas.drawText(mText,x+mPaddingHorizontal+mMarginLeft,y + fontShouldOffsetTop,paint); } }