• 【转载】TextView源码解析


    原文地址:https://github.com/7heaven/AndroidSdkSourceAnalysis/blob/master/article/textview%E6%BA%90%E7%A2%BC%E8%A7%A3%E6%9E%90.md

    1.简介


    TextView作为Android系统上显示和排版文字以及提供对文字的增删改查、图文混排等功能的控件,内部是相对比较复杂的。这么一个复杂的控件自然需要依赖于一些其他的辅助类,例如:Layout以及Layout的相关子类、Span相关的类、MovementMethod接口、TransformationMethod接口等。这篇文章主要介绍TextView的结构和内部处理文字的流程以及TextView相关的辅助类在TextView处理文字过程中的作用。

    2.TextView的内部结构和辅助类


    TextView内部除了继承自View的相关属性和measure、layout、draw步骤,还包括:

    1. Layout: TextView的文字排版、折行策略以及文本绘制都是在Layout里面完成的,TextView的自身测量也受Layout的影响。Layout是TextView执行setText方法后,由TextView内部创建的实例,并不能由外部提供。可以用getLayout()方法获取。
    2. TransformationMethod: 用来处理最终的显示结果的类,例如显示密码的时候把密码转换成圆点。这个类并不直接影响TextView内部储存的Text,只影响显示的结果。
    3. MovementMethod: 用来处理TextView内部事件响应的类,可以针对TextView内文本的某一个区域做软键盘输入或者触摸事件的响应。
    4. Drawables: TextView的静态内部类,用来处理和储存TextView的CompoundDrawables,包括TextView的上下左右的Drawable以及错误提示的Drawable。
    5. Spans: Spans并不是特定的某一个类或者实现了某一个接口的类。它可以是任意类型,Spans实际上做的事情是在TextView的内部的text的某一个区域做标记。其中有部分Spans可以影响TextView的绘制和测量,如ImageSpan、BackgroundColorSpan、AbsoluteSizeSpan。还有可以响应点击事件的ClickableSpan。
    6. Editor: TextView作为可编辑文本控件的时候(EditText),使用Editor来处理文本的区域选择处理和判断、拼写检查、弹出文本菜单等。
    7. InputConnection: EditText的文本输入部分是在TextView中完成的。而InputConnection是软键盘和TextView之间的桥梁,所有的软键盘的输入文字、修改文字和删除文字都是通过InputConnection传递给TextView的。

    3.TextView的onTouchEvent处理


    TextView内部能处理触摸事件的,包括自身的触摸处理、Editor的onTouchEvent、MovementMethod的onTouchEvent。Editor的onTouchEvent主要处理出于编辑状态下的触摸事件,比如点击选中、长按等。MovementMethod则主要负责文本内部有Span的时候的相关处理,比较常见的就是LinkMovementMethod处理ClickableSpan的点击事件。我们来看一下TextView内部对这些触摸事件的处理和优先级的分配:

    public boolean onTouchEvent(MotionEvent event) {
            final int action = event.getActionMasked();
    
            //当Editor不为空的时候,给Editor的双击事件预设值
            if (mEditor != null && action == MotionEvent.ACTION_DOWN) {
                if (mFirstTouch && (SystemClock.uptimeMillis() - mLastTouchUpTime) <=
                        ViewConfiguration.getDoubleTapTimeout()) {
                    mEditor.mDoubleTap = true;
                    mFirstTouch = false;
                } else {
                    mEditor.mDoubleTap = false;
                    mFirstTouch = true;
                }
            }
    
            if (action == MotionEvent.ACTION_UP) {
                mLastTouchUpTime = SystemClock.uptimeMillis();
            }
    
            //当Editor不为空,优先处理Editor的触摸事件
            if (mEditor != null) {
                mEditor.onTouchEvent(event);
    
                //由于Editor内部onTouchEvent实际上交给了mSelectionModifierCursorController处理,所以这边判断mSelectionModifierCursorController是否需要处理接下来的一系列事件,如果是则直接返回跳过下面的步骤
                if (mEditor.mSelectionModifierCursorController != null &&
                        mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) {
                    return true;
                }
            }
    
            final boolean superResult = super.onTouchEvent(event);
    
            //处理API 23新加入的InsertionActinoMode
            if (mEditor != null && mEditor.mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
                mEditor.mDiscardNextActionUp = false;
    
                if (mEditor.mIsInsertionActionModeStartPending) {
                    mEditor.startInsertionActionMode();
                    mEditor.mIsInsertionActionModeStartPending = false;
                }
                return superResult;
            }
    
            final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) &&
                    (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused();
    
             if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
                    && mText instanceof Spannable && mLayout != null) {
                boolean handled = false;
    
                //MovementMethod的触摸时间处理,如果MovementMethod类型是LinkMovementMethod则会处理文本内的所有ClickableSpan的点击
                if (mMovement != null) {
                    handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
                }
    
                final boolean textIsSelectable = isTextSelectable();
                if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
                    
                    //在文本可选择的情况下,默认是没有LinkMovementMethod来处理ClickableSpan相关的点击的,所以在文本可选择情况,TextView对所有的ClickableSpan进行统一处理
                    ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
                            getSelectionEnd(), ClickableSpan.class);
    
                    if (links.length > 0) {
                        links[0].onClick(this);
                        handled = true;
                    }
                }
    
                if (touchIsFinished && (isTextEditable() || textIsSelectable)) {
                    final InputMethodManager imm = InputMethodManager.peekInstance();
                    viewClicked(imm);
                    if (!textIsSelectable && mEditor.mShowSoftInputOnFocus) {
                        handled |= imm != null && imm.showSoftInput(this, 0);
                    }
    
                    mEditor.onTouchUpEvent(event);
    
                    handled = true;
                }
    
                if (handled) {
                    return true;
                }
            }
    
            return superResult;
        }

     

    4.TextView的创建Layout的过程


    TextView内部并不仅仅只有一个用来显示文本内容的Layout,在设置了hint的时候,还需要有一个mHintLayout来处理hint的内容。如果设置了Ellipsize类型为Marquee时,还会有一个mSavedMarqueeModeLayout专门用来显示marquee效果。这些Layout都是通过内部的makeNewLayout方法来创建的:

    protected void makeNewLayout(int wantWidth, int hintWidth
                                     BoringLayout.Metrics boring,
                                     BoringLayout.Metrics hintBoring,
                                     int ellipsisWidth, boolean bringIntoView) {
            //如果当前有marquee动画,则先停止动画
            stopMarquee();
    
            mOldMaximum = mMaximum;
            mOldMaxMode = mMaxMode;
    
            mHighlightPathBogus = true;
    
            if (wantWidth < 0) {
                wantWidth = 0;
            }
            if (hintWidth < 0) {
                hintWidth = 0;
            }
    
            //文本对齐方式
            Layout.Alignment alignment = getLayoutAlignment();
            final boolean testDirChange = mSingleLine && mLayout != null &&
                (alignment == Layout.Alignment.ALIGN_NORMAL ||
                 alignment == Layout.Alignment.ALIGN_OPPOSITE);
            int oldDir = 0;
            if (testDirChange) oldDir = mLayout.getParagraphDirection(0);
      
            //检测是否设置了ellipsize
            boolean shouldEllipsize = mEllipsize != null && getKeyListener() == null;
            final boolean switchEllipsize = mEllipsize == TruncateAt.MARQUEE &&
                    mMarqueeFadeMode != MARQUEE_FADE_NORMAL;
            TruncateAt effectiveEllipsize = mEllipsize;
            if (mEllipsize == TruncateAt.MARQUEE &&
                    mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
                effectiveEllipsize = TruncateAt.END_SMALL;
            }
        
            //文本方向
            if (mTextDir == null) {
                mTextDir = getTextDirectionHeuristic();
            }
    
            //创建主Layout
            mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
                    effectiveEllipsize, effectiveEllipsize == mEllipsize);
      
            //非常规的Marquee模式下,需要创建mSavedMarqueeModeLayout来保存marquee动画时所用的Layout,并且在动画期间把它和TextView的主Layout对换
            if (switchEllipsize) {
                TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE ?
                        TruncateAt.END : TruncateAt.MARQUEE;
                mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment,
                        shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
            }
    
            shouldEllipsize = mEllipsize != null;
            mHintLayout = null;
    
            //判断是否需要创建hintLayout
            if (mHint != null) {
                if (shouldEllipsize) hintWidth = wantWidth;
    
                if (hintBoring == UNKNOWN_BORING) {
                    hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
                                                       mHintBoring);
                    if (hintBoring != null) {
                        mHintBoring = hintBoring;
                    }
                }
    
                //判断是否为boring,如果是则创建BoringLayout
                if (hintBoring != null) {
                    if (hintBoring.width <= hintWidth &&
                        (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
                        if (mSavedHintLayout != null) {
                            mHintLayout = mSavedHintLayout.
                                    replaceOrMake(mHint, mTextPaint,
                                    hintWidth, alignment, mSpacingMult, mSpacingAdd,
                                    hintBoring, mIncludePad);
                        } else {
                            mHintLayout = BoringLayout.make(mHint, mTextPaint,
                                    hintWidth, alignment, mSpacingMult, mSpacingAdd,
                                    hintBoring, mIncludePad);
                        }
    
                        mSavedHintLayout = (BoringLayout) mHintLayout;
                    } else if (shouldEllipsize && hintBoring.width <= hintWidth) {
                        if (mSavedHintLayout != null) {
                            mHintLayout = mSavedHintLayout.
                                    replaceOrMake(mHint, mTextPaint,
                                    hintWidth, alignment, mSpacingMult, mSpacingAdd,
                                    hintBoring, mIncludePad, mEllipsize,
                                    ellipsisWidth);
                        } else {
                            mHintLayout = BoringLayout.make(mHint, mTextPaint,
                                    hintWidth, alignment, mSpacingMult, mSpacingAdd,
                                    hintBoring, mIncludePad, mEllipsize,
                                    ellipsisWidth);
                        }
                    }
                }
              
                //不是boring的状态下,用StaticLayout来创建
                if (mHintLayout == null) {
                    StaticLayout.Builder builder = StaticLayout.Builder.obtain(mHint, 0,
                            mHint.length(), mTextPaint, hintWidth)
                            .setAlignment(alignment)
                            .setTextDirection(mTextDir)
                            .setLineSpacing(mSpacingAdd, mSpacingMult)
                            .setIncludePad(mIncludePad)
                            .setBreakStrategy(mBreakStrategy)
                            .setHyphenationFrequency(mHyphenationFrequency);
                    if (shouldEllipsize) {
                        builder.setEllipsize(mEllipsize)
                                .setEllipsizedWidth(ellipsisWidth)
                                .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
                    }
                    mHintLayout = builder.build();
                }
            }
    
            if (bringIntoView || (testDirChange && oldDir != mLayout.getParagraphDirection(0))) {
                registerForPreDraw();
            }
    
            //判断是否需要开始Marquee动画
            if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
                if (!compressText(ellipsisWidth)) {
                    final int height = mLayoutParams.height;
                    if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
                        startMarquee();
                    } else {
                        mRestartMarquee = true;
                    }
                }
            }
    
            if (mEditor != null) mEditor.prepareCursorControllers();
        }

     

    TextView的布局创建过程涉及到一个boring的概念,boring是指布局所用的文本里面不包含任何Span,所有的文本方向都是从左到右的布局,并且仅需一行就能显示完全的布局。这种情况下,TextView会使用BoringLayout类来创建相关的布局,以节省不必要的文本测量以及文本折行、Span宽度、文本方向等的计算。下面我们来看一下makeNewLayout中使用频率比较高的makeSingleLayout的代码:

    private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
                Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
                boolean useSaved) {
            Layout result = null;
            //判断是否Spannable,如果是则用DynamicLayout类来创建布局,DynamicLayout内部实际也是使用StaticLayout来做文本的测量绘制,并在StaticLayout的基础上增加了文本或者Span改变时的监听,及时对文本或者Span的变化做出反应。
            if (mText instanceof Spannable) {
                result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
                        alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad,
                        mBreakStrategy, mHyphenationFrequency,
                        getKeyListener() == null ? effectiveEllipsize : null, ellipsisWidth);
            } else {
                //如果boring是未知状态,则重新判断一次是否boring
                if (boring == UNKNOWN_BORING) {
                    boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
                    if (boring != null) {
                        mBoring = boring;
                    }
                }
    
                //根据boring的属性来创建对应的布局,如果有mSavedLayout则从mSavedLayout创建
                if (boring != null) {
                    if (boring.width <= wantWidth &&
                            (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
                        if (useSaved && mSavedLayout != null) {
                            //从之前保存的Layout中创建
                            result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
                                    wantWidth, alignment, mSpacingMult, mSpacingAdd,
                                    boring, mIncludePad);
                        } else {
                            //创建新的Layout
                            result = BoringLayout.make(mTransformed, mTextPaint,
                                    wantWidth, alignment, mSpacingMult, mSpacingAdd,
                                    boring, mIncludePad);
                        }
    
                        if (useSaved) {
                            mSavedLayout = (BoringLayout) result;
                        }
                    } else if (shouldEllipsize && boring.width <= wantWidth) {
                        if (useSaved && mSavedLayout != null) {
                            result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
                                    wantWidth, alignment, mSpacingMult, mSpacingAdd,
                                    boring, mIncludePad, effectiveEllipsize,
                                    ellipsisWidth);
                        } else {
                            result = BoringLayout.make(mTransformed, mTextPaint,
                                    wantWidth, alignment, mSpacingMult, mSpacingAdd,
                                    boring, mIncludePad, effectiveEllipsize,
                                    ellipsisWidth);
                        }
                    }
                }
            }
      
            //如果没有创建BoringLayout, 则使用StaticLayout类来创建布局
            if (result == null) {
                StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
                        0, mTransformed.length(), mTextPaint, wantWidth)
                        .setAlignment(alignment)
                        .setTextDirection(mTextDir)
                        .setLineSpacing(mSpacingAdd, mSpacingMult)
                        .setIncludePad(mIncludePad)
                        .setBreakStrategy(mBreakStrategy)
                        .setHyphenationFrequency(mHyphenationFrequency);
                if (shouldEllipsize) {
                    builder.setEllipsize(effectiveEllipsize)
                            .setEllipsizedWidth(ellipsisWidth)
                            .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
                }
                result = builder.build();
            }
            return result;
        }

     

    5.TextView的文字处理和绘制


    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) == '
    ');
    
                    //获得所有的段落风格相关的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 == '	' || bm != null) {
                        //绘制文字
                        h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
                                i != lastRunIndex || j != mLen);
    
                        if (codept == '	') {
                            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才能得到最终真实的坐标。

    6.TextView接收软键盘输入


    Android上的标准文本编辑控件是EditText,而EditText对软键盘输入的处理,却是在TextView内部实现的。Android为所有的View预留了一个接收软键盘输入的接口类,叫InputConnection。软键盘以InputConnection为桥梁把文字输入、文字修改、文字删除等传递给View。任意View只要重写onCheckIsTextEditor()并返回true,然后重写onCreateInputConnection(EditorInfo outAttrs)返回一个InputConnection的实例,便可以接收软键盘的输入。TextView的软键盘输入接收,是通过EditableInputConnection类来实现的。

    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
            //判断是否处于可编辑状态
            if (onCheckIsTextEditor() && isEnabled()) {
                mEditor.createInputMethodStateIfNeeded();
              
                //设置输入法相关的信息
                outAttrs.inputType = getInputType();
                if (mEditor.mInputContentType != null) {
                    outAttrs.imeOptions = mEditor.mInputContentType.imeOptions;
                    outAttrs.privateImeOptions = mEditor.mInputContentType.privateImeOptions;
                    outAttrs.actionLabel = mEditor.mInputContentType.imeActionLabel;
                    outAttrs.actionId = mEditor.mInputContentType.imeActionId;
                    outAttrs.extras = mEditor.mInputContentType.extras;
                } else {
                    outAttrs.imeOptions = EditorInfo.IME_NULL;
                }
                if (focusSearch(FOCUS_DOWN) != null) {
                    outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
                }
                if (focusSearch(FOCUS_UP) != null) {
                    outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
                }
                if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION)
                        == EditorInfo.IME_ACTION_UNSPECIFIED) {
                  
                    if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
                        //把软键盘的enter设为下一步
                        outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
                    } else {
                        //把软键盘的enter设为完成
                        outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
                    }
                    if (!shouldAdvanceFocusOnEnter()) {
                        outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
                    }
                }
                if (isMultilineInputType(outAttrs.inputType)) {
                    outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
                }
                outAttrs.hintText = mHint;
              
                //判断TextView内部文本是否可编辑
                if (mText instanceof Editable) {
                    //返回EditableInputConnection实例
                    InputConnection ic = new EditableInputConnection(this);
                    outAttrs.initialSelStart = getSelectionStart();
                    outAttrs.initialSelEnd = getSelectionEnd();
                    outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType());
                    return ic;
                }
            }
            return null;
        }

     

    我们再来看一下EditableInputConnection里面的几个主要的方法:

    首先是commitText方法,这个方法接收输入法输入的字符并提交给TextView。

    public boolean commitText(CharSequence text, int newCursorPosition) {
            //判断TextView是否为空
            if (mTextView == null) {
                return super.commitText(text, newCursorPosition);
            }
            //判断文本是否Span,来自输入法的Span一般只有SuggestionSpan,SuggestionSpan携带了输入法的错别字修正的词
            if (text instanceof Spanned) {
                Spanned spanned = ((Spanned) text);
                SuggestionSpan[] spans = spanned.getSpans(0, text.length(), SuggestionSpan.class);
                mIMM.registerSuggestionSpansForNotification(spans);
            }
    
            mTextView.resetErrorChangedFlag();
            //提交字符
            boolean success = super.commitText(text, newCursorPosition);
            mTextView.hideErrorIfUnchanged();
            //返回是否成功
            return success;
        }

     

    getEditable方法,这个方法并不是InputConnection接口的一部分,而是EditableInputConnection的父类BaseInputConnection的方法,用来获取一个可编辑对象,EditableInputConnection里面的所有修改都针对这个可编辑对象来做。

    public Editable getEditable() {
            TextView tv = mTextView;
            if (tv != null) {
                //返回TextView的可编辑对象
                return tv.getEditableText();
            }
            return null;
        }

     

    deleteSurroundingText方法,这个方法用来删除光标前后的内容:

    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
            if (DEBUG) Log.v(TAG, "deleteSurroundingText " + beforeLength
                    + " / " + afterLength);
            final Editable content = getEditable();
            if (content == null) return false;
    
            //批量删除标记
            beginBatchEdit();
            
            //获取当前已选择的文本的位置
            int a = Selection.getSelectionStart(content);
            int b = Selection.getSelectionEnd(content);
    
            if (a > b) {
                int tmp = a;
                a = b;
                b = tmp;
            }
    
            int ca = getComposingSpanStart(content);
            int cb = getComposingSpanEnd(content);
            if (cb < ca) {
                int tmp = ca;
                ca = cb;
                cb = tmp;
            }
            if (ca != -1 && cb != -1) {
                if (ca < a) a = ca;
                if (cb > b) b = cb;
            }
    
            int deleted = 0;
    
            //删除光标之前的文本
            if (beforeLength > 0) {
                int start = a - beforeLength;
                if (start < 0) start = 0;
                content.delete(start, a);
                deleted = a - start;
            }
            //删除光标之后的文本
            if (afterLength > 0) {
                b = b - deleted;
    
                int end = b + afterLength;
                if (end > content.length()) end = content.length();
    
                content.delete(b, end);
            }
            
            //结束批量编辑
            endBatchEdit();
            
            return true;
        }

     

    commitCompletion和commitCorrection方法,即是用来补全单词和修正错别字的方法,这两个方法内部都是调用TextView对应的方法来实现的。

    public boolean commitCompletion(CompletionInfo text) {
            if (DEBUG) Log.v(TAG, "commitCompletion " + text);
            mTextView.beginBatchEdit();
            mTextView.onCommitCompletion(text);
            mTextView.endBatchEdit();
            return true;
        }
    
        @Override
        public boolean commitCorrection(CorrectionInfo correctionInfo) {
            if (DEBUG) Log.v(TAG, "commitCorrection" + correctionInfo);
            mTextView.beginBatchEdit();
            mTextView.onCommitCorrection(correctionInfo);
            mTextView.endBatchEdit();
            return true;
        }

     

    8.总结


    一个展示文本+文本编辑器功能的控件需要做的事情很多,要对文本进行排版、处理不同的段落风格、处理段落内的不同emoji和span、进行折行计算,然后还需要做文本编辑、文本选择等。而TextView把这些事情明确分工给不同的类。这样不仅仅把复杂问题拆分成了一个个简单的小功能,同时也大大增加了可扩展性。

  • 相关阅读:
    js 添加事件 attachEvent 和 addEventListener 的用法
    zepto的tap事件的点透问题的几种解决方案
    CSS3弹性盒模型flexbox完整版教程
    移动端的几款jq插件
    CSS3阴影 box-shadow的使用
    offset
    事件驱动
    mysql处理重复数据仅保留一条记录
    k8s ingress路由强制跳转至https设置
    linux查看进程数
  • 原文地址:https://www.cnblogs.com/anni-qianqian/p/6728833.html
Copyright © 2020-2023  润新知