【Android】TextView.onMeasure分析
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的top
、bottom
、acsend
、descend
属性,不过具体的计算逻辑我并没有找到。其中acsend
和descend
分别指代最终测量出来的实际文字上下界与基线的距离,而top
、bottom
分别指代对应Layout上下界与基线的距离。由于后者的绝对值一般大于前者,所以TextView上下都会有一些空隙。
到这,TextView的高度就测量出来了,即该metrics的bottom
减去top
。
7. 总结
TextView需要通过一个Layout对象来帮助测量,最普通的Layout是BoringLayout
。
在onMeasure的过程中,首先通过BoringLayout.isBoring
方法获得一个Metrics对象,再根据这个Metrics构造对应的BoringLayout对象并赋值给mLayout
。这个Layout的高度也就是TextView测量出来的高度。