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 类主要包含保存了字体测量相关的数据。

在这里插入图片描述

在接下来的字体测量中,会使用 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,每次扩容时都是双倍扩容。

5、完成以上处理后,调用out方法。
如果说 generate() 大部分是处理一些折行、段落相关的数据,那么 out() 方法就是将这些数据使用起来,真正地布局出来(注意,布局不是显示,显示的话还是在父类的 drawText() 方法中进行的)

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。

相关文档 ->
StaticLayout 源码分析

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Android中,可以使用SpannableStringBuilder类来实现TextView中嵌套TextView的效果。具体步骤如下: 1. 创建一个外部的TextView,设置它的text为一个SpannableStringBuilder对象。 2. 在SpannableStringBuilder对象中插入需要嵌套的TextView,使用setSpan()方法将TextView对象作为参数传入。 3. 设置TextView的布局参数,使其可以适应外部TextView的尺寸。 以下是示例代码: ``` TextView outerTextView = findViewById(R.id.outer_text_view); SpannableStringBuilder builder = new SpannableStringBuilder(); TextView innerTextView = new TextView(this); innerTextView.setText("Inner TextView"); builder.append("Outer TextView "); builder.setSpan(new MySpannable(innerTextView), builder.length(), builder.length() + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); outerTextView.setText(builder); outerTextView.setMovementMethod(LinkMovementMethod.getInstance()); class MySpannable extends ClickableSpan { private TextView textView; public MySpannable(TextView textView) { this.textView = textView; this.textView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); } @Override public void onClick(View widget) { // Do something when inner TextView is clicked } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setColor(ds.linkColor); // Use link color for inner TextView ds.setUnderlineText(false); } @Override public void drawBackground(Canvas canvas, Paint paint, int left, int right, int top, int baseline, int bottom, CharSequence text, int start, int end, int lnum) { // Do not draw background for inner TextView } } ``` 在上面的代码中,我们创建了一个内部TextView对象,并将它作为参数传入MySpannable类的构造方法中。然后,使用setSpan()方法将MySpannable对象插入到SpannableStringBuilder对象中,从而实现了TextView的嵌套效果。 注意,为了使内部TextView能够响应点击事件,需要调用setMovementMethod()方法并传入LinkMovementMethod.getInstance()。此外,还需要重写MySpannable类的updateDrawState()方法来设置内部TextView的颜色,以及重写drawBackground()方法来取消内部TextView的背景色。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KWMax

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值