Android:解讀BoringLayout

A BoringLayout is a very simple Layout implementation for text that fits on a single line and is all left-to-right characters. You will probably never want to make one of these yourself; if you do, be sure to call isBoring(CharSequence, TextPaint) first to make sure the text meets the criteria.

This class is used by widgets to control text layout. You should not need to use this class directly unless you are implementing your own widget or custom display object, in which case you are encouraged to use a Layout instead of calling Canvas.drawText() directly.

上面是 BoringLayout 的官方解釋。意思是說BoringLayout是一個非常簡單的佈局,是對單行且是LTR方向的文本進行適配的。不建議開發者自己使用,如果用的話,要確保通過isBoring判斷文本是否符合要求。除非你自己開發自定義的widget或者自定義的顯示對象,否則不建議直接使用。

我們在 中提到過,TextView內部Layout創建的過程,也看到了BoringLayout的創建入口(TextViewmakeSingleLayout方法中)。這一篇文章就來具體講講BoringLayout的工作過程。

假如有一個TextView(寬度:200dp,高度:100dp),手機屏幕密度是4,所以轉化成像素就是(寬度:800px,高度:400px)。

makeSingleLayout方法中,先判斷如果不符合

DynamicLayout的創建規則,那就會通過BoringLayoutisBoring來檢測TextView中的文本是否符合單行顯示的要求。isBoring其實返回的是一個度量對象Metrics。那isBoring測量的是什麼呢?

isBoring測量的結果表示,如果TextView中所有的文字,都顯示在一行中,需要多寬多高的顯示區域,然後用這個結果和佈局文件中設置的寬度進行比較,看看能否放得下(小於800px就能放得下),如果能,就會創建BoringLayout佈局。

那我們就來看看isBoring

<code>public static Metrics isBoring(CharSequence text, TextPaint paint,
TextDirectionHeuristic textDir, Metrics metrics) {
final int textLength = text.length();
//先檢測是否存在一些有趣的字符,比如'\\n',\\t'啥的
if (hasAnyInterestingChars(text, textLength)) {
return null; // There are some interesting characters. Not boring.
}

//檢測文字方向,不允許從右到左,有的國家是這樣的
if (textDir != null && textDir.isRtl(text, 0, textLength)) {
return null; // The heuristic considers the whole text RTL. Not boring.
}
//如果是Spanned類型的,判斷是否設置了Span樣式,有的話,不行
if (text instanceof Spanned) {
Spanned sp = (Spanned) text;
Object[] styles = sp.getSpans(0, textLength, ParagraphStyle.class);
if (styles.length > 0) {
return null; // There are some PargraphStyle spans. Not boring.
}
}
//條件都滿足,開始創建
Metrics fm = metrics;
if (fm == null) {
fm = new Metrics();
} else {
fm.reset();
}
//TextLine對文字進行測量
TextLine line = TextLine.obtain();
line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,
Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
fm.width = (int) Math.ceil(line.metrics(fm));
TextLine.recycle(line);

return fm;
}/<code>

結果返回後,進行對比,滿足下面的條件後,就會創建BoringLayout

<code>if (boring == UNKNOWN_BORING) {
//檢查是否可以用BoringLayout
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
......
}
//如果boring不為空,那麼就要把測量結果和實際的顯示區域進行比較,來確定是否可以單行顯示

if (boring != null) {
//如果測量寬度不大於需要的寬度,
//並且設置了省略號位置或者測量寬度不大於帶省略號時的寬度
if (boring.width <= wantWidth
&& (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
......
}
//或者需要處理省略號,並且寬度滿足
//比如在佈局中設置了singleLine
else if (shouldEllipsize && boring.width <= wantWidth) {
......
}
}/<code>

第一個對比條件就是沒有設置單行顯示,並且所有文本一行就可以放的下。

第二個對比條件就是,比如你設置了ellipsize。

如果滿足其中一個條件,表示可以用BoringLayout來處理文本顯示,那麼就會使用BoringLayoutreplaceOrMakemake來創建佈局。makeSingleLayout中useSaved參數如果為true,那麼就會把創建好的佈局保存,以後如果再次使用時,可以用replaceOrMake來處理。

<code>if (useSaved) {
mSavedLayout = (BoringLayout) result;
}/<code>

replaceOrMake內部和make一樣,都是調用的基類的replaceWith方法,所以我們來具體看看make的過程。

<code>public static BoringLayout make(CharSequence source,
TextPaint paint, int outerwidth,
Alignment align,
float spacingmult, float spacingadd,
BoringLayout.Metrics metrics, boolean includepad,
TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
return new BoringLayout(source, paint, outerwidth, align,
spacingmult, spacingadd, metrics,
includepad, ellipsize, ellipsizedWidth);
}/<code>

靜態方法make中調用了BoringLayout的構造方法,創建實例。

<code>public BoringLayout(CharSequence source,
TextPaint paint, int outerwidth,
Alignment align,
float spacingmult, float spacingadd,
BoringLayout.Metrics metrics, boolean includepad,
TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
/*
* 調用基類的初始化,保存一些變量
*/
super(source, paint, outerwidth, align, spacingmult, spacingadd);

boolean trust;

//如果沒有設置省略號位置,或者不是跑馬燈類型
if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
mEllipsizedWidth = outerwidth;
mEllipsizedStart = 0;
mEllipsizedCount = 0;
trust = true;
} else {
/*replace基類方法,替換掉一些屬性
*TextUtils.ellipsize把文字處理成帶省略號的文字
*/
replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth,
ellipsize, true, this),
paint, outerwidth, align, spacingmult,
spacingadd);


mEllipsizedWidth = ellipsizedWidth;
trust = false;
}
//初始化工作
init(getText(), paint, outerwidth, align, spacingmult, spacingadd,
metrics, includepad, trust);
}/<code>

來看看TextUtilsellipsize方法。

<code>public static CharSequence ellipsize(CharSequence text,
TextPaint paint,
float avail, TruncateAt where,
boolean preserveLength,
EllipsizeCallback callback) {
return ellipsize(text, paint, avail, where, preserveLength, callback,
TextDirectionHeuristics.FIRSTSTRONG_LTR,
(where == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS_STRING : ELLIPSIS_STRING);
}/<code>

如果設定的是END_SMALL類型省略號,就用兩個點(..),否則用三個點(...)。

<code>public static CharSequence ellipsize(CharSequence text,
TextPaint paint,
float avail, TruncateAt where,
boolean preserveLength,
EllipsizeCallback callback,
TextDirectionHeuristic textDir, String ellipsis) {

int len = text.length();
//MeasuredText中會保留文字的一些相關信息,比如字數,長度,字符數組等
MeasuredText mt = MeasuredText.obtain();
try {
//和前面一樣,測量需要多寬能夠顯示
float width = setPara(mt, paint, text, 0, text.length(), textDir);
//和前面一樣的比較,如果能放下,觸發回調,直接返回
if (width <= avail) {
if (callback != null) {
callback.ellipsized(0, 0);
}

return text;
}
//省略號寬度
float ellipsiswid = paint.measureText(ellipsis);
//可用寬度減去省略號寬度,是實際用來顯示文字的寬度
avail -= ellipsiswid;

int left = 0;
int right = len;
if (avail < 0) {
// it all goes
} else if (where == TruncateAt.START) {
right = len - mt.breakText(len, false, avail);
} else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
//計算從第幾個字符開始,後面的就被省略了
left = mt.breakText(len, true, avail);
} else {
right = len - mt.breakText(len, false, avail / 2);
avail -= mt.measure(right, len);
left = mt.breakText(right, true, avail);

}

if (callback != null) {
callback.ellipsized(left, right);
}

char[] buf = mt.mChars;
Spanned sp = text instanceof Spanned ? (Spanned) text : null;
//計算剩下多少個字符用來顯示
int remaining = len - (right - left);
if (preserveLength) {
/*
*這裡填充buf數組,第left+1個填充成省略號,
*後面的都填充成指定的相同內容,
*表示都被省略了
*/
if (remaining > 0) { // else eliminate the ellipsis too
buf[left++] = ellipsis.charAt(0);
}
for (int i = left; i < right; i++) {
buf[i] = ZWNBS_CHAR;
}
//生成帶省略號的文字
String s = new String(buf, 0, len);
if (sp == null) {
return s;
}
SpannableString ss = new SpannableString(s);
copySpansFrom(sp, 0, len, Object.class, ss, 0);
return ss;
}

if (remaining == 0) {
return "";
}

......
return ssb;
} finally {
MeasuredText.recycle(mt);
}
}/<code>

下圖是

buf數組的截圖。

Android:解讀BoringLayout

這樣就得到了帶省略號的結果。

然後,通過replaceLayout內部的屬性更新,比如mText屬性更新為帶省略號的。這樣整個BoringLayout對象就創建完成了

提示:這時候你通過TextView的getText方法和TextView的getLayout().getText得到的結果就是不一樣的了。


分享到:


相關文章: