• Android之TextView文字绘制流程


    一:TextView的onDraw()方法:

    1.第一句restartMarqueeIfNeeded()绘制字幕滚动。

    protected void onDraw(Canvas canvas) {
            restartMarqueeIfNeeded();
    
            // Draw the background for this view
            super.onDraw(canvas);
         ...
    }

    首先我们看一个东西:

    android.text.TextUtils.java

    public enum TruncateAt {
            START,
            MIDDLE,
            END,
            MARQUEE,
            /**
             * @hide
             */
            END_SMALL
        }

    很熟悉对不对,这就是平常在TextView的android:ellipsize属性,当字符显示不下的时候省略号所在的位置,有开始/结束/中间/滚动四个枚举值。每次onDraw的时候都检测是否需要滚动字幕,重新滚幕的条件就是android:ellipsize属性是MARQUEE(也就是滚动字幕)和mRestartMarquee 布尔值。

    private void restartMarqueeIfNeeded() {
            if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
                mRestartMarquee = false;
                startMarquee();
            }
        }

    关于这部分就讲这么多,知道这个是滚动字幕的就行了,若对滚幕感兴趣自行研究canMarquee()/startMarquee()/stopMarquee()/startStopMarquee(boolean start)/Marquee类。

    2.compoundDrawable的绘制,也就是drawableTop/Bottom/Left/Right属性。

    // Draw the background for this view
            super.onDraw(canvas);
    
            final int compoundPaddingLeft = getCompoundPaddingLeft();
            final int compoundPaddingTop = getCompoundPaddingTop();
            final int compoundPaddingRight = getCompoundPaddingRight();
            final int compoundPaddingBottom = getCompoundPaddingBottom();
    ....
    final Drawables dr = mDrawables; if (dr != null) { /* * Compound, not extended, because the icon is not clipped * if the text height is smaller. */ int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop; int hspace = right - left - compoundPaddingRight - compoundPaddingLeft; // IMPORTANT: The coordinates computed are also used in invalidateDrawable() // Make sure to update invalidateDrawable() when changing this code. if (dr.mShowing[Drawables.LEFT] != null) { canvas.save(); canvas.translate(scrollX + mPaddingLeft + leftOffset, scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2); dr.mShowing[Drawables.LEFT].draw(canvas); canvas.restore(); } // IMPORTANT: The coordinates computed are also used in invalidateDrawable() // Make sure to update invalidateDrawable() when changing this code. if (dr.mShowing[Drawables.RIGHT] != null) { canvas.save(); canvas.translate(scrollX + right - left - mPaddingRight - dr.mDrawableSizeRight - rightOffset, scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2); dr.mShowing[Drawables.RIGHT].draw(canvas); canvas.restore(); } // IMPORTANT: The coordinates computed are also used in invalidateDrawable() // Make sure to update invalidateDrawable() when changing this code. if (dr.mShowing[Drawables.TOP] != null) { canvas.save(); canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2, scrollY + mPaddingTop); dr.mShowing[Drawables.TOP].draw(canvas); canvas.restore(); } // IMPORTANT: The coordinates computed are also used in invalidateDrawable() // Make sure to update invalidateDrawable() when changing this code. if (dr.mShowing[Drawables.BOTTOM] != null) { canvas.save(); canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthBottom) / 2, scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom); dr.mShowing[Drawables.BOTTOM].draw(canvas); canvas.restore(); } }
    Drawables是TextView下的静态类,持有着mShowing(drawable数组)上下左右四个drawable,这四个drawable绘制在不同的位置。

    3.TextPaint和Layout,其实还有mEditor,也就是可编辑状态下的情况(EditText)。这部分先初始化画笔TextPaint,Cavans画布,最重要的就是Layout,由它负责文字绘制。
     Path highlight = getUpdatedHighlightPath();
            if (mEditor != null) {
                mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
            } else {
                layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
            }

    二:Layout类

    Layout是android.text下的一个抽象类,负责文字布局绘画,它有两个子类分别是DynamicLayout和StaticLayout,前者是可编辑状态下的(EditText),后者是静态的。

       /**
         * Draw this Layout on the specified Canvas.
        绘制在指定的画布
    */ public void draw(Canvas c) { draw(c, null, null, 0); } /** * Draw this Layout on the specified canvas, with the highlight path drawn * between the background and the text.
    在背景和文字之间绘制高亮 * *
    @param canvas the canvas * @param highlight the path of the highlight or cursor; can be null * @param highlightPaint the paint for the highlight * @param cursorOffsetVertical the amount to temporarily translate the * canvas while rendering the highlight */ public void draw(Canvas canvas, Path highlight, Paint highlightPaint, int cursorOffsetVertical) { final long lineRange = getLineRangeForDraw(canvas);//获取需要绘制的区间行 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);//第一行 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);//最后一行 if (lastLine < 0) return; drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical, firstLine, lastLine); drawText(canvas, firstLine, lastLine); }

    1.先看画背景:

    public void drawBackground(Canvas canvas, Path highlight, Paint highlightPaint,
                int cursorOffsetVertical, int firstLine, int lastLine) {
            // First, draw LineBackgroundSpans.//首先,绘制LineBackgroundSpans(不是View的Backgrond哦)
            // LineBackgroundSpans know nothing about the alignment, margins, or?/它不需要自动对齐方式,间距或方向
            // direction of the layout or line.  XXX: Should they?//xxx:需要吗?
            // They are evaluated at each line.//将会应用在每一行。
            if (mSpannedText) {//SpannedText才能设置Span
                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) {//行背景span数量
                    int previousLineBottom = getLineTop(firstLine);//记录上一行的top
                    int previousLineEnd = getLineStart(firstLine);//记录上一行的end
                    ParagraphStyle[] spans = NO_PARA_SPANS;//段落样式
                    int spansLength = 0;
                    TextPaint paint = mPaint;
                    int spanEnd = 0;
                    final int width = mWidth;
                    for (int i = firstLine; i <= lastLine; i++) {//遍历每行
                        int start = previousLineEnd;
                        int end = getLineStart(i + 1);//下一行的end
                        previousLineEnd = end;
    
                        int ltop = previousLineBottom;
                        int lbottom = getLineTop(i + 1);//获取下一行的top,也就是本行的bottom
                        previousLineBottom = lbottom;
                        int lbaseline = lbottom - getLineDescent(i);
    
                        if (start >= spanEnd) {
                            // These should be infrequent, so we'll use this so that
                            // we don't have to check as often.
                            spanEnd = mLineBackgroundSpans.getNextTransition(start, textLength);
                            // All LineBackgroundSpans on a line contribute to its background.
                            spansLength = 0;
                            // Duplication of the logic of getParagraphSpans
                            if (start != end || start == 0) {
                                // Equivalent to a getSpans(start, end), but filling the 'spans' local
                                // array instead to reduce memory allocation
                                for (int j = 0; j < mLineBackgroundSpans.numberOfSpans; j++) {//如果设置了多个LineBackgroundSpan将一一画上
                                    // equal test is valid since both intervals are not empty by
                                    // construction
                                    if (mLineBackgroundSpans.spanStarts[j] >= end ||
                                            mLineBackgroundSpans.spanEnds[j] <= start) continue;
                                    spans = GrowingArrayUtils.append(
                                            spans, spansLength, mLineBackgroundSpans.spans[j]);
                                    spansLength++;
                                }
                            }
                        }
    
                        for (int n = 0; n < spansLength; n++) {//所有的行数和行背景(line.number*span.number)
                            LineBackgroundSpan lineBackgroundSpan = (LineBackgroundSpan) spans[n];
                            lineBackgroundSpan.drawBackground(canvas, paint, 0, width,
                                    ltop, lbaseline, lbottom,
                                    buffer, start, end, i);
                        }
                    }
                }
                mLineBackgroundSpans.recycle();//SpanSet回收
            }
    
            // There can be a highlight even without spans if we are drawing
            // a non-spanned transformation of a spanned editing buffer.
            if (highlight != null) {//绘制hightlight路径(比如光标)
                if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical);
                canvas.drawPath(highlight, highlightPaint);
                if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical);
            }
        }

    至于lineBottom和linrEnd是由子类(DynamicLayout和StaticLayout)的getLineTop和getLineStart方法获取的,很复杂很复杂。

    2.画字drawText:

    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 tl = TextLine.obtain();
    
            // Draw the lines, one at a time.
            // The baseline is the top of the following line minus the current line's descent.
            for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {//遍历每行
                int start = previousLineEnd;//开始
                previousLineEnd = getLineStart(lineNum + 1);//记录end
                int end = getLineVisibleEnd(lineNum, start, previousLineEnd);//结束
    
                int ltop = previousLineBottom;//行top
                int lbottom = getLineTop(lineNum + 1);//行bottom,也就是下一行的top
                previousLineBottom = lbottom;//记录行Bottom
                int lbaseline = lbottom - getLineDescent(lineNum);//行基线,bottom-descent
    
                int dir = getParagraphDirection(lineNum);//段乱排版方向
                int left = 0;
                int right = mWidth;
    
           //一:画LeadingMargin
    if (mSpannedText) {//是spannedText Spanned sp = (Spanned) buf;//text int textLength = buf.length(); boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == ' ');//段落第一行 // New batch of paragraph styles, collect into spans array. // Compute the alignment, last alignment style wins. // Reset tabStops, we'll rebuild if we encounter a line with // tabs. // We expect paragraph spans to be relatively infrequent, use // spanEnd so that we can check less frequently. Since // paragraph styles ought to apply to entire paragraphs, we can // just collect the ones present at the start of the paragraph. // If spanEnd is before the end of the paragraph, that's not // our problem. 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; }
              //画出LeadingMarginSpan
    // Draw all leading margin spans. Adjust left or right according // to the paragraph direction of the line. 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 there is more than one LeadingMarginSpan2, use // the count that is greatest if (lineNum < startLine + count) { useFirstLineMargin = true; break; } } } for (int n = 0; n < length; n++) { if (spans[n] instanceof LeadingMarginSpan) {//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); } } } }
           //二:Tab或Emoji
    boolean hasTabOrEmoji = getLineContainsTab(lineNum); // Can't tell if we have tabs for sure, currently if (hasTabOrEmoji && !tabStopsIsInitialized) { if (tabStops == null) { tabStops = new TabStops(TAB_INCREMENT, spans); } else { tabStops.reset(TAB_INCREMENT, spans); } tabStopsIsInitialized = true; } // Determine whether the line aligns to normal, opposite, or center.
           //三:对齐方式
    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; }
                  //四:获取x轴,然后写字。
    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);
            //阅读方式从左向右的,没有tab和emoji表情,非SpannedText,就是最原始最传统最简单画文字cavans.drawText
    if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTabOrEmoji) { // XXX: assumes there's nothing additional to be done canvas.drawText(buf, start, end, x, lbaseline, paint); } else {//复杂的交给TextLine tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops); tl.draw(canvas, x, ltop, lbaseline, lbottom); } paint.setHyphenEdit(0); } TextLine.recycle(tl); }

    遍历每一行,主要是由四个流程:画LeadingMargin——>确认tab/emoji(TextLine来画)——>根据对齐方式确定从x轴哪个位置开始画(比如居左x就是0咯)——>

    根据条件判断是交给cavans直接drawText还是TextLine来画字。

    三:Canvas&TextLine

    1.先看Canvas的drawText方法,就看方法doc。

    /**
         * Draw the specified range of text, specified by start/end, with its
         * origin at (x,y), in the specified Paint. The origin is interpreted
         * based on the Align setting in the Paint.
         *
         * @param text     The text to be drawn
         * @param start    The index of the first character in text to draw
         * @param end      (end - 1) is the index of the last character in text
         *                 to draw
         * @param x        The x-coordinate of origin for where to draw the text
         * @param y        The y-coordinate of origin for where to draw the text
         * @param paint The paint used for the text (e.g. color, size, style)
         */
        public void drawText(@NonNull CharSequence text, int start, int end, float x, float y,
                @NonNull Paint paint) 

    这个注释说了三个点,一是画多少个字(start-end),二是从哪开始画,即原点(origin),这个有xy坐标轴来确定,基于对齐方式的设定,最后就是画笔paint。

    说明一下,start和end是从text里面的所以区段,而原点的x轴跟对齐方式相关,y轴一般是baseline。

    2.TextLine的draw流程:

    void draw(Canvas c, float x, int top, int y, int bottom) {
         //drawRun画字
    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; } }
         //根据字符转成emoji位图
    float h = 0; int[] runs = mDirections.mDirections; RectF emojiRect = null; int lastRunIndex = runs.length - 2; for (int i = 0; i < runs.length; i += 2) { ...for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { int codept = 0; Bitmap bm = null; ... bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept); ... emojiRect.set(x + h, y + bmAscent, x + h + width, y); c.drawBitmap(bm, null, emojiRect, mPaint); ... } } } }

    转入drawRun方法

    /**
         * Draws a unidirectional (but possibly multi-styled) run of text.
         *
         *
         * @param c the canvas to draw on
         * @param start the line-relative start
         * @param limit the line-relative limit
         * @param runIsRtl true if the run is right-to-left
         * @param x the position of the run that is closest to the leading margin
         * @param top the top of the line
         * @param y the baseline
         * @param bottom the bottom of the line
         * @param needWidth true if the width value is required.
         * @return the signed width of the run, based on the paragraph direction.
         * Only valid if needWidth is true.
         */
        private float drawRun(Canvas c, int start,
                int limit, boolean runIsRtl, float x, int top, int y, int bottom,
                boolean needWidth)

    而真正的方法还得往下走:

    private float handleRun(int start, int measureLimit,
                int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
                int bottom, FontMetricsInt fmi, boolean needWidth) {
    
            // Case of an empty line, make sure we update fmi according to mPaint
         //空行,更新FontMetricsInt
    if (start == measureLimit) { TextPaint wp = mWorkPaint; wp.set(mPaint); if (fmi != null) { expandMetricsFromPaint(fmi, wp); } return 0f; }
         //无mSpanned,直接handleText
    if (mSpanned == null) { TextPaint wp = mWorkPaint; wp.set(mPaint); final int mlimit = measureLimit; return handleText(wp, start, mlimit, start, limit, runIsRtl, c, x, top, y, bottom, fmi, needWidth || mlimit < measureLimit); }
         //初始化MetricAffectingSpan和CharacterStyleSpan mMetricAffectingSpanSpanSet.init(mSpanned, mStart
    + start, mStart + limit); mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit); // Shaping needs to take into account context up to metric boundaries, // but rendering needs to take into account character style boundaries. // So we iterate through metric runs to get metric bounds, // then within each metric run iterate through character style runs // for the run bounds. final float originalX = x; for (int i = start, inext; i < measureLimit; i = inext) { TextPaint wp = mWorkPaint; wp.set(mPaint); inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) - mStart; int mlimit = Math.min(inext, measureLimit); ReplacementSpan replacement = null;
            //遍历MetrixAffectingSpan
    for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) { // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT // empty by construction. This special case in getSpans() explains the >= & <= tests if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) || (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue; MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j]; if (span instanceof ReplacementSpan) {//ReplacementSpan特俗处理 replacement = (ReplacementSpan)span; } else { // We might have a replacement that uses the draw // state, otherwise measure state would suffice. span.updateDrawState(wp);//TextPaint抛出去 } }
           //处理ReplacementSpan
    if (replacement != null) { x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y, bottom, fmi, needWidth || mlimit < measureLimit); continue; }
           //遍历CharecterStyleSpan
    for (int j = i, jnext; j < mlimit; j = jnext) { jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + mlimit) - mStart; wp.set(mPaint); for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) { // Intentionally using >= and <= as explained above if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + jnext) || (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue; CharacterStyle span = mCharacterStyleSpanSet.spans[k]; span.updateDrawState(wp);//更新draw状态 } // Only draw hyphen on last run in line if (jnext < mLen) { wp.setHyphenEdit(0); }
              //渲染文字... x
    += handleText(wp, j, jnext, i, inext, runIsRtl, c, x, top, y, bottom, fmi, needWidth || jnext < measureLimit); } } return x - originalX; }

    看到这个TextLine主要还是处理SpannedText,遍历出MetricAffectingSpan和CharactStyleSpan,MetricAffectingSpan下面有个ReplacementSpan,其余

    的span都是更新draw状态,渲染文字最终还是在handleTextprivate float handleText(TextPaint wp, int start, int end,

    int contextStart, int contextEnd, boolean runIsRtl,
                Canvas c, float x, int top, int y, int bottom,
                FontMetricsInt fmi, boolean needWidth) {
         ...
    
            if (c != null) {
                if (runIsRtl) {
                    x -= ret;
                }
    
                if (wp.bgColor != 0) {//画背景色
                    int previousColor = wp.getColor();
                    Paint.Style previousStyle = wp.getStyle();
    
                    wp.setColor(wp.bgColor);
                    wp.setStyle(Paint.Style.FILL);
                    c.drawRect(x, top, x + ret, bottom, wp);
    
                    wp.setStyle(previousStyle);
                    wp.setColor(previousColor);
                }
    
                if (wp.underlineColor != 0) {//画下划线
                    // kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h
              //下划线.top=文字大小的1/9+baseline+baselineShift,也就是说是从baseline空格再往下字符大小的1/9

              float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize(); int previousColor = wp.getColor(); Paint.Style previousStyle = wp.getStyle(); boolean previousAntiAlias = wp.isAntiAlias(); wp.setStyle(Paint.Style.FILL); wp.setAntiAlias(true); wp.setColor(wp.underlineColor);
              //线的粗细,是在TextPaint中定义的 c.drawRect(x, underlineTop, x
    + ret, underlineTop + wp.underlineThickness, wp); wp.setStyle(previousStyle); wp.setColor(previousColor); wp.setAntiAlias(previousAntiAlias); } drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl, x, y + wp.baselineShift); } return runIsRtl ? -ret : ret; }

    那drawTextRun中的方法是怎么实现的呢?

    private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
                int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
    
            if (mCharsValid) {
                int count = end - start;
                int contextCount = contextEnd - contextStart;
                c.drawTextRun(mChars, start, count, contextStart, contextCount,
                        x, y, runIsRtl, wp);
            } else {
                int delta = mStart;
                c.drawTextRun(mText, delta + start, delta + end,
                        delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
            }
        }

    可以看到是调用canvas实现的,canvas都是通过native方法来实现的。

    最后上公众号,文章同步,手机阅读。

  • 相关阅读:
    [题解](组合数/二位前缀和)luogu_P2822组合数问题
    [题解](tarjan割点/点双)luogu_P3225_矿场搭建
    [题解](树形dp/换根)小x游世界树
    [題解](DP)CF713C_Sonya and Problem Wihtout a Legend
    [題解]hdu_6412公共子序列
    [題解](最小生成樹)luogu_P2916安慰奶牛
    [题解](堆)luogu_P1631序列合并
    [题解](最短路)luogu_P5122 Fine Dining
    [题解](次短路)luogu_P2865路障(未)
    [题解](最短路(树))luogu_P5201_short cut
  • 原文地址:https://www.cnblogs.com/bvin/p/5370490.html
Copyright © 2020-2023  润新知