关于自定义View 自定义ViewGroup
场景一:自定义View,使用父类的 super.onMeasure
这种场景实际上是使用了 super.onMeasure 先测量一遍,让系统自己先填充 mMeasuredWidth,mMeasuredHeight 成员变量,之后就可以通过
getMeasuredWidth(); getMeasuredHeight(); 直接获取测量之后的宽高值。最后再调用 setMeasuredDimension 重新将计算出来的新的宽高填充 mMeasuredWidth,mMeasuredHeight 成员变量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = getMeasuredWidth();
int measureHeight = getMeasuredHeight();
if (measureWidth > measureHeight) {
measureWidth = measureHeight;
} else {
measureHeight = measureWidth;
}
setMeasuredDimension(measureWidth, measureHeight);
}
场景二:自定义View,【不】使用父类的 super.onMeasure
这种场景需要自行根据View的测量类型,算出真实的宽高,并将结果填充至 mMeasuredWidth,mMeasuredHeight 成员变量。
特别说明:widthMeasureSpec、heightMeasureSpec 是一个32位的数值,前2位指代测量模式,后30位指代大小
xml 中 layout_xxx 的属性就是父布局对子Veiw的属性声明
MeasureSpec.UNSPECIFIED:父布局对子View的大小没有限制,子View想多大都可以
MeasureSpec. AT_MOST:父布局限制了子View的大小上限,子View最大不得超过父布局的上限
MeasureSpec. EXACTLY:父布局指定了子View的大小,子View只能使用这个固定值
实际返回的测量结果,需要根据具体业务进行计算。
最后依然要调用 setMeasuredDimension 把测量出来的结果填充至 mMeasuredWidth,mMeasuredHeight 成员变量。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int userSize = 200;
int measureWidth = customResolveSize(userSize, widthMeasureSpec);
int measureHeight = customResolveSize(userSize, heightMeasureSpec);
setMeasuredDimension(measureWidth, measureHeight);
}
private static int customResolveSize(int size, int measureSpec) {
int measureMode = MeasureSpec.getMode(measureSpec);
int measureSize = MeasureSpec.getSize(measureSpec);
int realSize = 0;
switch (measureMode) {
case MeasureSpec.UNSPECIFIED: // 父view对子view的大小没有限制,直接返回子view的size
realSize = size;
break;
case MeasureSpec.AT_MOST: // 父view限制了子view的大小上限,子view的大小不得超过父view指定的值
if (size >= measureSize) {
realSize = measureSize;
} else {
realSize = size;
}
break;
case MeasureSpec.EXACTLY: // 父view指定了子view的大小,直接返回父view指定的值
realSize = measureSize;
break;
default:
realSize = size;
break;
}
return realSize;
}
场景三:自定义ViewGroup
自定义ViewGroup比较复杂,难点在测量过程,例子代码,具体返回的测量宽高值,根据实际业务计算。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//触发所有子View的onMeasure函数去测量宽高
measureChildren(widthMeasureSpec, heightMeasureSpec);
//MeasureSpec封装了父View传递给子View的布局要求
int wMode = MeasureSpec.getMode(widthMeasureSpec);
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int hMode = MeasureSpec.getMode(heightMeasureSpec);
int hSize = MeasureSpec.getSize(heightMeasureSpec);
switch (wMode) {
case MeasureSpec.EXACTLY: // 说明这个ViewGroup在父布局中的宽度是一个定值
mWidth = wSize;
break;
case MeasureSpec.AT_MOST: // 说明这个ViewGroup会尽量填满父布局的宽度,但不能超过父布局的宽度
mWidth = wSize;
break;
case MeasureSpec.UNSPECIFIED: // 说明这个ViewGroup的宽度不受父布局的宽度约束,有可能会超过父布局的宽度
break;
}
switch (hMode) {
case MeasureSpec.EXACTLY: // 说明这个ViewGroup在父布局中的高度是一个定值
mHeight = hSize;
break;
case MeasureSpec.AT_MOST: // 说明这个ViewGroup会尽量填满父布局的高度,但不能超过父布局的高度
mHeight = hSize;
break;
case MeasureSpec.UNSPECIFIED: // 说明这个ViewGroup的宽度不受父布局的高度约束,有可能会超过父布局的高度
break;
}
// setMeasuredDimension 的作用是将测量出来的最新宽高值设置到成员变量 mMeasuredWidth,mMeasuredHeight 中,下一阶段
// onLayout 可以获取到经过测量之后的准确宽高值
setMeasuredDimension(mWidth, mHeight);
}
【重点来了】measureChildren,做了什么事情?
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) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
遍历子view,过滤掉Gone的子View,并再次调用 measureChild 方法。通过 getChildMeasureSpec 方法算出子View的 MeasureSpec 值,并调用子View的measure方法,进行子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);
}
蛋疼的来了,getChildMeasureSpec 做了什么?
/**
* 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) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
实际就是根据不同的测量模式,算出真实的 mode、size,并调用 MeasureSpec.makeMeasureSpec 生成 MeasureSpec,并返回。
过程很绕,看英文原著吧。实际使用其实用 measureChildren 让系统自己测量就好了,ViewGroup的实际宽高值根据具体情况计算测量值即可。
下一个阶段是 onLayout,根据左、上、右、下的原则,计算子View在ViewGroup的内部位置,之后使用 child. layout(int l, int t, int r, int b) 方法,将子View进行重新定位。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 对子View进行位置布局
int count = getChildCount();
for (int i = 0; i < count; i++) {
View childView = getChildAt(i);
childView.layout(xx,xx,xx,xx);
}
}
场景四:让ViewGroup支持margin
要让自定义ViewGroup支持 layout_margin 属性,需要重写 generateLayoutParams,generateDefaultLayoutParams
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i = 0; i < count; i++) {
View childView = getChildAt(i);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams marginLayoutParams = (MarginLayoutParams)childView.getLayoutParams();
int childWidth =
childView.getMeasuredWidth() + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin;
int childHeight =
childView.getMeasuredHeight() + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin;
// ........
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
View childView = getChildAt(i);
MarginLayoutParams marginLayoutParams = (MarginLayoutParams)childView.getLayoutParams();
int childWidth =
childView.getMeasuredWidth() + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin;
int childHeight =
childView.getMeasuredHeight() + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin;
// .......
}
}
为什么要重写 generateLayoutParams,generateDefaultLayoutParams ?
/**
* Returns a new set of layout parameters based on the supplied attributes set.
*
* @param attrs the attributes to build the layout parameters from
*
* @return an instance of {@link android.view.ViewGroup.LayoutParams} or one
* of its descendants
*/
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
/**
* Returns a safe set of layout parameters based on the supplied layout params.
* When a ViewGroup is passed a View whose layout params do not pass the test of
* {@link #checkLayoutParams(android.view.ViewGroup.LayoutParams)}, this method
* is invoked. This method should return a new set of layout params suitable for
* this ViewGroup, possibly by copying the appropriate attributes from the
* specified set of layout params.
*
* @param p The layout parameters to convert into a suitable set of layout parameters
* for this ViewGroup.
*
* @return an instance of {@link android.view.ViewGroup.LayoutParams} or one
* of its descendants
*/
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return p;
}
/**
* Returns a set of default layout parameters. These parameters are requested
* when the View passed to {@link #addView(View)} has no layout parameters
* already set. If null is returned, an exception is thrown from addView.
*
* @return a set of default layout parameters or null
*/
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
系统默认的ViewGroup只返回了LayoutParams对象,只能获取到 layout_width,layout_height 属性,获取不到 margin 的属性
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();
}
如果想获取 margin 的属性,则需要返回 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);
.......
}
由于 MarginLayoutParams 是 LayoutParams 的派生类,所以 (MarginLayoutParams) 强转是合法的,不会报错
自定义View、ViewGroup讲完。