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的内容,最终绘制到界面上。


分享到:


相關文章: