学习资料
- Android开发艺术探索
- 鸿洋大神的Android 手把手教您自定义ViewGroup(一)
- 爱哥自定义控件其实很简单7/12
上篇学习了View的测量方法,了解一些Android UI架构图
的知识,这篇记录学习ViewGroup
的测量
1. ViewGroup <p>
A ViewGroup is a special view that can contain other views (called children.) The view group is the base class for layouts and views containers. This class also defines the ViewGroup.LayoutParams class which serves as the base class for layouts parameters.
直译: ViewGroup
是一个可以包含其他子View
特殊的View
。并且是那些子View
或者布局的父容器。而且ViewGroup
定义了ViewGroup.LayoutParams
这个类
ViewGroup
是一个抽象类,内部的子View
可以是一个View
也可以是另一个ViewGroup
例如,在LinearLayout
中,可以加入一个TextView
也可以加入另外一个LinearLayout
ViewGroup的职责
ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属性,都是为用于告诉容器的),我们的宽度(
layout_width
)、高度(layout_height
)、对齐方式(layout_gravity
)等;当然还有margin
等;于是乎,ViewGroup
的职能为:给childView
计算出建议的宽和高和测量模式 ;决定childView
的位置;为什么只是建议的宽和高,而不是直接确定呢,别忘了childView
宽和高可以设置为wrap_content
,这样只有childView
才能计算出自己的宽和高。
View的职责
View的职责,根据测量模式和ViewGroup给出的建议的宽和高,计算出自己的宽和高;同时还有个更重要的职责是:在ViewGroup为其指定的区域内绘制自己的形态。
以上摘抄鸿洋大神的Android 手把手教您自定义ViewGroup(一)
2. 测量方法 <p>
View
的测量大小除了自身还会受父容器的影响。一般这个父容器就是一个ViewGroup
。对于一个ViewGroup
来说,除了完成自身的测量外,还要遍历内部的childView
的测量方法,各个childView
再递归执行这个步骤。
ViewGroup
源代码内并没有重写onMeasure()
方法,而是提供了几个测量相关的方法。
原因也比较容易理解,由于ViewGroup
是一个抽象类,有不同的子类childView
,有不同的布局属性,测量的细节不同。例如LinearLayput
和RelativeLayout
。每个继承之ViewGroup
的Layout
,各自根据自身的布局属性来重写onMeasure()
方法
2.1 测量的过程 <p>
ViewGroup
的测量过程主要用到了三个方法
- measureChildren() ,遍历所有的
childView
- getChildMeasureSpec(),确定测量规格
- measureChild(),调用测量规格。这个方法内,根据
2
确定好的测量规格,childView
调用了measure()
方法,而measure()
内部调用的方法就有onMeasure()
2.1.1 measureChildren() 遍历所有的childView
<p>
源码:
/**
* Ask all of the children of this view to measure themselves, taking into account both the MeasureSpec requirements for this view and its padding.
*
* We skip children that are in the GONE state The heavy liftingis done in getChildMeasureSpec.
*
* @param widthMeasureSpec The width requirements for this view
* @param heightMeasureSpec The height requirements for this view
*/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) { //进行遍历
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {//确定childview是否可见
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
方法内主要就是遍历了所有的chlidView
,判断每个childView
的visibility
值,确定当前的这个childView
可见,然后调用了measureChild(child, widthMeasureSpec, heightMeasureSpec)
方法
2.1.2 measureChild(),调用测量规格 <p>
把这个方法放在getChildMeasureSpec()确定测量规格
之前,是因为measureChild()
内部调用了getChildMeasureSpec()
源码:
/**
* 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) {
// 获取childView的布局参数
final LayoutParams lp = child.getLayoutParams();
//将ViewGroup的测量规格,上下和左右的边距还有childView自身的宽高传入getChildMeasureSpec方法计算最终测量规格
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom, lp.height);
//调用childView的measure(),measure()方法内就是回调`onMeasure()`方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
View
的measure()
测量方法调用过程,在上篇View的测量方法学习过程中,只是用文字简单概括了几句,并没有记录学习源码方法的调用过程,可以去爱哥的自定义控件其实很简单7/12进行补充学习 : )
2.1.3 getChildMeasureSpec(),确定childview的测量规格 <p>
源码:
/**
* Does the hard part of measureChildren: figuring out the MeasureSpec to pass to a particular child. This method figures out the right MeasureSpec for one dimension (height or width) of one child view.
*
* The goal is to combine information from our MeasureSpec with the LayoutParams of the child to get the best possible results. For example, if the this view knows its size (because its MeasureSpec has a mode of EXACTLY), and the child has indicated in its LayoutParams that it wants to be the same size as the parent, the parent should ask the child to layout given an exact size.
*
* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and margins, if applicable
* @param childDimension How big the child wants to be in the current dimension
* @return a MeasureSpec integer for the child
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//ViewGroup的测量模式及大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//将ViewGroup的测量大小减去内边距
int size = Math.max(0, specSize - padding);
// 声明临时变量存值
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
case MeasureSpec.EXACTLY://ViewGroup的测量模式为精确模式
//根据childView的布局参数判断
if (childDimension >= 0) {//如果childDimension是一个具体的值
// 将childDimension赋予resultSize ,作为结果
resultSize = childDimension;
//将临时resultMode 也设置为精确模式
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {//childView的布局参数为精确模式
//将ViewGroup的大小做为结果
resultSize = size;
//因为ViewGroup的大小是受到限制值的限制所以childView的大小也应该受到父容器的限制
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {//childView的布局参数为最大值模式
//ViewGroup的大小作为结果
resultSize = size;
//将临时resultMode 也设置为最大值模式
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST://ViewGroup的测量模式为精确模式
//根据childView的布局参数判断
if (childDimension >= 0) {//如果childDimension是一个具体的值
// 将childDimension赋予resultSize ,作为结果
resultSize = childDimension;
//将临时resultMode 也设置为精确模式
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {//如果childDimension是精确模式
//因为ViewGroup的大小是受到限制值的限制所以chidlView的大小也应该受到父容器的限制
//ViewGroup的大小作为结果
resultSize = size;
//将临时resultMode 也设置为最大值模式
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {// 如果childDimension是最大值模式
//ViewGroup的大小作为结果
resultSize = size;
//将临时resultMode 也设置为最大值模式
//childView的大小包裹了其内容后不能超过ViewgGroup
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED://ViewGroup尺寸大小未受限制
if (childDimension >= 0) {//如果childDimension是一个具体的值
// 将childDimension赋予resultSize ,作为结果
resultSize = childDimension;
// 将临时resultMode 也设置为精确模式
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {如果childDimension是精确模式
//ViewGroup大小不受限制,对childView来说也可以是任意大小,所以不指定也不限制childView的大小
//对是否总是返回0进行判断 sUseZeroUnspecifiedMeasureSpec受版本影响
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
// 将临时resultMode 也设置为UNSPECIFIED,无限制摸式
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {如果childDimension是最大值
//ViewGroup大小不受限制,对childView来说也可以是任意大小,所以不指定也不限制childView的大小
//sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < M
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
// 将临时resultMode 也设置为UNSPECIFIED,无限制摸式
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//返回封装后的测量规格
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
至此我们可以看到一个
View
的大小由其父容器的测量规格MeasureSpec
和View
本身的布局参数LayoutParams
共同决定,但是即便如此,最终封装的测量规格也是一个期望值,究竟有多大还是我们调用setMeasuredDimension
方法设置的。上面的代码中有些朋友看了可能会有疑问为什么childDimension >= 0
就表示一个确切值呢?原因很简单,因为在LayoutParams中MATCH_PARENT
和WRAP_CONTENT
均为负数、哈哈!!正是基于这点,Android
巧妙地将实际值和相对的布局参数分离开来。
3. 布局方法 <p>
ViewGorup
是个抽象类,继承ViewGroup
,肯定就有必须要实现的抽象方法,这个抽象方法就是onLayout()
代码:
public class CustomLayout extends ViewGroup {
public CustomLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
经过了onMeasure()
方法后,确定ViewGroup
的位置和childView
宽高后,在ViewGroup
的onLayout()
方法内,遍历ViewGroup
内所有的childView
,并让每个childView
调用View
的layout()
方法,在layout()
方法内,首先会确定每个childView
的顶点的位置,之后又调用childView
的onLayout()
方法
3.1 简单实现CustomLayout <p>
public class CustomLayout extends ViewGroup {
public CustomLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 测量
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int count = getChildCount();
if (count > 0) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
}
/**
* 布局
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = getChildCount();
if (count > 0) {
// 遍历内部的childView
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
}
}
}
}
代码很简单,就是先遍历测量,在遍历布局
布局xml
:
<com.szlk.customview.custom.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="英勇青铜5"
android:textColor="@color/colorAccent"
android:textSize="30sp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="@string/view_group__name" />
</com.szlk.customview.custom.CustomLayout>
虽然TextView
和Button
在CustomLayout
都已经绘制出来,但Button
把TextView
给盖住了。原因很明显,在绘制第2个子控件Button
时,依然从CustomView
的(0,0)
点开始绘制,并没有考虑TextView
的高度
3.2 进行优化修改 <p>
修改需要考虑的就是已经绘制过的childView
的高度
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = getChildCount();
if (count > 0) {
int mHeight = 0;
// 遍历内部的childView
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
child.layout(0, mHeight, child.getMeasuredWidth(), child.getMeasuredHeight()+mHeight);
mHeight += child.getMeasuredHeight();
}
}
}
增加一个临时变量int mHeight = 0
,绘制过TextView
就将高度加起来,就等于绘制Button
时,开始绘制的点便是(0,mHeight)
,于是,Button
也就在TextView
下方
有点像一个超级简单的Vertical
的LinearLayout
而Horizontal
的,就可以考虑child.layout()
时,改变开始绘制时,x
轴的坐标点
3.3 getMeasuredWidth()和getWidth() <p>
在onLayout()
方法中
child.layout(0, 0, child.getMeasuredWidth(),child.getMeasuredHeight())
使用的是child.getMeasuredWidth()
,而不是child.getWidth()
child.getWidth()
源码:
/**
* Return the width of the your view.
*
* @return The width of your view, in pixels.
*/
@ViewDebug.ExportedProperty(category = "layout")
public final int getWidth() {
return mRight - mLeft;
}
其中mRight
和mleft
值,是在onLayout()
方法后拿到的,在onLayout()
方法中,返回的是0
child.getMeasuredWidth()
源码:
/**
* Like {@link #getMeasuredWidthAndState()}, but only returns the raw width component (that is the result is masked by {@link #MEASURED_SIZE_MASK}).
*
* @return The raw measured width of this view.
*/
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
之后便是追着mMeasuredWidth
这个值走,经过一系列的测量方法后,最终来到onMeasure()
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(...);
}
而mMeasuredWidth
则在onMeasure()
方法后便可以到了,拿到的时间比getWidth()
要早
使用场景:
- getMeasuredWidth():onLayout()方法内
- getWidth():除了
onLayout()
方法,其他之外
使用场景绝大部分情况下都是符合的,这两个方法拿到的值,绝大多数时候也是一样的
可以看看Android开发之getMeasuredWidth和getWidth区别从源码分析
4.考虑Padding,Margins <p>
有了上篇onMeasure()
经验,知道Padding
和Margins
,也需要优化处理的
4.1 Padding
在xml
文件中加入padding
之后
修改代码:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = getChildCount();
final int parentPaddingLeft = getPaddingLeft();
final int parentPaddingTop = getPaddingTop();
if (count > 0) {
int mHeight = 0;
// 遍历内部的childView
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
final int left = parentPaddingLeft;
final int top = mHeight + parentPaddingTop;
final int right = child.getMeasuredWidth() + parentPaddingLeft;
final int bottom = child.getMeasuredHeight() + mHeight + parentPaddingTop;
child.layout(left, top, right, bottom);
mHeight += child.getMeasuredHeight();
}
}
}
主要就是考虑getPaddingLeft()
和getPaddingTop()
这样也只是做了最简单的优化,一旦Padding
大到了一定程度,还是会吃掉内部的childView
4.2 Margins <p>
CustomLayout
内加Margins
有效,可内部的childView
加了却无效。上篇提到过,View
的Margins
是封装在LayoutParams
后由ViewGroup
来处理的
自定义LayoutParams
:
public static class LayoutParams extends MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
并没有做任何设置,还对更多属性进行设置,以后再学习
完整代码:
public class CustomLayout extends ViewGroup {
public CustomLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 测量
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int count = getChildCount();
// 临时ViewGroup大小值
int viewGroupWidth = 0;
int viewGroupHeight = 0;
if (count > 0) {
// 遍历childView
for (int i = 0; i < count; i++) {
// childView
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
//测量childView包含外边距
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 计算父容器的期望值
viewGroupWidth += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
viewGroupHeight += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
// ViewGroup内边距
viewGroupWidth += getPaddingLeft() + getPaddingRight();
viewGroupHeight += getPaddingTop() + getPaddingBottom();
//和建议最小值进行比较
viewGroupWidth = Math.max(viewGroupWidth, getSuggestedMinimumWidth());
viewGroupHeight = Math.max(viewGroupHeight, getSuggestedMinimumHeight());
}
setMeasuredDimension(resolveSize(viewGroupWidth, widthMeasureSpec), resolveSize(viewGroupHeight, heightMeasureSpec));
}
/**
* 布局
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// ViewGroup的内边距
int parentPaddingLeft = getPaddingLeft();
int parentPaddingTop = getPaddingTop();
if (getChildCount() > 0) {
int mHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
//获取 LayoutParams
LayoutParams lp = (LayoutParams) child.getLayoutParams();
//childView的四个顶点
final int left = parentPaddingLeft + lp.leftMargin;
final int top = mHeight + parentPaddingTop + lp.topMargin;
final int right = child.getMeasuredWidth() + parentPaddingLeft + lp.leftMargin;
final int bottom = child.getMeasuredHeight() + mHeight + parentPaddingTop + lp.topMargin;
child.layout(left, top, right, bottom);
// 累加已经绘制的childView的高
mHeight += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
}
}
/**
* 获取布局文件中的布局参数
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new CustomLayout.LayoutParams(getContext(), attrs);
}
/**
* 获取默认的布局参数
*/
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
/**
* 生成自己的布局参数
*/
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
/**
* 检查当前布局参数是否是我们定义的类型
*/
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
/**
* 自定义LayoutParams
*/
public static class LayoutParams extends MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
}
代码基本照搬的爱哥的。。。。
xml
布局文件
<com.szlk.customview.custom.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="50dp"
android:background="@android:color/holo_blue_bright"
android:padding="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:background="@color/colorPrimary"
android:text="英勇青铜5"
android:textColor="@color/colorAccent"
android:textSize="30sp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:text="@string/view_group__name"
android:textAllCaps="false" />
</com.szlk.customview.custom.CustomLayout>
这时,CustomLayout
和内部控件的Margin
都已经支持,但真正以后实际开发,要优化考虑的要比这严谨。这里只是了解学习
5.最后 <p>
重点是理解ViewGroup
的测量过程,理解后,接下来再学习View
的工作流程就会比较容易理解
本人很菜,有错误,请指出
共勉 : )