【Android】TextView.onMeasure分析

2021-12-17  本文已影响0人  littlefogcat

onMeasure过程分为高度和宽度。

先看高度。

文中代码均删除了大部分,只留下关键步骤。

一、height

1. onMeasure

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width;
        int height;

        boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
        if (mLayout == null) {
            makeNewLayout(want, hintWant, boring, hintBoring,
                          width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
        }
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            int desired = getDesiredHeight();

            height = desired;

            if (heightMode == MeasureSpec.AT_MOST) {
                height = Math.min(desired, heightSize);
            }
        }

        setMeasuredDimension(width, height);
    }

可以看到,实际的测量高度是根据getDesiredHeight决定的。

2. getDesiredHeight

    private int getDesiredHeight() {
        return Math.max(
                getDesiredHeight(mLayout, true),
                getDesiredHeight(mHintLayout, mEllipsize != null));
    }

    private int getDesiredHeight(Layout layout, boolean cap) {
        if (layout == null) {
            return 0;
        }
        int desired = layout.getHeight(cap);
        final int padding = getCompoundPaddingTop() + getCompoundPaddingBottom();
        desired += padding;
        return desired;
    }

实际是调用了mLayout.getHeight(true)。而这个mLayout又是哪儿来的呢?

3. mLayout的由来

onMeasure中有这么一句:

        if (mLayout == null) {
            makeNewLayout(want, hintWant, boring, hintBoring,
                          width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
        }

继续跟踪:

    public void makeNewLayout(int wantWidth, int hintWidth,
                                 BoringLayout.Metrics boring,
                                 BoringLayout.Metrics hintBoring,
                                 int ellipsisWidth, boolean bringIntoView) {
        mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
                effectiveEllipsize, effectiveEllipsize == mEllipsize);
    }

    protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
            Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
            boolean useSaved) {
        Layout result = null;
        result = BoringLayout.make(mTransformed, mTextPaint,
                                wantWidth, alignment, mSpacingMult, mSpacingAdd,
                                boring, mIncludePad);
        return result;
    }

也就是说,mLayout是一个BoringLayout类型的对象。即,TextView的高度即是BoringLayout的高度。

4. BoringLayout.getHeight

    public int getHeight() {
        return mBottom;
    }

BoringLayout只是返回了mBottom变量。

mBotton在BoringLayout构造时即确定,其值是如此计算的:

        int spacing = metrics.bottom - metrics.top;
        mBottom = spacing;

而这里的metrics是在onMeasure中通过BoringLayout.isBoring()方法生成的,这个在 1. 中可以看到。

5. BoringLayout.isBoring

    public static Metrics isBoring(CharSequence text, TextPaint paint,
            TextDirectionHeuristic textDir, Metrics metrics) {
        Metrics fm = metrics;
        if (fm == null) {
            fm = new Metrics();
        } else {
            fm.reset();
        }
        TextLine line = TextLine.obtain();
        line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,
                Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
        fm.width = (int) Math.ceil(line.metrics(fm));
        TextLine.recycle(line);
        return fm;
    }

TextLine是辅助布局文字的类,在这里,TextLine.metrics(metrics)方法计算出了文字布局的宽高。

6. TextLine.metrics

    // TextLine.java

    float metrics(FontMetricsInt fmi) {
        return measure(mLen, false, fmi);
    }

    float measure(int offset, boolean trailing, FontMetricsInt fmi) {
        return measureRun(0, offset, mLen, false, fmi);
    }

    private float measureRun(int start, int offset, int limit, boolean runIsRtl,
            FontMetricsInt fmi) {
        return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
    }

    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) {
        if (mSpanned == null) {
            TextPaint wp = mWorkPaint;
            wp.set(mPaint);
            wp.setHyphenEdit(adjustHyphenEdit(start, limit, wp.getHyphenEdit()));
            return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
                    y, bottom, fmi, needWidth, measureLimit);
        }
    }

    private float handleText(TextPaint wp, FontMetricsInt fmi) {
        expandMetricsFromPaint(fmi, wp);
    }

    private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
        wp.getFontMetricsInt(fmi);
    }

    // Paint.java

    public int getFontMetricsInt(FontMetricsInt fmi) {
        return nGetFontMetricsInt(mNativePaint, mNativeTypeface, fmi);
    }

    private static native int nGetFontMetricsInt(long paintPtr,
            long typefacePtr, FontMetricsInt fmi);

就是一路把这个metrics对象传递下去,没啥可说的。

继续下去就到了native代码了,简单尝试看一下:

    static jint getFontMetricsInt(JNIEnv* env, jobject paint, jobject metricsObj) {
        NPE_CHECK_RETURN_ZERO(env, paint);
        Paint::FontMetrics metrics;
        getMetricsInternal(env, paint, &metrics);
        int ascent = SkScalarRoundToInt(metrics.fAscent);
        int descent = SkScalarRoundToInt(metrics.fDescent);
        int leading = SkScalarRoundToInt(metrics.fLeading);
        if (metricsObj) {
            SkASSERT(env->IsInstanceOf(metricsObj, gFontMetricsInt_class));
            env->SetIntField(metricsObj, gFontMetricsInt_fieldID.top, SkScalarFloorToInt(metrics.fTop));
            env->SetIntField(metricsObj, gFontMetricsInt_fieldID.ascent, ascent);
            env->SetIntField(metricsObj, gFontMetricsInt_fieldID.descent, descent);
            env->SetIntField(metricsObj, gFontMetricsInt_fieldID.bottom, SkScalarCeilToInt(metrics.fBottom));
            env->SetIntField(metricsObj, gFontMetricsInt_fieldID.leading, leading);
        }
        return descent - ascent + leading;
    }

static SkScalar getMetricsInternal(jlong paintHandle, SkFontMetrics *metrics) {
    SkFont* font = &paint->getSkFont();
    SkScalar spacing = font->getMetrics(metrics);
    return spacing;
}
SkScalar SkFont::getMetrics(SkFontMetrics* metrics) const {
    auto cache = strikeSpec.findOrCreateStrike();
    *metrics = cache->getFontMetrics();
    return metrics->fDescent - metrics->fAscent + metrics->fLeading;
}
const SkFontMetrics& SkScalerCache::getFontMetrics() const {
      return fFontMetrics;
}

c++代码看的我一头雾水,看不下去了。
大概就是根据具体字体来设定metrics的topbottomacsenddescend属性,不过具体的计算逻辑我并没有找到。其中acsenddescend分别指代最终测量出来的实际文字上下界与基线的距离,而topbottom分别指代对应Layout上下界与基线的距离。由于后者的绝对值一般大于前者,所以TextView上下都会有一些空隙。

到这,TextView的高度就测量出来了,即该metrics的bottom减去top

7. 总结

TextView需要通过一个Layout对象来帮助测量,最普通的Layout是BoringLayout

在onMeasure的过程中,首先通过BoringLayout.isBoring方法获得一个Metrics对象,再根据这个Metrics构造对应的BoringLayout对象并赋值给mLayout。这个Layout的高度也就是TextView测量出来的高度。

上一篇 下一篇

猜你喜欢

热点阅读