android onMeasure 实现

ViewonMeasure

先看普通 View的测量方法

onMeasure要关注那些内容:
子控件的宽高能大于父控件吗?
<LinearLayout
        android:layout_width="400dp"
        android:layout_height="wrap_content"
        android:background="#3333"
        android:orientation="vertical">

        <com.python.cat.potato.view.custom.ItemView
            android:layout_width="600dp"
            android:layout_height="wrap_content"
            android:background="#600f" />

        <TextView
            android:id="@+id/tv_test"
            android:layout_width="700dp"
            android:layout_height="80dp"
            android:text="@string/task_count" />
    </LinearLayout>

View testView = view.findViewById(R.id.tv_test);

testView.post(() -> {
    LogUtils.w("700dp==" + SizeUtils.dp2px(700));
    LogUtils.w("testView " + testView.getWidth() + " , " + testView.getHeight());
    ViewGroup parent = (ViewGroup) testView.getParent();
    LogUtils.w("testView.parent " + parent.getWidth() + " , " + parent.getHeight());
});

输出log:

2018-12-19 23:51:28.699 7096-7096/ W/LogUtils:  700dp==1837
2018-12-19 23:51:28.700 7096-7096/ W/LogUtils : testView 1838 , 210
2018-12-19 23:51:28.700 7096-7096/W/LogUtils: testView.parent1050 , 236

通过日志可以看到系统控件并没有对这种情况进行处理,那么,自定义控件也就不用处理这种情况。

正常的处理逻辑
  • MeasureSpec.AT_MOST的情况下,定义一个最小值,可以直接是0,或者是一个自定义的最小值,比如10dp.
  • 在其他情况下,使用父控件建议的宽高。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  
    int fw = resolveMeasure(widthMeasureSpec, MIN_WIDTH);
    int fh = resolveMeasure(heightMeasureSpec, MIN_HEIGHT);
    LogUtils.d("fw=" + fw + " , " + fh);
    setMeasuredDimension(fw, fh);
}

private int resolveMeasure(int lenMeasureSpec, int minLength) {
    int len;
    int mode = MeasureSpec.getMode(lenMeasureSpec);
    int size = MeasureSpec.getSize(lenMeasureSpec);
    switch (mode) {
        case MeasureSpec.AT_MOST:
            len = minLength;
            break;
        default:
            len = size;
            break;
    }
    return len;
}

在不重写 onLayout() 的前提下 ,setMeasuredDimension(fw, fh); 里面设置的大小,就是当前控件的尺寸大小了。

ViewGrouponMeasure

对于容器而言,宽高往往跟里面的item大小有关,而且,要考虑对item设置margin的支持。

measureChildmeasureChildWithMargins的区别
  • 简单来说,就是一句话: measureChildWithMargins支持给child设置 margin,而measureChild 不支持。

  • 细分的话,通过这两个方法测量的child,在调用getMeasuredWidth/Height的时候,获取的值是不一样的(前提是给child设置了margin)。

// 二者之间的关系可以用一行表达式来说明:
int measuredChildWidth = child.lp.marginLeft + child.lp.marginRight + measureChildWithMarginsWidth;

这两个方法都是ViewGroup.java里面的实现方法。其实源码很短,可以直接看一下。

    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this view
     */
    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     * @param parentHeightMeasureSpec The height requirements for this view
     * @param heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     */
    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);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

只看宽度,高度同理。对于宽度,

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);  // 这是 measureChild() 里面的宽度
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);  // 这是 measureChildWithMargins() 里面的宽度设置

可以看出,对于 带margin 的测量方法,比起不带margin而言,多了
+ lp.leftMargin + lp.rightMargin + widthUsed 这样的一个数值。那么,这里的 widthUsed 应该写多少呢?

我一开始也不知道这里应该写多少,但是对比一下上面的方法就知道了,下面这个方法的目的是支持child设置margin ,那么,widthUsed这个变量就没什么作用了,直接设置为0即可。

但是这样说似乎也没有很强的说服力,看一下很多系统自带的容器控件,在使用这个方法的时候,给widthUsed赋值为多少。几乎全部是0,heightUsed也是一样,全部是0。

比如FrameLayout,ActionBarView等。

好,那对于自定义的ViewGrouponMeasure 就好办了。

比如,我们自定义一个线性布局,竖直排列的。那么,onMeasure就可以这样写:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int ws = MeasureSpec.getSize(widthMeasureSpec);
        int wm = MeasureSpec.getMode(widthMeasureSpec);
        int hs = MeasureSpec.getSize(heightMeasureSpec);
        int hm = MeasureSpec.getMode(heightMeasureSpec);
        int childCount = getChildCount();
        int width = 0;
        int height = 0;
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            int measuredWidth = child.getMeasuredWidth();
            int measuredHeight = child.getMeasuredHeight();
            height += measuredHeight + lp.topMargin + lp.bottomMargin;
            width = measuredWidth + lp.leftMargin + lp.rightMargin;
            width = Math.min(width, ws);
        }
        int finalW = wm == MeasureSpec.EXACTLY ? ws : width;
        int finalH = hm == MeasureSpec.EXACTLY ? hs : height;
        com.apkfuns.logutils.LogUtils.i("measure: " + finalW + " , " + finalH);
        setMeasuredDimension(finalW, finalH);
    }

特别注意,这里面测量 child 的尺寸的目的还是为了设置自己的尺寸。

然后,既然测量的时候需要考虑 margin,那么 布局的时候,也是一样。

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        com.apkfuns.logutils.LogUtils.d(String.format("%s -- %s , %s , %s , %s", changed, l, t, r, b));
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

//            LogUtils.d(lp);
            l = lp.leftMargin;
            t = t + lp.topMargin;
            r = l + child.getMeasuredWidth();
            b = t + child.getMeasuredHeight();
            child.layout(l, t, r, b);
            t += child.getMeasuredHeight() + lp.bottomMargin;
//            break;
            com.apkfuns.logutils.LogUtils.d("item width:" + (lp.rightMargin + lp.leftMargin + child.getMeasuredWidth()));
        }
    }

这样,就在布局 child的时候,设置上了对应的margin

注意:以上只是实现了对margin的支持,至于 padding并没有。

完成代码(添加了对自身 padding 的支持):

public class LineLayout extends ViewGroup {


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

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

    public LineLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

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

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        LogUtils.d("size w=" + w + " ,,, h=" + h);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int ws = MeasureSpec.getSize(widthMeasureSpec);
        int wm = MeasureSpec.getMode(widthMeasureSpec);
        int hs = MeasureSpec.getSize(heightMeasureSpec);
        int hm = MeasureSpec.getMode(heightMeasureSpec);
        int childCount = getChildCount();
        int width = 0;
        int height = 0;
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            int measuredWidth = child.getMeasuredWidth();
            int measuredHeight = child.getMeasuredHeight();
            height += measuredHeight + lp.topMargin + lp.bottomMargin;
            width = measuredWidth + lp.leftMargin + lp.rightMargin;
            width = Math.min(width, ws);
        }
        int finalW = wm == MeasureSpec.EXACTLY ? ws : width + getPaddingLeft() + getPaddingRight();
        int finalH = hm == MeasureSpec.EXACTLY ? hs : height + getPaddingTop() + getPaddingBottom();
        com.apkfuns.logutils.LogUtils.i("measure: " + finalW + " , " + finalH);
        setMeasuredDimension(finalW, finalH);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        com.apkfuns.logutils.LogUtils.d(String.format("%s -- %s , %s , %s , %s", changed, l, t, r, b));
        t = getPaddingTop();
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

//            LogUtils.d(lp);
            l = lp.leftMargin + getPaddingLeft();
            t = t + lp.topMargin;
            r = l + child.getMeasuredWidth();
            b = t + child.getMeasuredHeight();
            child.layout(l, t, r, b);
            t += child.getMeasuredHeight() + lp.bottomMargin;
//            break;
            com.apkfuns.logutils.LogUtils.d("item width:" + (lp.rightMargin + lp.leftMargin + child.getMeasuredWidth()));
        }
    }

    @Override
    protected MarginLayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值