zoukankan      html  css  js  c++  java
  • 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方法来实现的。

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

  • 相关阅读:
    解决PKIX:unable to find valid certification path to requested target 的问题
    Linux 上的常用文件传输方式介绍与比较
    用VNC远程图形化连接Linux桌面的配置方法
    红帽中出现”This system is not registered with RHN”的解决方案
    linux安装时出现your cpu does not support long mode的解决方法
    CentOS SSH配置
    es6扩展运算符及rest运算符总结
    es6解构赋值总结
    tortoisegit安装、clon、推送
    es6环境搭建
  • 原文地址:https://www.cnblogs.com/bvin/p/5370490.html
Copyright © 2011-2022 走看看