返回介绍

5. TextView 的文字处理和绘制

发布于 2024-12-23 22:05:51 字数 16213 浏览 0 评论 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 才能得到最终真实的坐标。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

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