Android:解讀StaticLayout

StaticLayout用於佈局之後,不會再進行編輯的文本。

中提到過Layout創建的過程,所以這裡直接從TextViewmakeSingleLayout開始。

當判斷不滿足DynamicLayoutBoringLayout之後,就會開始StaticLayout的創建。

我們在佈局中對TextView進行如下設置:手機屏幕密度為4

<code>android:layout_width="200dp"
android:layout_height="100" style="height:100px"
android:ellipsize="end"
android:lines="2"/<code>

創建StaticLayout的代碼如下:

<code>if (result == null) {
StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
0, mTransformed.length(), mTextPaint, wantWidth)
.setAlignment(alignment)
.setTextDirection(mTextDir)
.setLineSpacing(mSpacingAdd, mSpacingMult)
.setIncludePad(mIncludePad)
.setBreakStrategy(mBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency)
.setJustificationMode(mJustificationMode)
.setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
if (shouldEllipsize) {

builder.setEllipsize(effectiveEllipsize)
.setEllipsizedWidth(ellipsisWidth);
}
// TODO: explore always setting maxLines
result = builder.build();
}/<code>

StaticLayout採用的是Builder模式,顯示進行一些屬性的設置,包括對齊方式、文字方向、行數、行間距等等。

因為我們設置了android:ellipsize="end",所以shouldEllipsize=true,這樣就會接著設置省略號位置以及寬度(我們沒有設置左右padding,所以ellipsisWidth=200dp*4=800像素)。

設置好後,通過builder.build()進行構建。

<code>public StaticLayout build() {
//創建佈局
StaticLayout result = new StaticLayout(this);
Builder.recycle(this);
return result;
}/<code>

我們來分步看一下StaticLayout的創建過程。

<code>private StaticLayout(Builder b) { 

super((b.mEllipsize == null)
? b.mText
: (b.mText instanceof Spanned)
? new SpannedEllipsizer(b.mText)
: new Ellipsizer(b.mText),
b.mPaint, b.mWidth, b.mAlignment, b.mSpacingMult, b.mSpacingAdd);
......
}/<code>

首先回調用基類的構造函數,第一個參數是CharSequence類型的,其實就是要顯示的文本內容。在基類的內部有一個內部靜態類Ellipsizer,用來組織省略內容的。

<code>static class Ellipsizer implements CharSequence, GetChars/<code>

上面語句表示,先判斷是否設置了android:ellipsize,如果沒有,就直接用text屬性內容,如果有,那麼就要再判斷,文本是否是Spanned類型,如果不是,就會創建Ellipsizer對象。

繼續看。

<code>private StaticLayout(Builder b) {
......

if (b.mEllipsize != null) {
Ellipsizer e = (Ellipsizer) getText();

e.mLayout = this;
e.mWidth = b.mEllipsizedWidth;
e.mMethod = b.mEllipsize;
mEllipsizedWidth = b.mEllipsizedWidth;

mColumns = COLUMNS_ELLIPSIZE;
} else {
mColumns = COLUMNS_NORMAL;
mEllipsizedWidth = b.mWidth;
}
mLines = new int[mLineDirections.length];
......
generate(b, b.mIncludePad, b.mIncludePad);
}/<code>

如果設置了android:ellipsize,那麼就把text轉化為Ellipsizer對象,並且初始化一些屬性。最後通過generate進行創建。

mColumns這個屬性,後面會根據它來創建數組,mLines就是行數組,你可以理解為mLines中的每一行都有mColumns個屬性,裡面記錄著這一行相關的信息,比如,如果需要省略號模式,mColumns = 6,將會記錄一些這一行的開始、結束位置,上下的度量信息以及省略模式的信息。

<code>    private static final int START = 0;
private static final int TOP = 1;
private static final int DESCENT = 2;
private static final int HYPHEN = 3;
private static final int ELLIPSIS_START = 4;
private static final int ELLIPSIS_COUNT = 5;/<code>

generate首先還是從Builder中讀取信息,並初始化一些局部變量。

<code>void generate(Builder b, boolean includepad, boolean trackpad) {
CharSequence source = b.mText;
//文字開始索引
int bufStart = b.mStart;
//文字結束索引,一般就是字符串的長度
int bufEnd = b.mEnd;
TextPaint paint = b.mPaint;
//寬度
int outerWidth = b.mWidth;
TextDirectionHeuristic textDir = b.mTextDir;
//行間距
float spacingmult = b.mSpacingMult;
float spacingadd = b.mSpacingAdd;
//容納內容的區域,不包括左右padding
float ellipsizedWidth = b.mEllipsizedWidth;
//省略號位置
TextUtils.TruncateAt ellipsize = b.mEllipsize;
//這個保存換行信息,比如總共需要多少行,每行寬度等。
LineBreaks lineBreaks = new LineBreaks();
//負責對文字的測量
MeasuredText measured = b.mMeasuredText;
......
}/<code>

隨後,開始遍歷。

<code>void generate(Builder b, boolean includepad, boolean trackpad) {
for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
/*
*從當前位置到結束,查找是否存在換行符\\n,如果沒有,那麼段落的end索引就指向字符串的結尾位置

*/
paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
if (paraEnd < 0)
paraEnd = bufEnd;
else
paraEnd++;
}
}/<code>

通過measured進行測量,measured內部有兩個數組,mWidthsmChars,分別存放每一個字符的寬度和字符內容。

通過下面的代碼,對這兩個數組賦值。

<code>measured.setPara(source, paraStart, paraEnd, textDir, b);
......
nGetWidths(b.mNativePtr, widths);/<code>

獲得換行信息。

<code>int breakCount = nComputeLineBreaks(b.mNativePtr, lineBreaks, lineBreaks.breaks,
lineBreaks.widths, lineBreaks.flags, lineBreaks.breaks.length);

int[] breaks = lineBreaks.breaks;
float[] lineWidths = lineBreaks.widths;
int[] flags = lineBreaks.flags;/<code>

nComputeLineBreaks將計算,顯示這些文字,需要多少行,lineBreaks中記錄了每行換行的位置,每行的長度。

Android:解讀StaticLayout

再通過下面的代碼調整數值。

<code>//開始時,這就是我們設置的行數
final int remainingLineCount = mMaximumVisibleLineCount - mLineCount;
//判斷省略顯示是否可以執行
final boolean ellipsisMayBeApplied = ellipsize != null
&& (ellipsize == TextUtils.TruncateAt.END
|| (mMaximumVisibleLineCount == 1
&& ellipsize != TextUtils.TruncateAt.MARQUEE));
//可以執行,並且行數大於0,並小於總換行的行數
if (remainingLineCount > 0 && remainingLineCount < breakCount &&
ellipsisMayBeApplied) {
// Calculate width and flag.
float width = 0;
int flag = 0;
//從第二行開始
for (int i = remainingLineCount - 1; i < breakCount; i++) {
//判斷是否最後一行
if (i == breakCount - 1) {
width += lineWidths[i];
} else {
//這個width會累計從第二行開始到最後的所有字符的寬度和
for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) {
width += widths[j];
}
}
//標記TAB建信息
flag |= flags[i] & TAB_MASK;
}
// 把最後一行當作一個單行,更新數組第二個元素的值
breaks[remainingLineCount - 1] = breaks[breakCount - 1];
lineWidths[remainingLineCount - 1] = width;
flags[remainingLineCount - 1] = flag;

breakCount = remainingLineCount;
}/<code>

接下來就要遍歷可見行,然後計算一些度量信息。見out方法。

<code>//lines就是根據mColumns數量創建的數組
void out(......) {
int j = mLineCount;
int off = j * mColumns;
int want = off + mColumns + TOP;
int[] lines = mLines;
//當遍歷到第二行時,將進行擴容
if (want >= lines.length) {
Directions[] grow2 = ArrayUtils.newUnpaddedArray(
Directions.class, GrowingArrayUtils.growSize(want));
System.arraycopy(mLineDirections, 0, grow2, 0,
mLineDirections.length);
......
}
}/<code>

這個方法末尾,將會使mLineCount++,這將影響到generate方法中的remainingLineCount的數值。當滿足需要進行省略條件的行時(這裡是第二行),就會通過calculateEllipsis來計算省略號的位置,寬度,然後將信息寫入mLines

<code>mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;/<code>

至此,各種信息都準備好了。它和

BoringLayou不一樣,不是直接把mText屬性處理成帶省略號的,而是在繪製時才處理。就是通過內部的Ellipsizer

根據View的繪製過程,佈局、測量都完成了,那麼最後就是繪製View的過程。所以必然要執行TextViewonDraw方法,因為這裡講的是佈局,所以我們看佈局的繪製。

<code>protected void onDraw(Canvas canvas){
......
//如果是EditText
if (mEditor != null) {
mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
} else {
//TextView執行佈局的draw方法
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
......
}/<code>
<code>public void draw(Canvas canvas, Path highlight, Paint highlightPaint,
int cursorOffsetVertical) {
......
drawText(canvas, firstLine, lastLine);
}/<code>

LayoutdrawText中會循環每一行,調用

RecordingCanvasdrawText繪製文字。

<code>public void drawText(Canvas canvas, int firstLine, int lastLine){
......
if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab && !justify) {
canvas.drawText(buf, start, end, x, lbaseline, paint);
}
......
}/<code>

RecordingCanvasdrawText。

<code>public final void drawText(@NonNull CharSequence text, int start, int end, float x, float y,
@NonNull Paint paint) {
if ((start | end | (end - start) | (text.length() - end)) < 0) {
throw new IndexOutOfBoundsException();
}
if (text instanceof String || text instanceof SpannedString
|| text instanceof SpannableString) {
nDrawText(mNativeCanvasWrapper, text.toString(), start, end, x, y,
paint.mBidiFlags, paint.getNativeInstance(), paint.mNativeTypeface);
} else if (text instanceof GraphicsOperations) {
((GraphicsOperations) text).drawText(this, start, end, x, y,
paint);
} else {
//我們這裡是Ellipsizer類型,所以走這裡的代碼
char[] buf = TemporaryBuffer.obtain(end - start);
TextUtils.getChars(text, start, end, buf, 0);
nDrawText(mNativeCanvasWrapper, buf, 0, end - start, x, y,
paint.mBidiFlags, paint.getNativeInstance(), paint.mNativeTypeface);
TemporaryBuffer.recycle(buf);
}
}/<code>

從前面我們知道,StaticLayout是傳入的Ellipsizer類型文字,所以上面代碼執行最後一個

else中的語句。

<code>public static void getChars(CharSequence s, int start, int end,
char[] dest, int destoff) {
Class extends CharSequence> c = s.getClass();

......
//執行Ellipsizer中的getChars
else if (s instanceof GetChars)
((GetChars) s).getChars(start, end, dest, destoff);
......
}

/*GetChars extends CharSequence*//<code>

TextUtils.getChars中又會判斷這個類型,繼續執行Ellipsizer中的方法。

<code>public void getChars(int start, int end, char[] dest, int destoff) {
int line1 = mLayout.getLineForOffset(start);
int line2 = mLayout.getLineForOffset(end);

TextUtils.getChars(mText, start, end, dest, destoff);

for (int i = line1; i <= line2; i++) {
mLayout.ellipsize(start, end, i, dest, destoff, mMethod);
}
}/<code>

首先通過getLineForOffset來計算startend字符索引可能在的行數,然後遍歷(startend可能不在一行),執行

mLayout.ellipsize

<code>private void ellipsize(int start, int end, int line,
char[] dest, int destoff, TextUtils.TruncateAt method) {
//獲取需要省略的數量
int ellipsisCount = getEllipsisCount(line);

if (ellipsisCount == 0) {
return;
}

int ellipsisStart = getEllipsisStart(line);
int linestart = getLineStart(line);
//遍歷,更新dest中的元素
for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) {
char c;

if (i == ellipsisStart) {
//獲取省略號
c = getEllipsisChar(method); // ellipsis
} else {
c = '\\\\uFEFF'; // 0-width space
}

int a = i + linestart;
//如果索引在start和end之間,就更新dest
if (a >= start && a < end) {
dest[destoff + a - start] = c;
}
}
}/<code>

getEllipsisCount的計算,就是從之前保留到mLines中的數據中讀取。這裡又看到了mColumns的身影。

<code>public int getEllipsisCount(int line) {
if (mColumns < COLUMNS_ELLIPSIZE) {
return 0;

}
/*
*第一行 line = 0,所以取 mLines[5],第二行就是 mLines[13]
*/
return mLines[mColumns * line + ELLIPSIS_COUNT];
}/<code>

一樣的道理,getEllipsisStart也是這個算法。getLineStart也一樣。

<code>public int getEllipsisStart(int line) {
if (mColumns < COLUMNS_ELLIPSIZE) {
return 0;
}
return mLines[mColumns * line + ELLIPSIS_START];
}/<code>

最終回到RecordingCanvasdrawText。buf數組就是上面代碼處理好的dest的內容,最終繪製到界面上。


分享到:


相關文章: