本文是(4.1.37.1)深入理解setContentView过程和View绘制过程的补充篇,上文中有大量相关概念的描述,建议认真阅读
基础
MeasureSpec对象包含一个size和一个mode,其中mode可以取以下三个数值之一:
- UNSPECIFIED,1073741824 [0x40000000],未加规定的,表示没有给子view添加任何规定。
- EXACTLY,0 [0x0],精确的,表示父view为子view确定精确的尺寸。
- AT_MOST,-2147483648 [0x80000000],子view可以在指定的尺寸内尽量大
onMeasure 中的参数是 父布局为当前控件(布局或view) 所生成好的 计划的宽高属性,在上一篇中我们从父布局角度,演示了父布局为子控件生成计划宽高的过程。现在我们把视角转换一下,从当前控件角度,看看当前传入的计划宽高是如何被父布局生成的:
- 如果当前控件配置的android:width,android:height 是一个具体的值( android:width = 500 dp),那么传入的widthMeasureSpec,heightMeasureSpec对应的就是EXACTLY + value
如果 当前控件配置的android:width,android:height 是 MATCH_PARENT, 当前控件应该是父布局的准确大小EXACTLY ,但是由于父控件宽高不确定性,那么传入的值就分以下几种情况:
- 如果 父容器宽高是一个确定的值 EXACTLY (具体数值 or match_parent)
也就是说,父容器的大小是被确定的,所以子元素大小也是可以被确定的
那么widthMeasureSpec,heightMeasureSpec直接将父容器大小作为参考EXACTLY + parentvalue - 如果 父容器宽高是一个限制值的值 AT_MOST (wrap_content)
也就是说,父容器的大小是受到限制值的限制,所以子元素的大小也应该受到父容器的限制
那么widthMeasureSpec,heightMeasureSpec将父容器大小作为参考 AT_MOST + parentvalue - 如果 父容器尺寸大小未受限制 并且未定义
也就是说,父容器的大小不受限制,而对子元素来说也可以是任意大小,所以不指定也不限制子元素的大小
那么widthMeasureSpec,heightMeasureSpec不指定大小 UNSPECIFIED + 0
- 如果 父容器宽高是一个确定的值 EXACTLY (具体数值 or match_parent)
如果 自身 width || height 是 WRAP_CONTENT ,当前控件应该是<=父布局的准确大小At_Most,但是由于父控件宽高不确定性,那么传入的值就分以下几种情况:
- 如果 父容器宽高是一个确定的值 EXACTLY (具体数值 or match_parent)
也就是说,子元素的大小包裹了其内容后不能超过父容器
那么widthMeasureSpec,heightMeasureSpec将父容器大小作为参考 AT_MOST + parentvalue - 如果 父容器宽高是一个限制值的值 AT_MOST (wrap_content)
也就是说,父容器的大小是受到限制值的限制,所以子元素的大小也应该受到父容器的限制
那么widthMeasureSpec,heightMeasureSpec将父容器大小作为参考 AT_MOST + parentvalue - 如果 父容器尺寸大小未受限制 并且未定义
父容器的大小不受限制而对子元素来说也可以是任意大小所以不指定也不限制子元素的大小
返回不指定大小 UNSPECIFIED + 0
- 如果 父容器宽高是一个确定的值 EXACTLY (具体数值 or match_parent)
画布大小 == view的测量后的大小
- 画布的原点就是 view的左上原点
- 在ViewGroup的onMeasure中调用Child.measure()的时候,传入的参数应考虑到:子控件布局的margin,父布局的计划宽高,已被其他子view占用的计划宽高,当前布局的padding
二、示例
2.1 未实现padding的View
public class CustomViewIconView7_12_1 extends View{
private Bitmap mBitmap;// 位图
private TextPaint mPaint;// 绘制文本的画笔
private String mStr;// 绘制的文本
private float mTextSize;// 画笔的文本尺寸
private enum Ratio {
WIDTH, HEIGHT
}
public CustomViewIconView7_12_1(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
public void init(Context context, AttributeSet attrs) {
// 计算参数
calArgs(context);
// 初始化
init();
}
private void calArgs(Context context) {
// 获取屏幕宽
int sreenW = MeasureUtil.getScreenSize((Activity) context)[0];
// 计算文本尺寸
mTextSize = sreenW * 1 / 10F;
}
private void init() {
if (null == mBitmap) {
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.logo);
}
if (null == mStr || mStr.trim().length() == 0) {
mStr = "AigeStudio";
}
mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);
mPaint.setColor(Color.LTGRAY);
mPaint.setTextSize(mTextSize);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setTypeface(Typeface.DEFAULT_BOLD);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 设置测量后的尺寸
setMeasuredDimension(getMeasureSize(widthMeasureSpec, Ratio.WIDTH), getMeasureSize(heightMeasureSpec, Ratio.HEIGHT));
}
private int getMeasureSize(int measureSpec, Ratio ratio) {
int result = 0;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.EXACTLY:// EXACTLY时直接赋值
result = size;
break;
default:// 默认情况下将UNSPECIFIED和AT_MOST一并处理
if (ratio == Ratio.WIDTH) {
float textWidth = mPaint.measureText(mStr);
result = ((int) (textWidth >= mBitmap.getWidth() ? textWidth : mBitmap.getWidth())) + getPaddingLeft() + getPaddingRight();
} else if (ratio == Ratio.HEIGHT) {
result = ((int) ((mPaint.descent() - mPaint.ascent()) * 2 + mBitmap.getHeight())) + getPaddingTop() + getPaddingBottom();
}
/*
* AT_MOST时判断size和result的大小取小值
*/
if (mode == MeasureSpec.AT_MOST) {
result = Math.min(result, size);
}
break;
}
return result;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.GRAY);
/*
* 绘制
* 参数就不做单独处理了因为只会Draw一次不会频繁调用
*/
canvas.drawBitmap(mBitmap, getWidth() / 2 - mBitmap.getWidth() / 2, getHeight() / 2 - mBitmap.getHeight() / 2, null);
canvas.drawText(mStr, getWidth() / 2, mBitmap.getHeight() + getHeight() / 2 - mBitmap.getHeight() / 2 - mPaint.ascent(), mPaint);
}
}
2.2 考虑margin与padding的ViewGroup
- 调用ViewGroup#measureChildWithMargins(View child,int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed)
- 其中帮我们实现了 子View的计划宽高设定 = (当前padding + 子View的marging + 被其他子控件占用的宽高) 与 (当前布局的父布局给当前布局的计划宽高)与(子控件配置的android:widht,height属性)的分析与取舍
- 也可以自定调用 child.measure()但也应该考虑以上元素
public class CustomViewCustomLayout7_12_2 extends ViewGroup {
private Bitmap mBitmap;// 位图
private TextPaint mPaint;// 绘制文本的画笔
private String mStr;// 绘制的文本
private float mTextSize;// 画笔的文本尺寸
private enum Ratio {
WIDTH, HEIGHT
}
public CustomViewCustomLayout7_12_2(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
public void init(Context context, AttributeSet attrs) {
// 计算参数
calArgs(context);
// 初始化
init();
}
private void calArgs(Context context) {
// 获取屏幕宽
int sreenW = MeasureUtil.getScreenSize((Activity) context)[0];
// 计算文本尺寸
mTextSize = sreenW * 1 / 10F;
}
void init() {
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 考虑 padding和margin
// 声明临时变量存储父容器的期望值
int parentDesireWidth = 0;
int parentDesireHeight = 0;
if (getChildCount() > 0) {
// 那么遍历子元素并对其进行测量
for (int i = 0; i < getChildCount(); i++) {
// 获取子元素
View child = getChildAt(i);
// 获取子元素的布局参数
CustomLayoutParams clp = (CustomLayoutParams) child.getLayoutParams();
// 【2】测量子元素并考虑子元素外边距 效果与 measureChildren
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 计算父容器的期望值 = 子元素实际宽高 + 子元素的外边距
parentDesireWidth += child.getMeasuredWidth() + clp.leftMargin + clp.rightMargin;
parentDesireHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;
}
// 考虑父容器的内边距
parentDesireWidth += getPaddingLeft() + getPaddingRight();
parentDesireHeight += getPaddingTop() + getPaddingBottom();
// 尝试比较建议最小值和期望值的大小并取大值
parentDesireWidth = Math.max(parentDesireWidth, getSuggestedMinimumWidth());
parentDesireHeight = Math.max(parentDesireHeight, getSuggestedMinimumHeight());
}
// 【2】设置最终测量值O
setMeasuredDimension(resolveSize(parentDesireWidth, widthMeasureSpec), resolveSize(parentDesireHeight, heightMeasureSpec));
}
private int getMeasureSize(int measureSpec, Ratio ratio) {
// 声明临时变量保存测量值
int result = 0;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.EXACTLY:// EXACTLY时直接赋值
result = size;
break;
default:// 默认情况下将UNSPECIFIED和AT_MOST一并处理
if (ratio == Ratio.WIDTH) {
float textWidth = mPaint.measureText(mStr);
result = ((int) (textWidth >= mBitmap.getWidth() ? textWidth : mBitmap.getWidth())) + getPaddingLeft() + getPaddingRight();
} else if (ratio == Ratio.HEIGHT) {
result = ((int) ((mPaint.descent() - mPaint.ascent()) * 2 + mBitmap.getHeight())) + getPaddingTop() + getPaddingBottom();
}
/*
* AT_MOST时判断size和result的大小取小值
*/
if (mode == MeasureSpec.AT_MOST) {
result = Math.min(result, size);
}
break;
}
return result;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.GREEN);
}
/**
* onLayoutLayout的目的是为了确定子元素 在父容器中的位置,那么这个步骤理应该由父容器来决定而不是子元素,因此,我们可以猜到View中的onLayout方法应该是一个空实现
* @param changed 是否与上一次位置不同,其具体值在View的layout方法中通过setFrame等方法确定
* @param l 四个参数则表示当前View与父容器的相对距离
* @param t 四个参数则表示当前View与父容器的相对距离
* @param r 四个参数则表示当前View与父容器的相对距离
* @param b 四个参数则表示当前View与父容器的相对距离
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 获取父容器内边距
int parentPaddingLeft = getPaddingLeft();
int parentPaddingTop = getPaddingTop();
/*
* 如果有子元素
*/
if (getChildCount() > 0) {
// 声明一个临时变量存储高度倍增值
int mutilHeight = 0;
// 那么遍历子元素并对其进行定位布局
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
//child.layout(parentPaddingLeft, mutilHeight + parentPaddingTop , getMeasuredWidth() + parentPaddingLeft, getMeasuredHeight() + mutilHeight + parentPaddingTop);
// 对比和上边的区别 这行代码 是每个子view 的layout大小都按照 当前布局的底端为底端,并没有按照子view实际需要的
//child.layout(parentPaddingLeft, mutilHeight + parentPaddingTop , child.getMeasuredWidth() + parentPaddingLeft, child.getMeasuredHeight() + mutilHeight + parentPaddingTop);
// 通知子元素进行布局
// 此时考虑父容器内边距和子元素外边距的影响
CustomLayoutParams clp = (CustomLayoutParams) child.getLayoutParams();
child.layout(parentPaddingLeft + clp.leftMargin, mutilHeight + parentPaddingTop + clp.topMargin, child.getMeasuredWidth() + parentPaddingLeft + clp.leftMargin, child.getMeasuredHeight() + mutilHeight + parentPaddingTop + clp.topMargin);
// 改变高度倍增值
mutilHeight += child.getMeasuredHeight();
}
}
}
public static class CustomLayoutParams extends MarginLayoutParams {
public CustomLayoutParams(MarginLayoutParams source) {
super(source);
}
public CustomLayoutParams(android.view.ViewGroup.LayoutParams source) {
super(source);
}
public CustomLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public CustomLayoutParams(int width, int height) {
super(width, height);
}
}
/**
* 生成默认的布局参数
*/
@Override
protected CustomLayoutParams generateDefaultLayoutParams() {
return new CustomLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
/**
* 生成布局参数
* 将布局参数包装成我们的
*/
@Override
protected android.view.ViewGroup.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams p) {
return new CustomLayoutParams(p);
}
/**
* 生成布局参数
* 从属性配置中生成我们的布局参数
*/
@Override
public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new CustomLayoutParams(getContext(), attrs);
}
/**
* 检查当前布局参数是否是我们定义的类型这在code声明布局参数时常常用到
*/
@Override
protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {
return p instanceof CustomLayoutParams;
}
}
其中调用了ViewGroup#measureChildWithMargins:
/**
* child: 当前布局的子控件
* parentWidthMeasureSpec:当前布局的父布局给当前布局的计划宽高
* widthUsed:已经被其他孩子使用的宽高
*/
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//childWidthMeasureSpec ,childHeightMeasureSpec 就是当前布局为子布局混合的计划宽高
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec); // 1
}
4.2 自定义实现9*9的子控件要求宽高和计划宽高的取舍算法
public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context) {
super(context);
}
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
if(getChildCount() > 0) {
View view = getChildAt(0);
LayoutParams layoutParams = view.getLayoutParams();
int childWidthSpec, childHeightSpec;
int childWidthSize, childWidthMode, childHeightSize, childHeightMode;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
if(layoutParams.width == LayoutParams.MATCH_PARENT){
childWidthSize = widthSize;
childWidthMode = MeasureSpec.EXACTLY;
}else if(layoutParams.width == LayoutParams.WRAP_CONTENT){
childWidthSize = widthSize;
childWidthMode = MeasureSpec.AT_MOST;
}else {
childWidthSize = layoutParams.width;
if(childWidthSize > widthSize){//这里的不同操作带来不同的效果
childWidthSize = widthSize;
}
childWidthMode = MeasureSpec.EXACTLY;
}
} else if (widthMode == MeasureSpec.AT_MOST) {
if(layoutParams.width == LayoutParams.MATCH_PARENT ){
childWidthSize = widthSize;
childWidthMode = MeasureSpec.AT_MOST;
}else if(layoutParams.width == LayoutParams.WRAP_CONTENT){
childWidthSize = widthSize;
childWidthMode = MeasureSpec.AT_MOST;
} else {
childWidthSize = layoutParams.width;
if(childWidthSize > widthSize){//这里的不同操作带来不同的效果
childWidthSize = widthSize;
}
childWidthMode = MeasureSpec.EXACTLY;
}
} else {//父布局没被指定多大,那么就以子布局为准吧
if(layoutParams.width == LayoutParams.MATCH_PARENT){
childWidthSize = 0;
childWidthMode = MeasureSpec.UNSPECIFIED;
}else if(layoutParams.width == LayoutParams.WRAP_CONTENT){
childWidthSize = 0;
childWidthMode = MeasureSpec.UNSPECIFIED;
}else {//既然你要那么大,就给你那么大吧
childWidthSize = layoutParams.width;
childWidthMode = MeasureSpec.EXACTLY;
}
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
if(layoutParams.height == LayoutParams.MATCH_PARENT){
childHeightSize = heightSize;
childHeightMode = MeasureSpec.EXACTLY;
}else if(layoutParams.height == LayoutParams.WRAP_CONTENT){
childHeightSize = heightSize;
childHeightMode = MeasureSpec.AT_MOST;
}else {
childHeightSize = layoutParams.height;
if(childHeightSize > heightSize){//这里的不同操作带来不同的效果
childHeightSize = heightSize;
}
childHeightMode = MeasureSpec.EXACTLY;
}
} else if (heightMode == MeasureSpec.AT_MOST) {
if(layoutParams.height == LayoutParams.MATCH_PARENT ){
childHeightSize = heightSize;
childHeightMode = MeasureSpec.AT_MOST;
}else if(layoutParams.height == LayoutParams.WRAP_CONTENT){
childHeightSize = heightSize;
childHeightMode = MeasureSpec.AT_MOST;
} else {
childHeightSize = layoutParams.height;
if(childHeightSize > heightSize){//这里的不同操作带来不同的效果
childHeightSize = heightSize;
}
childHeightMode = MeasureSpec.EXACTLY;
}
} else {//父布局没被指定多大,那么就以子布局为准吧
if(layoutParams.height == LayoutParams.MATCH_PARENT){
childHeightSize = 0;
childHeightMode = MeasureSpec.UNSPECIFIED;
}else if(layoutParams.height == LayoutParams.WRAP_CONTENT){
childHeightSize = 0;
childHeightMode = MeasureSpec.UNSPECIFIED;
}else {//既然你要那么大,就给你那么大吧
childHeightSize = layoutParams.height;
childHeightMode = MeasureSpec.EXACTLY;
}
}
childWidthSpec = MeasureSpec.makeMeasureSpec(childWidthSize, childWidthMode);
childHeightSpec = MeasureSpec.makeMeasureSpec(childHeightSize, childHeightMode);
view.measure(childWidthSpec, childHeightSpec);
if(widthMode != MeasureSpec.EXACTLY){
width = view.getMeasuredWidth();
}
if(heightMode != MeasureSpec.EXACTLY){
height = view.getMeasuredHeight();
}
setMeasuredDimension(width, height);
}else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.e("test", "MyViewGroup.onLayout left = " + l + " top = " + t + " right = " + r + " bottom = " + b);
View view = getChildAt(0);
int d = 200;
d = -d;
// d = t;
// view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
l = -100 + new Random().nextInt(100);
t = 0;
r = view.getMeasuredWidth() - 100;
b = view.getMeasuredHeight() - 100;
view.layout(l, t, r, b);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.e("test", "MyViewGroup.onDraw canvas = " + canvas);
}
}