Android TextView绘制之StaticLayout

StaticLayout

官网中,StaticLayout的描述如下:

StaticLayout is a Layout for text that will not be edited after it is laid out. Use DynamicLayout for text that may change.

This 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, or would be tempted to call Canvas.drawText() directly

StaticLayout是一个适用于不会再被编辑的文本的布局。如果可能改变的要用DynamicLayout。

它的创建在TextView#makeSingleLayout方法中,在DynamicLayout和BoringLayout的判断之后。

protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,boolean useSaved) {
	Layout result = null;
    if (useDynamicLayout()) {
    	... //DynamicLayou创建
    }else {
        if (boring == UNKNOWN_BORING) {
            boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
            if (boring != null) {
                mBoring = boring;
            }
        }
        if (boring != null) {
            ...//BoringLayout创建
        }
    }
    if (result == null) {
            StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
                    0, mTransformed.length(), mTextPaint, wantWidth)
                    .setAlignment(alignment)
                    .setTextDirection(mTextDir)
                    .setLineSpacing(mSpacingAdd, mSpacingMult)
                    .setIncludePad(mIncludePad)
                    .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
                    .setBreakStrategy(mBreakStrategy)
                    .setHyphenationFrequency(mHyphenationFrequency)
                    .setJustificationMode(mJustificationMode)
                    .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
            if (shouldEllipsize) {
                builder.setEllipsize(effectiveEllipsize)
                        .setEllipsizedWidth(ellipsisWidth);
            }
            result = builder.build();
     }
}

StaticaLayout使用Build模式构造。

在StaticLayout构造方法中,调用父类方法前,先处理文本内容。判断是否设置了android:ellipsize,若无,直接用mText,若有,再文本是否是Spanned类型,决定创建SpannedEllipsizer还是Ellipsizer对象。

Ellipsizer 是 Layout 的嵌套内部类,实现了 CharSequence 和 GetChars 接口。该类就是用来对文本进行省略处理的。SpannedEllipsizer是Ellipsizer的子类。

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.mTextDir, b.mSpacingMult, b.mSpacingAdd);

        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  = ArrayUtils.newUnpaddedIntArray(2 * mColumns);
		...
        generate(b, b.mIncludePad, b.mIncludePad);
    }

mLines为行数组,这个数组需要通过mColumns属性生成。

在TextView中,文本是一行行处理的。每一行文本处理时需要记录四个值,start,top,desent,hyphen 值。这四个值也是StaticLayout中的四个静态变量。当文本需要被省略时,还需要记录 ellipsis_start 和 ellipsis_count 值。因此正常的 mColumn 值为4(个变量),省略时则是 6(个变量)。

最后执行generate,该方法主要完成文本的段落、折行的处理。

1、首先从Builder中读取信息,并初始化一些局部变量。

/* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
        final CharSequence source = b.mText;
        //文字的开始和结束索引
        final int bufStart = b.mStart;
        final int bufEnd = b.mEnd;
        TextPaint paint = b.mPaint;
        int outerWidth = b.mWidth;
        TextDirectionHeuristic textDir = b.mTextDir;
        final boolean fallbackLineSpacing = b.mFallbackLineSpacing;
    	//行间距
        float spacingmult = b.mSpacingMult;
        float spacingadd = b.mSpacingAdd;
    	//容纳内容的区域,不包括左右padding
        float ellipsizedWidth = b.mEllipsizedWidth;
    	//省略号位置
        TextUtils.TruncateAt ellipsize = b.mEllipsize;
        ...
       	//字体的测量
        Paint.FontMetricsInt fm = b.mFontMetricsInt;
 }

字体测量,主要通过FontMetricsInt完成,它是Paint的内部类。

FontMetricsInt 类主要包含保存了字体测量相关的数据。

Android TextView绘制之StaticLayout

在接下来的字体测量中,会使用 fmCache 数组来缓存字体测量的信息,缓存 top, bottom, ascent 和 descen 四个值,因此 fmCache 数组的大小始终是 4 的倍数。

2、处理制表位,这里的制表位是使用 方式插入到文本中的,通过 Spanned 接口提供的方法来获取到 TabStopSpan,排序后将所有的制表位的位置存在 variableTabStops数组中。完成以上处理后,由JNI 层来处理段落文本,主要处理了段落的制表行缩进、折行等。

3、按段落来处理文本

通过查找换行符,确定每个段落的起止位置。

lineBreaks中记录了每行换行的位置,每行的长度。

...
final int remainingLineCount = mMaximumVisibleLineCount - mLineCount;
//判断省略显示是否可以执行
final boolean ellipsisMayBeApplied = ellipsize != null
              && (ellipsize == TextUtils.TruncateAt.END
                        || (mMaximumVisibleLineCount == 1
                                && ellipsize != TextUtils.TruncateAt.MARQUEE));
//可以执行,且行数大于0,小于总行数
if (0 < remainingLineCount && remainingLineCount < breakCount && ellipsisMayBeApplied) {
         // Calculate width
         float width = 0;
         boolean hasTab = false;  // XXX May need to also have starting hyphen edit
         //从第二行开始
         for (int i = remainingLineCount - 1; i < breakCount; i++) {
              if (i == breakCount - 1) {//判断是否是最后一行
                  width += lineWidths[i];
              } else {
                  for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) {
                      width += measuredPara.getCharWidthAt(j);
                  }
              }
              //标记tab键信息
              hasTab |= hasTabs[i];
         }
         // 把最后一行当做一个单行,更新数组第二个元素值
         breaks[remainingLineCount - 1] = breaks[breakCount - 1];
         lineWidths[remainingLineCount - 1] = width;
         hasTabs[remainingLineCount - 1] = hasTab;

         breakCount = remainingLineCount;
}
...

接下来遍历。通过spanEndCache 缓存记录文本中的 Span 的结束位置(这里的 span 具体类型是 MetricAffectingSpan)。通过out方法计算一些度量信息。

int fmTop = 0, fmBottom = 0, fmAscent = 0, fmDescent = 0;
int fmCacheIndex = 0;
int spanEndCacheIndex = 0;
int breakIndex = 0;
for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd){
	spanEnd = spanEndCache[spanEndCacheIndex++];
    ...
    v = out(source, here, endPos,ascent, descent, fmTop, fmBottom,v, spacingmult,         			spacingadd, chooseHt, chooseHtv, fm,hasTabs[breakIndex], 			                     hyphenEdits[breakIndex], needMultiply,measuredPara, bufEnd, includepad,                   trackpad, addLastLineSpacing, chs,
          paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex],paint,                     moreChars);
    ...
}

4、字体测量。

fmCache 用来缓存字体信息,初始化时大小是 16,每次扩容时都是双倍扩容。

private int out(final CharSequence text, final int start, final int end, int above, int below,int top, int bottom, int v, final float spacingmult, final float spacingadd,
final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
final boolean hasTab, final int hyphenEdit, final boolean needMultiply,@NonNull final MeasuredParagraph measured,final int bufEnd, final boolean includePad, final boolean trackPad,final boolean addLastLineLineSpacing, final char[] chs,final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,final float textWidth, final TextPaint paint, final boolean moreChars) {

}

这个方法末尾,将会使mLineCount++,这将影响到generate方法中的remainingLineCount的数值。

当满足需要进行省略条件的行时,就会通过calculateEllipsis来计算省略号的位置、宽度,然后将信息写入mLines。和BoringLayou不一样,不是直接把mText属性处理成带省略号的,而是在绘制时才通过内部的Ellipsizer处理。
至此,完成StaticLayout的generate。

上一篇:Android Studio 如何在TextView中设置图标并按需调整图标大小


下一篇:2.02TextView(文本框)_UI组件_Android