LayoutParams
在前面的measure过程中我们知道,当父View给子View构建MeasureSpec的时候不是直接构建的,而是根据父View的宽高模式和子View的LayoutParams共同的作用下生成MeasureSpec。
这里就关乎到了LayoutParams,从名字可以看出这是布局参数,也就是子View所携带的布局参数,通常是在xml中设置的,如我们常用的android:layout_width等,最终在解析为View的时候都会将某些参数封装到LayoutParams中由View携带。
这个LayoutParms代表的是子View本身的意愿,父View对子View进行measure和layout的时候都会参考到它的意愿。
另外,上一章谈到测量子View的时候最好使用measureChildWithMargins方法进行测量,因为这样会使得控件更加友好。但是,在该方法中,ViewGroup将LayoutParams强转成了MarginLayoutParams,而强转很容易抛出类型转换异常,而这样是否会出现问题则需要看child.getLayoutParams()的返回值具体是什么,即child的LayoutParams是如何生成的。
由于LayoutParams中存放的是View的属性,View的属性我们通常是在xml中设置的,那么这里又将涉及到View从xml到对象的解析过程,这里涉及的比较复杂,而这里又只是为了分析LayoutParams的生成,就简单的过一遍这个流程吧。(非通过布局添加的View的LayoutParams在后面也会有提及)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
上面是我们在Activity加载xml布局的方法,通过setContentView进行加载。而继续追踪setContentView可以看到该方法是在AppCompatActivity的方法。
public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
...
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
...
}
在AppCompatActivity,依旧没有处理布局而是交给了AppCompatDelegate,而AppCompatDelegate只是个抽象方法,具体实现在AppCompatDelegateImpl中。
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
从上面我们可以看出布局文件实际上是通过LayoutInflater加载的,而在它的方法inflate中,则是具体的解析xml的过程的地方。而inflate方法最终会调用下面这个重载方法进行设置LayoutParams。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
...
ViewGroup.LayoutParams params = null;
...
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
temp.setLayoutParams(params);
}
...
if (root != null && attachToRoot) {
root.addView(temp, params);
}
...
}
绕了一大圈最后发现是在LayoutInflater中通过调用root.generateLayoutParams(attrs)创建的,并通过addView添加进父View中。也就是说,子View本身所携带的LayoutParams是由父View根据子View的布局属性来生成的。 而generateLayoutParams方法则是定义在ViewGroup中,追踪这么多其实就是想引出generateLayoutParams而已。
在了解这些方法之前,还需要对LayoutParams有所了解。在ViewGroup中,通过静态内部类定义了两个LayoutParams。一个就是LayoutParams而另一个则是继承LayoutParams的MarginLayoutParams,后者就是在前者的基础属性之上又封装了margin属性等。
public static class LayoutParams {
public static final int FILL_PARENT = -1;
public static final int MATCH_PARENT = -1;
public static final int WRAP_CONTENT = -2;
public int width;
public int height;
...
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();
}
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
width = a.getLayoutDimension(widthAttr, "layout_width");
height = a.getLayoutDimension(heightAttr, "layout_height");
}
...
}
public static class MarginLayoutParams extends ViewGroup.LayoutParams {
public int leftMargin;
public int topMargin;
public int rightMargin;
public int bottomMargin;
private int startMargin = DEFAULT_MARGIN_RELATIVE;
private int endMargin = DEFAULT_MARGIN_RELATIVE;
public static final int DEFAULT_MARGIN_RELATIVE = Integer.MIN_VALUE;
...
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);
// 先获取margin
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;
// 未设置margin则获取marginRight等
} 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);
}
...
}
...
a.recycle();
}
...
}
上面就是LayoutParams和MarginLayoutParams的定义,其中只列出了部分,但是我们可以看出它其实就是用来存放一些布局属性的。并且,对应布局文件的那个构造方法会读取相应的属性。在LayoutParams中,只获取了宽高属性。而在MarginLayoutParams中则获取了所有的margin属性。
并且,获取margin属性的时候会有覆盖。根据上面的构造方法可以看出,若是设置了layout_margin则会设置上下左右的margin都是该值,并且不再读取其他margin属性。若是没有设置,则进一步读取layout_marginVertical和layout_marginHorizontal。若是设置了,则会覆盖上下margin和左右margin,若是都没有设置才会去读取layout_marginLeft等属性。
那么接下来继续看generateLayoutParams方法。
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
从LayoutInflater中我们知道LayoutParams是通过generateLayoutParams方法生成的,当attachToRoot为false并且root非空时就直接将param设置到View中,否则的话通过root.addView(temp, params)添加进生成的View和param添加进父View。
这里也能看出来,当inflate一个布局的时候,若是未传进root父布局,则会丢失本身所携带的属性参数。
那么继续看addView方法。
@Override
public void addView(View child, LayoutParams params) {
addView(child, -1, params);
}
public void addView(View child, int index, LayoutParams params) {
...
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
...
if (!checkLayoutParams(params)) {
params = generateLayoutParams(params);
}
...
if (preventRequestLayout) {
child.mLayoutParams = params;
} else {
child.setLayoutParams(params);
}
...
}
addView一路调用到了addViewInner,然后调用方法checkLayoutParams进行判断Params是否满足条件,不满足的话则根据已有的params重新生成一个LayoutParams,然后才会调用setLayoutParams进行设置Params。
// 检查params是否满足我们的要求,满足则返回true
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p != null;
}
// 根据已有的Params生成LayoutParams
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return p;
}
但是在ViewGroup中,判断param是否满足条件的方法中只是简答的判断params是否为空,而重新生成param的方法更是什么也没做,而是简单粗暴地再将原param再返回来,因此当我们自定义ViewGroup的时候,需要重写这两个方法,以满足通过addView动态添加子View时未传递params的情况。
分析到这里,我们可以知道在View添加到ViewGroup中的时候,布局参数只是ViewGroup.LayoutParams,因此,若是我们不进行处理的话,在使用measureChildWithMargins方法进行测量子View的时候,将会在强转为MarginLayoutParams的时候抛出异常。因此,自定义ViewGroup若要使用margin属性,则必须重写generateLayoutParams(attr)方法,让其返回MarginLayoutParams对象。(对应于通过布局文件声明的ViewGroup)
上述讨论的是在xml布局中给自定义ViewGroup添加子View并生成LayoutParams的过程,还有另一种情况,我们并不是通过布局文件给我们自定义的ViewGroup添加子View的,而是直接调用构造方法生成的子View然后调用VIewGroup的addView添加进去的。而这里addView 有多个重载方法,对应不同的情况,但是所有这些情况都是针对于是否含有LayoutParams的。
// 1
public void addView(View child) {
addView(child, -1);
}
//2
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
// 若是添加的子View没有LayoutParams,则构建一个默认的params
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
//3
public void addView(View child, LayoutParams params) {
addView(child, -1, params);
}
//4
public void addView(View child, int width, int height) {
// 带宽高参数的方法,构建一个默认的LayoutParams然后修改宽高
final LayoutParams params = generateDefaultLayoutParams();
params.width = width;
params.height = height;
addView(child, -1, params);
}
//5
public void addView(View child, int index, LayoutParams params) {
...
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
从上面我们看到,一共有5种添加子View的方法,其中我们常用的是1,3,4这三种方法。而这些的方法实际上的区别就是是否有LayoutParams参数,当不存在的时候则会通过generateDefaultLayoutParams方法构建一个默认的LayoutParams。并且这几种方法到最后都是调用了方法5,也就是通过xml布局生成params调用的方法,而方法5我们上面已经说过了。
// 构建默认的LayoutParams
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
从generateDefaultLayoutParams我们可以看到默认情况下生成的LayoutParams宽高的模式都是wrap_content。前面提过,inflate布局时未设置父View,则会丢失最外层的布局属性,而没有外层属性的时候,则会直接生成默认的params,宽高都是wrap_content。
那么总结一下,这一趟下来我们接触了3个generateLayoutParams的重载方法,1个checkLayoutParams方法。这四个方法是涉及到LayoutParams的。
// 方法1
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {}
// 方法2
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {}
// 方法3
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {}
// 方法4
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {}
那么总结一下这四个方法的关系,首先判断子View是否有LayoutParams,没有的话则通过方法1生成默认的LayoutParams,然后方法4判断这个LayoutParams是否满足我们的要求,不满足的话通过方法2重新生成我们需要的LayoutParams,最后给子View添加params。方法3是使用xml方式添加子View生成LayoutParams,添加进子View的时候也要经历上面的步骤。
知道了它们的关系,那么我们也就知道了在自定义ViewGroup时如何处理LayoutParams了。也就是需要重写这四个方法,让其返回的LayoutParams是我们需要的。通常若是没有别的需求我们应该给子View添加MarginLayoutParams,这样就可以使用measureChildWithMargins方法来测量子View了。若是有其他需求的话,我们应该自定义LayoutParams,然后在重写的方法中构建我们自定义的LayoutParams返回。同样的,我们自定义的LayoutParams应该继承MarginLayoutParams,原因与上面一样。
总结
若是想要响应子View的某些属性,如margin,gravity等,则在自定义ViewGroup的时候应当做如下操作。
1,根据需求决定是否需要定义LayoutParams
1.1,需求即是否需要响应子View除了宽高margin以外的属性
1.2,定义LayoutParams时应当继承ViewGroup.LayoutParams或者MarginLayoutParams
2,重写generateDefaultLayoutParams生成我们需要的默认的LayoutParams
3,重写checkLayoutParams判断是否是我们所需的LayoutParams
4,重写两个generateLayoutParams方法,生成对应的LayoutParams
相关文章目录:
从源码学习自定义View(一):Measure过程
从源码学习自定义View(二):LayoutParams
从源码学习自定义View(三):Layout和Draw过程