自定义控件中,measure的流程

继承 View 的子类

一般来说继承 View 的子类需要重写 onMeasure() ,会在 measure() 中被调用,而 measure() 是被 final 修饰的,也就表明它不希望被重写,所以只要重写 onMeasure() 完成测量即可。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

onMeasure() 只有一行代码,进入 getDefaultSize()

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

可以发现,这里面的 AT_MOST 和 EXACTLY 两种模式,得到的 size 都是一样。也就是说,不论 View 设置 wrap_content 还是 match_parent,getDefaultSize() 都会返回父容器剩余的空间。所以,在自定义 View 的时候,如果不重写 onMeasure(),设置宽高为 wrap_content 或 match_parent 时,展示是没有任何区别的。

下面我们先看下熟悉的 TextView 的 onMeasure() 的源码。

TextView 源码分析

@Override
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;
    ...省略代码...
    if (widthMode == MeasureSpec.EXACTLY) {
        // Parent has told us how big to be. So be it.
        width = widthSize;// 当前view的尺寸就为父容器的尺寸
    } else {
        ...省略代码...
        if (widthMode == MeasureSpec.AT_MOST) {
            width = Math.min(widthSize, width);// 当前view的尺寸就为内容尺寸和父容器尺寸当中的最小值
        }
    }
    ...省略代码...
    if (heightMode == MeasureSpec.EXACTLY) {
        // Parent has told us how big to be. So be it.
        height = heightSize;// 当前view的尺寸就为父容器的尺寸
        mDesiredHeightAtMeasure = -1;
    } else {
        ...省略代码...
        if (heightMode == MeasureSpec.AT_MOST) {
            height = Math.min(desired, heightSize);// 当前view的尺寸就为内容尺寸和父容器尺寸当中的最小值
        }
    }
    ...省略代码...
    setMeasuredDimension(width, height);// 调用View的方法
}

最后调用 View.java 的 setMeasuredDimension() 保存 measuredWidth 和 measuredHeight。

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    // 保存宽高,注意是measuredWidthm不是width
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

套路总结

View 的 measure 的流程就是 measure -> onMeasure -> setMeasuredDimension -> setMeasuredDimensionRaw

在自定义 View 只需要重写 onMeasure() 测量自己的宽高,最终调用 setMeasuredDimension() 保存自己的测量宽高。
伪代码:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
    int mode = MeasureSpec.getMode(measureSpec);
    int size = MeasureSpec.getSize(measureSpec);
    
    int viewSize = 0;
    switch (mode) {
        case MeasureSpec.EXACTLY:
            viewSize = size;//当前view的尺寸就为父容器的尺寸
            break;
        case MeasureSpec.AT_MOST:
            viewSize = Math.min(size, getContentSize());//当前view的尺寸就为内容尺寸和费容器尺寸当中的最小值。
            break;
        case MeasureSpec.UNSPECIFIED:
            viewSize = getContentSize();//内容有多大,久设置多大尺寸。
            break;
        default:
            break;
    }
    setMeasuredDimension(viewSize);
}

继承 ViewGroup 的子类

和 View 一样,只需要重写 onMeasure() 即可,但是里面涉及到 child 的测量,还是比较复杂的。

FrameLayout 源码分析

这里目的是总结归纳,所以简化并修改了一些代码,源码要比下面复杂的多。
FrameLayout 中的 onMeasure() 方法。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        ...省略代码...
        measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);// ①
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        maxWidth = Math.max(maxWidth,
                child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
        maxHeight = Math.max(maxHeight,
                child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
    }
    ...省略代码...
    // ⑤ 保存 FrameLayout 的 measuredWidth 和 measuredHeight
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
    ...省略代码...
}

ViewGroup 的 measureChildWithMargins() 方法。

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    // ② 为每一个 child 计算 MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);
    // ③ 对child 完成测量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

View 的 resolveSizeAndState() 方法。

// ④ 计算 FrameLayout 的 measuredWidth 和 measuredHeight
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

上面主要的流程是

  1. ②处 为每一个 child 计算 MeasureSpec。
  2. ③处 对 child 完成测量。
  3. ④处 计算 FrameLayout 的 measuredWidth 和 measuredHeight。
  4. ⑤处 保存 FrameLayout 的 measuredWidth 和 measuredHeight。

LinearLayout 源码分析

这里目的是总结归纳,所以简化并修改了一些代码,源码要比下面复杂的多。
LinearLayout 有 HORIZONTAL 和 VERTICAL 两种样式,这里就用纵向举例 measureVertical() 方法。

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {

    int maxWidth = 0;
    int childState = 0;

    final int count = getVirtualChildCount();

    ...省略代码...

    // See how tall everyone is. Also remember max width.
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
    ...省略代码...
        measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                heightMeasureSpec, usedHeight);// ①
    ...省略代码...
        final int measuredWidth = child.getMeasuredWidth() + margin;
        maxWidth = Math.max(maxWidth, measuredWidth);
    }

    maxWidth += mPaddingLeft + mPaddingRight;

    // Check against our minimum width
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            heightSizeAndState);// ⑥ 保存 LinearLayout 的 measuredWidth 和 measuredHeight。
}

LinearLayout 的 measureChildBeforeLayout() 方法。

void measureChildBeforeLayout(View child, int childIndex,
        int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
        int totalHeight) {
  measureChildWithMargins(child, widthMeasureSpec, totalWidth,
            heightMeasureSpec, totalHeight);// ②
}

ViewGroup 的 measureChildWithMargins() 方法。

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);// ③ 为每一个 child 计算 MeasureSpec
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);// ③ 为每一个 child 计算 MeasureSpec

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);// ④ 对child 完成测量
}

View 的 resolveSizeAndState() 方法。

// ⑤ 计算 LinearLayout 的 measuredWidth 和 measuredHeight
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

上面的流程是

  1. ③处 为每一个 child 计算 MeasureSpec。
  2. ④处 对 child 完成测量。
  3. ⑤处 计算 LinearLayout 的 measuredWidth 和 measuredHeight。
  4. ⑥处 保存 LinearLayout 的 measuredWidth 和 measuredHeight。

套路总结

ViewGroup 的 measure 的流程就是 measure -> onMeasure(测量子控件的宽高) -> setMeasuredDimension -> setMeasuredDimensionRaw(保存自己宽高)。
通过 FrameLayout 和 LinearLayout 不难看出 自定义 ViewGroup 的 measure 的流程。主要是两点:

  1. 测量所有子控件的尺寸。
  2. 设置自己的尺寸。

伪代码:

// 为每一个child计算测量规格信息(MeasureSpec)
getChildMeasureSpec();
// 将上面测量后的结果,传给每一个子View,子view测量自己的尺寸
child.measure();
// 子View测量完,ViewGroup就可以拿到这个子View的测量后的尺寸了
child.getChildMeasuredSize();//child.getMeasuredWidth() 和 child.getMeasuredHeight()
// ViewGroup自己就可以根据自身的情况(Padding等等),来计算自己的尺寸
ViewGroup.calculateSelfSize();
// 保存ViewGroup自己的尺寸
setMeasuredDimension(size);

自定义 ViewGroup 的实现

最后写个 demo,按照上面总结的套路,自定义 ViewGroup ,实现下面的效果。


自定义 ViewGroup

@UiThread
public class MyViewGroup extends ViewGroup {

    private static final int OFFSET = 80; // 每个child横向偏移量

    public MyViewGroup(Context context) {
        this(context, null);
    }

    public MyViewGroup(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width = 0;
        int height = 0;

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            LayoutParams lp = child.getLayoutParams();
            // 为每一个child计算测量规格信息(MeasureSpec)
            int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
            int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);
            // 将上面测量后的结果,传给每一个子View,子view测量自己的尺寸
            child.measure(childWidthSpec, childHeightSpec);
        }

        // ViewGroup自己就可以根据自身的情况(Padding等等),来计算自己的尺寸
        switch (widthMode) {
            case MeasureSpec.EXACTLY:
                width = widthSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                for (int i = 0; i < childCount; i++) {
                    View child = getChildAt(i);
                    // 子View测量完,ViewGroup就可以拿到这个子View的测量后的尺寸了
                    int widthAndOffset = i * OFFSET + child.getMeasuredWidth();
                    width = Math.max(width, widthAndOffset);
                }
                break;
            default:
                break;
        }


        switch (heightMode) {
            case MeasureSpec.EXACTLY:
                height = heightSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                for (int i = 0; i < childCount; i++) {
                    View child = getChildAt(i);
                    // 子View测量完,ViewGroup就可以拿到这个子View的测量后的尺寸了
                    height = height + child.getMeasuredHeight();
                }
                break;
            default:
                break;
        }
        // 保存ViewGroup自己的尺寸
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 摆放
        int left = 0;
        int top = 0;
        int right = 0;
        int bottom = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            left = i * OFFSET;
            right = left + child.getMeasuredWidth();
            bottom = top + child.getMeasuredHeight();
            child.layout(left, top, right, bottom);

            top += child.getMeasuredHeight();
        }
    }
}

布局文件

<?xml version="1.0" encoding="utf-8"?>
<com.ff.ui.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/darker_gray">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我是文本aaaa" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我是文本bbbb" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我是文本cccc" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我是文本ddddd" />

</com.ff.ui.MyViewGroup>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容