对《View的工作原理》一文的补充一

随着工作经验的丰富,接触源码的增多,发现原本所写的博文有很多遗漏的地方,比如现在,这篇博文对《View的工作原理》的补充一样。主要是涉及到自定义ViewGroup类型的View的时候对子View的Margin参数的梳理。具体如下:

自定义ViewGroup类型的View的时候对子View的Margin参数的处理主要涉及到一个方法generateLayoutParams()和一个类MarginLayoutParams;

我们先来看看generateLayoutParams()这个方法的源码实现:

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

里面仅仅是返回了一个LayoutParams的对象,LayoutParams对象的构造方法如下:

public LayoutParams(Context c, AttributeSet attrs) {
        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
        setBaseAttributes(a, R.styleable.ViewGroup_Layout_layout_width, R.styleable.ViewGroup_Layout_layout_height);
        a.recycle();
}

LayoutParams对象的构造方法所做的事情也不多,我们继续看setBaseAttributes方法:

protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
        width = a.getLayoutDimension(widthAttr, "layout_width");
        height = a.getLayoutDimension(heightAttr, "layout_height");
}

我们可以看到,这里仅仅是获取我们xml布局里面写好的宽高,也就是说generateLayoutParams()方法生成的LayoutParams属性只有layout_width和layout_height的两个属性值。

我们再来看看MarginLayoutParams类构造方法的实现:

	public MarginLayoutParams(Context c, AttributeSet attrs) {
        super();

        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
        setBaseAttributes(a, R.styleable.ViewGroup_MarginLayout_layout_width, R.styleable.ViewGroup_MarginLayout_layout_height);

        int margin = a.getDimensionPixelSize(com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
        if (margin >= 0) {
            leftMargin = margin;
            topMargin = margin;
            rightMargin= margin;
            bottomMargin = margin;
        } else {
            int horizontalMargin = a.getDimensionPixelSize(R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1);
            int verticalMargin = a.getDimensionPixelSize(R.styleable.ViewGroup_MarginLayout_layout_marginVertical, -1);
            if (horizontalMargin >= 0) {
                leftMargin = horizontalMargin;
                rightMargin = horizontalMargin;
            } else {
                leftMargin = a.getDimensionPixelSize(R.styleable.ViewGroup_MarginLayout_layout_marginLeft, UNDEFINED_MARGIN);
                if (leftMargin == UNDEFINED_MARGIN) {
                    mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK;
                    leftMargin = DEFAULT_MARGIN_RESOLVED;
                }
                rightMargin = a.getDimensionPixelSize(R.styleable.ViewGroup_MarginLayout_layout_marginRight, UNDEFINED_MARGIN);
                if (rightMargin == UNDEFINED_MARGIN) {
                    mMarginFlags |= RIGHT_MARGIN_UNDEFINED_MASK;
                    rightMargin = DEFAULT_MARGIN_RESOLVED;
                }
            }
            startMargin = a.getDimensionPixelSize(R.styleable.ViewGroup_MarginLayout_layout_marginStart, DEFAULT_MARGIN_RELATIVE);
            endMargin = a.getDimensionPixelSize(R.styleable.ViewGroup_MarginLayout_layout_marginEnd, DEFAULT_MARGIN_RELATIVE);
            if (verticalMargin >= 0) {
                topMargin = verticalMargin;
                bottomMargin = verticalMargin;
            } else {
                topMargin = a.getDimensionPixelSize(R.styleable.ViewGroup_MarginLayout_layout_marginTop, DEFAULT_MARGIN_RESOLVED);
                bottomMargin = a.getDimensionPixelSize(R.styleable.ViewGroup_MarginLayout_layout_marginBottom, DEFAULT_MARGIN_RESOLVED);
            }
            if (isMarginRelative()) {
               mMarginFlags |= NEED_RESOLUTION_MASK;
            }
        }
        final boolean hasRtlSupport = c.getApplicationInfo().hasRtlSupport();
        final int targetSdkVersion = c.getApplicationInfo().targetSdkVersion;
        if (targetSdkVersion < JELLY_BEAN_MR1 || !hasRtlSupport) {
            mMarginFlags |= RTL_COMPATIBILITY_MODE_MASK;
        }

        // Layout direction is LTR by default
        mMarginFlags |= LAYOUT_DIRECTION_LTR;
        a.recycle();
    }

这里我们可以看到:MarginLayoutParams的构造方法主要干了三件事:

一是通过下面这个方法,获取我们在xml文件中设置的宽高:

setBaseAttributes(a, R.styleable.ViewGroup_MarginLayout_layout_width, R.styleable.ViewGroup_MarginLayout_layout_height);

这一点,跟前面的 generateLayoutParams(AttributeSet attrs)是一致的;

二是提取layout_margin的值并设置,这里对应的场景是我们给子View设置统一的margin参数:

android:layout_margin="xxxx"

三是分门别类的提取margin值,对应着以下三个子场景:

一是我们设置了android:layout_marginHorizontal="xxxx",但top和bottom以另外的方式设置的;
二是我们设置了android:layout_marginVertical="xxxx",但是left和right是以另外的方式设置的;
三是left、top、right、bottom都是独立设置的。如下:

......else {
            int horizontalMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1);
            int verticalMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginVertical, -1);

            if (horizontalMargin >= 0) {
                leftMargin = horizontalMargin;
                rightMargin = horizontalMargin;
            } else {
                leftMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
                        UNDEFINED_MARGIN);
                if (leftMargin == UNDEFINED_MARGIN) {
                    mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK;
                    leftMargin = DEFAULT_MARGIN_RESOLVED;
                }
                rightMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginRight,
                        UNDEFINED_MARGIN);
                if (rightMargin == UNDEFINED_MARGIN) {
                    mMarginFlags |= RIGHT_MARGIN_UNDEFINED_MASK;
                    rightMargin = DEFAULT_MARGIN_RESOLVED;
                }
            }

            startMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginStart,
                    DEFAULT_MARGIN_RELATIVE);
            endMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
                    DEFAULT_MARGIN_RELATIVE);

            if (verticalMargin >= 0) {
                topMargin = verticalMargin;
                bottomMargin = verticalMargin;
            } else {
                topMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginTop,
                        DEFAULT_MARGIN_RESOLVED);
                bottomMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginBottom,
                        DEFAULT_MARGIN_RESOLVED);
            }

            if (isMarginRelative()) {
               mMarginFlags |= NEED_RESOLUTION_MASK;
            }
        }

这里就是对layout_marginLeft、layout_marginRight、layout_marginTop、layout_marginBottom的值一个个提取的过程。
因此到这里,我们便知道:

自定义ViewGroup类的View时,如果子View对margin参数有要求的时候,就必须重写MarginLayoutParams();单单处理generateLayoutParams()函数是不够的的,因为默认的generateLayoutParams()函数内部返回的LayoutParams对象的构造方法里面只是提取了layout_width、layout_height的值,没有做其他的处理,这时margin参数是不生效的,均为默认值0;而作为继承了LayoutParams的子类,MarginLayoutParams的构造方法才具有提取margin的能力,获得具体的Margin值。

我们的具体实现是在onMeasure()方法里面进行的:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
    int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
    int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
    int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);

    int height = 0;
    int width = 0;
    int count = getChildCount();
    for (int i=0;i<count;i++) {
		//注意这里就是重点了,//注意这里就是重点了,//注意这里就是重点了
        View child = getChildAt(i);
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
        int childHeight = child.getMeasuredHeight();
        int childWidth = child.getMeasuredWidth();

        height += childHeight;
        width = Math.max(childWidth, width);
    }

    setMeasuredDimension(
    (measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width, 
    (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight  : height);
}

onLayout()方法则简单很多:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int top = 0;
    int count = getChildCount();
    for (int i=0;i<count;i++) {

        View child = getChildAt(i);

        int childHeight = child.getMeasuredHeight();
        int childWidth = child.getMeasuredWidth();

        child.layout(0, top, childWidth, top + childHeight);
        top += childHeight;
    }
}

现在我们来看一看这两个方法反馈在代码上的结果:
一、我们重写MarginLayoutParams构造方法的代码:

public class SuiYiXie extends LinearLayout {
public SuiYiXie(@NonNull Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int wMod = MeasureSpec.getMode(widthMeasureSpec);
    int sSize = MeasureSpec.getSize(widthMeasureSpec);
    int hMod = MeasureSpec.getMode(heightMeasureSpec);
    int hSize = MeasureSpec.getSize(heightMeasureSpec);

    int width = 0;
    int height = 0;
    int count = getChildCount();
    
    for (int i = 0; i < count; i++) {
        View view = getChildAt(i);
        measureChild(view, widthMeasureSpec, heightMeasureSpec);
        MarginLayoutParams mlp = (MarginLayoutParams) view.getLayoutParams();
        int childHeight = view.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;
        int childWidth = view.getMeasuredHeight() + mlp.leftMargin + mlp.leftMargin;
        height += childHeight;
        width = Math.max(width, childWidth);
    }
    setMeasuredDimension(wMod == MeasureSpec.AT_MOST ? width : sSize, hMod == MeasureSpec.AT_MOST ? height : hSize);
}

}
运行结果如下:
在这里插入图片描述

二、我们调用默认的generateLayoutParams()方法,不重写MarginLayoutParams构造方法的代码:

public class SuiYiXie extends LinearLayout {
    public SuiYiXie(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int wMod = MeasureSpec.getMode(widthMeasureSpec);
        int sSize = MeasureSpec.getSize(widthMeasureSpec);
        int hMod = MeasureSpec.getMode(heightMeasureSpec);
        int hSize = MeasureSpec.getSize(heightMeasureSpec);

        int width = 0;
        int height = 0;
        int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View view = getChildAt(i);
            measureChild(view, widthMeasureSpec, heightMeasureSpec);
            int childHeight = view.getMeasuredHeight();
            int childWidth = view.getMeasuredWidth();
            height += childHeight;
            width = Math.max(width, childWidth);
        }

   	 setMeasuredDimension(wMod == MeasureSpec.AT_MOST ? width : sSize, hMod == MeasureSpec.AT_MOST ? height : hSize);
 	}
}

结果如下:
在这里插入图片描述
会发现,不重写MarginLayoutParams构造方法的时候,我们少了一个子View不说,就连下面的间距marginBottom都没有了,差距不是一点点的小。

注意:
在onMeasure()之后才能调用getMeasuredWidth()获得值;同样,只有调用onLayout()后,getWidth()才能获取值。
getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过layout(left,top,right,bottom)方法设置的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值