自定义控件Android自定义View

自定义控件中,measure的流程

2019-03-26  本文已影响17人  JamFF

继承 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>
上一篇下一篇

猜你喜欢

热点阅读