随着工作经验的丰富,接触源码的增多,发现原本所写的博文有很多遗漏的地方,比如现在,这篇博文对《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)方法设置的。