返回介绍

5. TextView 的文字处理和绘制

发布于 2024-12-23 22:05:51 字数 16213 浏览 0 评论 0 收藏

TextView 主要的文字排版和渲染并不是在 TextView 里面完成的,而是由 Layout 类来处理文字排版工作。在单纯地使用 TextView 来展示静态文本的时候,这件事情则是由 Layout 的子类 StaticLayout 来完成的。

StaticLayout 接收到字符串后,首先做的事情是根据字符串里面的换行符对字符串进行拆分。

for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
      paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
      if (paraEnd < 0)
        paraEnd = bufEnd;
      else
        paraEnd++;

拆分后的段落(Paragraph) 被分配给辅助类 MeasuredText 进行测量得到每个字符的宽度以及每个段落的 FontMetric。并通过 LineBreaker 进行折行的判断

//把段落载入到 MeasuredText 中,并分配对应的缓存空间
measured.setPara(source, paraStart, paraEnd, textDir, b);
      char[] chs = measured.mChars;
      float[] widths = measured.mWidths;
      byte[] chdirs = measured.mLevels;
      int dir = measured.mDir;
      boolean easy = measured.mEasy;
    //把相关属性传给 JNI 层的 LineBreaker
    nSetupParagraph(b.mNativePtr, chs, paraEnd - paraStart,
        firstWidth, firstWidthLineCount, restWidth,
        variableTabStops, TAB_INCREMENT, b.mBreakStrategy, b.mHyphenationFrequency);

      int fmCacheCount = 0;
      int spanEndCacheCount = 0;
      for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
        if (fmCacheCount * 4 >= fmCache.length) {
          int[] grow = new int[fmCacheCount * 4 * 2];
          System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4);
          fmCache = grow;
        }

        if (spanEndCacheCount >= spanEndCache.length) {
          int[] grow = new int[spanEndCacheCount * 2];
          System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount);
          spanEndCache = grow;
        }

        if (spanned == null) {
          spanEnd = paraEnd;
          int spanLen = spanEnd - spanStart;
          //段落没有 Span 的情况下,把整个段落交给 MeasuredText 计算每个字符的宽度和 FontMetric
          measured.addStyleRun(paint, spanLen, fm);
        } else {
          spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
              MetricAffectingSpan.class);
          int spanLen = spanEnd - spanStart;
          MetricAffectingSpan[] spans =
              spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
          spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class);
          //把对排版有影响的 Span 交给 MeasuredText 测量宽度并计算 FontMetric
          measured.addStyleRun(paint, spans, spanLen, fm);
        }

        //把测量后的 FontMetric 缓存下来方便后面使用
        fmCache[fmCacheCount * 4 + 0] = fm.top;
        fmCache[fmCacheCount * 4 + 1] = fm.bottom;
        fmCache[fmCacheCount * 4 + 2] = fm.ascent;
        fmCache[fmCacheCount * 4 + 3] = fm.descent;
        fmCacheCount++;

        spanEndCache[spanEndCacheCount] = spanEnd;
        spanEndCacheCount++;
      }

      nGetWidths(b.mNativePtr, widths);
      //计算段落中需要折行的位置,并返回折行的数量
      int breakCount = nComputeLineBreaks(b.mNativePtr, lineBreaks, lineBreaks.breaks,
          lineBreaks.widths, lineBreaks.flags, lineBreaks.breaks.length);

计算完每一行的测量相关信息、Span 宽高以及折行位置,就可以开始按照最终的行数一行一行地保存下来,以供后面绘制和获取对应文本信息的时候使用。

for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
        spanEnd = spanEndCache[spanEndCacheIndex++];

        // 获取之前缓存的 FontMetric 信息
        fm.top = fmCache[fmCacheIndex * 4 + 0];
        fm.bottom = fmCache[fmCacheIndex * 4 + 1];
        fm.ascent = fmCache[fmCacheIndex * 4 + 2];
        fm.descent = fmCache[fmCacheIndex * 4 + 3];
        fmCacheIndex++;

        if (fm.top < fmTop) {
          fmTop = fm.top;
        }
        if (fm.ascent < fmAscent) {
          fmAscent = fm.ascent;
        }
        if (fm.descent > fmDescent) {
          fmDescent = fm.descent;
        }
        if (fm.bottom > fmBottom) {
          fmBottom = fm.bottom;
        }

        while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) {
          breakIndex++;
        }

        while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) {
          int endPos = paraStart + breaks[breakIndex];

          boolean moreChars = (endPos < bufEnd);

          //逐行把相关信息储存下来
          v = out(source, here, endPos,
              fmAscent, fmDescent, fmTop, fmBottom,
              v, spacingmult, spacingadd, chooseHt, chooseHtv, fm, flags[breakIndex],
              needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad,
              chs, widths, paraStart, ellipsize, ellipsizedWidth,
              lineWidths[breakIndex], paint, moreChars);

          if (endPos < spanEnd) {
            fmTop = fm.top;
            fmBottom = fm.bottom;
            fmAscent = fm.ascent;
            fmDescent = fm.descent;
          } else {
            fmTop = fmBottom = fmAscent = fmDescent = 0;
          }

          here = endPos;
          breakIndex++;

          if (mLineCount >= mMaximumVisibleLineCount) {
            return;
          }
        }
      }

这样 StaticLayout 的排版过程就完成了。文本的绘制则是交给父类 Layout 来做的,Layout 的绘制分为两大部分,drawBackground 和 drawText。drawBackground 做的事情是如果文本内有 LineBackgroundSpan 则绘制所有的 LineBackgroundSpan,然后判断是否有高亮背景(文本选中的背景),如果有则绘制高亮背景。

public void drawBackground(Canvas canvas, Path highlight, Paint highlightPaint,
      int cursorOffsetVertical, int firstLine, int lastLine) {
  
    //判断并绘制 LineBackgroundSpan
    if (mSpannedText) {
      if (mLineBackgroundSpans == null) {
        mLineBackgroundSpans = new SpanSet<LineBackgroundSpan>(LineBackgroundSpan.class);
      }

      Spanned buffer = (Spanned) mText;
      int textLength = buffer.length();
      mLineBackgroundSpans.init(buffer, 0, textLength);

      if (mLineBackgroundSpans.numberOfSpans > 0) {
        int previousLineBottom = getLineTop(firstLine);
        int previousLineEnd = getLineStart(firstLine);
        ParagraphStyle[] spans = NO_PARA_SPANS;
        int spansLength = 0;
        TextPaint paint = mPaint;
        int spanEnd = 0;
        final int width = mWidth;
        //逐行绘制 LineBackgroundSpan
        for (int i = firstLine; i <= lastLine; i++) {
          int start = previousLineEnd;
          int end = getLineStart(i + 1);
          previousLineEnd = end;

          int ltop = previousLineBottom;
          int lbottom = getLineTop(i + 1);
          previousLineBottom = lbottom;
          int lbaseline = lbottom - getLineDescent(i);

          if (start >= spanEnd) {
            spanEnd = mLineBackgroundSpans.getNextTransition(start, textLength);
            
            spansLength = 0;
            if (start != end || start == 0) {
              //排除不在绘制范围内的 LineBackgroundSpan
              for (int j = 0; j < mLineBackgroundSpans.numberOfSpans; j++) {
                if (mLineBackgroundSpans.spanStarts[j] >= end ||
                    mLineBackgroundSpans.spanEnds[j] <= start) continue;
                spans = GrowingArrayUtils.append(
                    spans, spansLength, mLineBackgroundSpans.spans[j]);
                spansLength++;
              }
            }
          }
          //对当前行内的 LineBackgroundSpan 进行绘制
          for (int n = 0; n < spansLength; n++) {
            LineBackgroundSpan lineBackgroundSpan = (LineBackgroundSpan) spans[n];
            lineBackgroundSpan.drawBackground(canvas, paint, 0, width,
                ltop, lbaseline, lbottom,
                buffer, start, end, i);
          }
        }
      }
      mLineBackgroundSpans.recycle();
    }

    //判断并绘制高亮背景(即选中的文本)
    if (highlight != null) {
      if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical);
      canvas.drawPath(highlight, highlightPaint);
      if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical);
    }
  }

drawText 用来逐行绘制 Layout 的文本、影响显示效果的 Span、以及 Emoji 表情等。当有 Emoji 或者 Span 的时候,实际绘制工作交给 TextLine 类来完成。

public void drawText(Canvas canvas, int firstLine, int lastLine) {
    int previousLineBottom = getLineTop(firstLine);
    int previousLineEnd = getLineStart(firstLine);
    ParagraphStyle[] spans = NO_PARA_SPANS;
    int spanEnd = 0;
    TextPaint paint = mPaint;
    CharSequence buf = mText;

    Alignment paraAlign = mAlignment;
    TabStops tabStops = null;
    boolean tabStopsIsInitialized = false;

    //获取 TextLine 实例
    TextLine tl = TextLine.obtain();

    //逐行绘制文本
    for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
      int start = previousLineEnd;
      previousLineEnd = getLineStart(lineNum + 1);
      int end = getLineVisibleEnd(lineNum, start, previousLineEnd);

      int ltop = previousLineBottom;
      int lbottom = getLineTop(lineNum + 1);
      previousLineBottom = lbottom;
      int lbaseline = lbottom - getLineDescent(lineNum);

      int dir = getParagraphDirection(lineNum);
      int left = 0;
      int right = mWidth;

      if (mSpannedText) {
        Spanned sp = (Spanned) buf;
        int textLength = buf.length();
        //检测是否段落的第一行
        boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n');

        //获得所有的段落风格相关的 Span
        if (start >= spanEnd && (lineNum == firstLine || isFirstParaLine)) {
          spanEnd = sp.nextSpanTransition(start, textLength,
                          ParagraphStyle.class);
          spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);

          paraAlign = mAlignment;
          for (int n = spans.length - 1; n >= 0; n--) {
            if (spans[n] instanceof AlignmentSpan) {
              paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
              break;
            }
          }

          tabStopsIsInitialized = false;
        }

        //获取影响行缩进的 Span
        final int length = spans.length;
        boolean useFirstLineMargin = isFirstParaLine;
        for (int n = 0; n < length; n++) {
          if (spans[n] instanceof LeadingMarginSpan2) {
            int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
            int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
            if (lineNum < startLine + count) {
              useFirstLineMargin = true;
              break;
            }
          }
        }
        for (int n = 0; n < length; n++) {
          if (spans[n] instanceof LeadingMarginSpan) {
            LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
            if (dir == DIR_RIGHT_TO_LEFT) {
              margin.drawLeadingMargin(canvas, paint, right, dir, ltop,
                           lbaseline, lbottom, buf,
                           start, end, isFirstParaLine, this);
              right -= margin.getLeadingMargin(useFirstLineMargin);
            } else {
              margin.drawLeadingMargin(canvas, paint, left, dir, ltop,
                           lbaseline, lbottom, buf,
                           start, end, isFirstParaLine, this);
              left += margin.getLeadingMargin(useFirstLineMargin);
            }
          }
        }
      }

      boolean hasTabOrEmoji = getLineContainsTab(lineNum);
      if (hasTabOrEmoji && !tabStopsIsInitialized) {
        if (tabStops == null) {
          tabStops = new TabStops(TAB_INCREMENT, spans);
        } else {
          tabStops.reset(TAB_INCREMENT, spans);
        }
        tabStopsIsInitialized = true;
      }

      //判断当前行的第五方式
      Alignment align = paraAlign;
      if (align == Alignment.ALIGN_LEFT) {
        align = (dir == DIR_LEFT_TO_RIGHT) ?
          Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
      } else if (align == Alignment.ALIGN_RIGHT) {
        align = (dir == DIR_LEFT_TO_RIGHT) ?
          Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
      }

      int x;
      if (align == Alignment.ALIGN_NORMAL) {
        if (dir == DIR_LEFT_TO_RIGHT) {
          x = left + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
        } else {
          x = right + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
        }
      } else {
        int max = (int)getLineExtent(lineNum, tabStops, false);
        if (align == Alignment.ALIGN_OPPOSITE) {
          if (dir == DIR_LEFT_TO_RIGHT) {
            x = right - max + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
          } else {
            x = left - max + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
          }
        } else { // Alignment.ALIGN_CENTER
          max = max & ~1;
          x = ((right + left - max) >> 1) +
              getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
        }
      }

      paint.setHyphenEdit(getHyphen(lineNum));
      Directions directions = getLineDirections(lineNum);
      if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTabOrEmoji) {
        //没有任何 Emoji 或者 span 的时候,直接调用 Canvas 来绘制文本
        canvas.drawText(buf, start, end, x, lbaseline, paint);
      } else {
        //当有 Emoji 或者 Span 的时候,交给 TextLine 类来绘制
        tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops);
        tl.draw(canvas, x, ltop, lbaseline, lbottom);
      }
      paint.setHyphenEdit(0);
    }

    TextLine.recycle(tl);
  }

我们下面再来看看 TextLine 是如何绘制有特殊情况的文本的

void draw(Canvas c, float x, int top, int y, int bottom) {
    //判断是否有 Tab 或者 Emoji
    if (!mHasTabs) {
      if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
        drawRun(c, 0, mLen, false, x, top, y, bottom, false);
        return;
      }
      if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
        drawRun(c, 0, mLen, true, x, top, y, bottom, false);
        return;
      }
    }

    float h = 0;
    int[] runs = mDirections.mDirections;
    RectF emojiRect = null;

    int lastRunIndex = runs.length - 2;
    //逐个绘制
    for (int i = 0; i < runs.length; i += 2) {
      int runStart = runs[i];
      int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
      if (runLimit > mLen) {
        runLimit = mLen;
      }
      boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;

      int segstart = runStart;
      for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
        int codept = 0;
        Bitmap bm = null;

        if (mHasTabs && j < runLimit) {
          codept = mChars[j];
          if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) {
            codept = Character.codePointAt(mChars, j);
            if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) {
              //获取 Emoji 对应的图像
              bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
            } else if (codept > 0xffff) {
              ++j;
              continue;
            }
          }
        }

        if (j == runLimit || codept == '\t' || bm != null) {
          //绘制文字
          h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
              i != lastRunIndex || j != mLen);

          if (codept == '\t') {
            h = mDir * nextTab(h * mDir);
          } else if (bm != null) {
            float bmAscent = ascent(j);
            float bitmapHeight = bm.getHeight();
            float scale = -bmAscent / bitmapHeight;
            float width = bm.getWidth() * scale;

            if (emojiRect == null) {
              emojiRect = new RectF();
            }
            //调整 emoji 图像绘制矩形
            emojiRect.set(x + h, y + bmAscent,
                x + h + width, y);
            //绘制 Emoji 图像
            c.drawBitmap(bm, null, emojiRect, mPaint);
            h += width;
            j++;
          }
          segstart = j + 1;
        }
      }
    }
  }

这样就完成了文本的绘制工作,简单地总结就是:分析整体文本—>拆分为段落—>计算整体段落的文本包括 Span 的测量信息—>对文本进行折行—>根据最终行数把文本测量信息保存—>绘制文本的行背景—>判断并获取文本种的 Span 和 Emoji 图像—>绘制最终的文本和图像。当然我们省略了一部分内容,比如段落文本方向,单行的文本排版方向的计算,实际的处理要更为复杂。

接下来我们来看一下在测量过程中出现的 FontMetrics,这是一个 Paint 的静态内部类。主要用来储存文字排版的 Y 轴相关信息。内部仅包含 ascent、descent、top、bottom、leading 五个数值。如下图:

1339061786_4121

除了 leading 以外,其他的数值都是相对于每一行的 baseline 的,也就是说其他的数值需要加上对应行的 baseline 才能得到最终真实的坐标。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。