新来同事在学习自定义view的时候,参照书上的例子自定义了一个view:
MyView.java
private int getMySize(int defaultSize, int measureSpec){
int mySize = defaultSize;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode){
case MeasureSpec.UNSPECIFIED:{//如果没有指定大小,就设置为默认大小
mySize = defaultSize;
break;
}
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY: {//如果测量模式是最大取值为size
//我们将大小取最大值,你也可以取其他值
mySize = size;
break;
}//如果是固定大小,那就不要去改变它
}
return mySize;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMySize(100,widthMeasureSpec);
int height = getMySize(100,heightMeasureSpec);
if (width<height){
height = width;
}else {
width = height;
}
setMeasuredDimension(width,height);
Log.d("TAG", "onMeasure: "+width+":"+height);
}
他在重写的onMeasure中重新设置了view宽高,但是他神奇的发现Linerlayout中放入自定view表现达到预期,是一个正方形。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.my.textanimator.view.MyView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@android:color/holo_green_light"
app:layout_constraintBottom_toBottomOf="parent"/>
</LinearLayout>
但是一旦将父布局修改为RelativeLayout时,宽高分别交换表现不一致,width为match_parent 时画出来是矩形,height为match_parent时又是能达到预期的正方形。这一下给我问懵了,我也没认真看过布局的源码一时半会儿还真觉得很神奇,讲不出来为什么。但是觉得挺有意思就决定看看源码分析分析。
1.LinearLayout
子view如何布局是父布局的onLayout方法决定的,方法如下:
@Override
//对子view进行布局
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
//垂直布局
void layoutVertical(int left, int top, int right, int bottom) {
...
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
//拿到子view测量到的宽高
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
//子组件的Params
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
//默认情况下走left
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
//layout时 right=left+childWidth,bottom=top+childHeight 即右边和底部都是根
据子组件自己测量的值来的
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
由上源码分析可知,具体layout 是根据子组件的测量结果来布局的,所以接下看看测量方法。
既然是长宽表现问题,那么就直接去看看view的onMeasure方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
根据方向不同走不同方法,我们就看默认的垂直布局的吧!
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
//省略部分不重要代码
....
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;//false
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
...
} else {
//useExcessSpace=false
if (useExcessSpace) {
// The heightMode is either UNSPECIFIED or AT_MOST, and
// this child is only laid out using excess space. Measure
// using WRAP_CONTENT so that we can find out the view's
// optimal height. We'll restore the original height of 0
// after measurement.
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
//测量子view 调用measureChildWithMargins 最终调用我们自定义viewonMeasure方法
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
}
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//lp.width=MATCH_PARENT 根据getChildMeasureSpec方法得到传输给自定义view的宽为父布局
的宽度
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
//lp.height=100dp 同理通过getChildMeasureSpec方法得到传递给子view的高为子view的高
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//传给子view width=parentWidth(1080)height=100
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
由上知道传给myview的参数,myView中重写onMeasure方法 导致宽高相同并且取最小的,所以最终子view测量出来的width=height=100。
至此了解到LinearLayout表现达到预期的原因了:LinearLayout最终布局时是使用的子View自己测量出来的宽高,子view又重写了测量方法,导致宽高相等,所以显示出来是预期的正方形。
2.RelativeLayout中的表现
然而在RelativeLayout中设置:layout_width=match_parent height=100dp 显示结果却是一个高度为100dp的矩形,未能达到预期的正方形。
参照LinearLayout的分析,我们直接看RelativeLayout的onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
//父布局RelativeLayout宽高都MATCH_PARENT所以 为EXACTLY
if (widthMode != MeasureSpec.UNSPECIFIED) {
//父布局宽1920
myWidth = widthSize;
}
if (heightMode != MeasureSpec.UNSPECIFIED) {
//父布局高1080
myHeight = heightSize;
}
if (widthMode == MeasureSpec.EXACTLY) {
width = myWidth;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = myHeight;
}
...
for (int i = 0; i < count; i++) {
View child = views[i];
if (child.getVisibility() != GONE) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
int[] rules = params.getRules(layoutDirection);
applyHorizontalSizeRules(params, myWidth, rules);
//测量子view的方法
measureChildHorizontal(child, params, myWidth, myHeight);
//设置子view right的方法
if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
offsetHorizontalAxis = true;
}
}
}
...
for (int i = 0; i < count; i++) {
final View child = views[i];
if (child.getVisibility() != GONE) {
final LayoutParams params = (LayoutParams) child.getLayoutParams();
applyVerticalSizeRules(params, myHeight, child.getBaseline());
//测量子view的方法
measureChild(child, params, myWidth, myHeight);
//设置子viewbottom的方法
if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
offsetVerticalAxis = true;
}
...
setMeasuredDimension(width, height);
}
源码内容很多,我们忽略掉不重要的部分,主要是获取父布局的宽高,测量子view,给子view设置左右位置方便绘制,再次测量子view的宽高。
第一次测量子view宽高的方法measureChildHorizontal
private void measureChildHorizontal(
View child, LayoutParams params, int myWidth, int myHeight) {
//子view宽的测量要求
final int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft, params.mRight,
params.width, params.leftMargin, params.rightMargin, mPaddingLeft, mPaddingRight,
myWidth);
final int childHeightMeasureSpec;
//myHeight=1080
if (myHeight < 0 && !mAllowBrokenMeasureSpecs) {
...
} else {
final int maxHeight;
if (mMeasureVerticalWithPaddingMargin) {
maxHeight = Math.max(0, myHeight - mPaddingTop - mPaddingBottom
- params.topMargin - params.bottomMargin);
} else {
maxHeight = Math.max(0, myHeight);
}
final int heightMode;
if (params.height == LayoutParams.MATCH_PARENT) {
heightMode = MeasureSpec.EXACTLY;
} else {
heightMode = MeasureSpec.AT_MOST;
}
//得到最终的高度为1080
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, heightMode);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
private int getChildMeasureSpec(int childStart, int childEnd,
int childSize, int startMargin, int endMargin, int startPadding,
int endPadding, int mySize) {
//childSize=MATCH_PARENT=-1 mySize=1920
int childSpecMode = 0;
int childSpecSize = 0;
final boolean isUnspecified = mySize < 0;//>0
if (isUnspecified && !mAllowBrokenMeasureSpecs) {
...
}
int tempStart = childStart;
int tempEnd = childEnd;
//true
if (tempStart == VALUE_NOT_SET) {
// 0 0
tempStart = startPadding + startMargin;
}
//true
if (tempEnd == VALUE_NOT_SET) {
// 1920 0 0
tempEnd = mySize - endPadding - endMargin;
}
// Figure out maximum size available to this view
final int maxAvailable = tempEnd - tempStart;//maxAvailable=1920
if (childStart != VALUE_NOT_SET && childEnd != VALUE_NOT_SET) {
...
} else {
//childSize=-1
if (childSize >= 0) {
...
} else if (childSize == LayoutParams.MATCH_PARENT) {//true
// Child wanted to be as big as possible. Give all available
// space.
childSpecMode = isUnspecified ? MeasureSpec.UNSPECIFIED :
MeasureSpec.EXACTLY;
//childSpecSize=1920
childSpecSize = Math.max(0, maxAvailable);
} else if (childSize == LayoutParams.WRAP_CONTENT) {
// Child wants to wrap content. Use AT_MOST to communicate
// available space if we know our max size.
if (maxAvailable >= 0) {
// We have a maximum size in this dimension.
childSpecMode = MeasureSpec.AT_MOST;
childSpecSize = maxAvailable;
} else {
// We can grow in this dimension. Child can be as big as it
// wants.
childSpecMode = MeasureSpec.UNSPECIFIED;
childSpecSize = 0;
}
}
}
由上分析可知传递给子view的参数为(1920,1080),根据子view重写方法可以得到测量结果:(1080,1080)。
接下来是确定子view的左右位置positionChildHorizontal()
private boolean positionChildHorizontal(View child, LayoutParams params, int myWidth,
boolean wrapContent) {
final int layoutDirection = getLayoutDirection();
int[] rules = params.getRules(layoutDirection);
//false
if (params.mLeft == VALUE_NOT_SET && params.mRight != VALUE_NOT_SET) {
...
} else if (params.mLeft != VALUE_NOT_SET && params.mRight == VALUE_NOT_SET) {
...
} else if (params.mLeft == VALUE_NOT_SET && params.mRight == VALUE_NOT_SET) {
// Both left and right vary
//false
if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) {
if (!wrapContent) {
centerHorizontal(child, params, myWidth);
} else {
positionAtEdge(child, params, myWidth);
}
return true;
} else {
// This is the default case. For RTL we start from the right and for LTR we start
// from the left. This will give LEFT/TOP for LTR and RIGHT/TOP for RTL.
positionAtEdge(child, params, myWidth);
}
}
return rules[ALIGN_PARENT_END] != 0;
}
private void positionAtEdge(View child, LayoutParams params, int myWidth) {
if (isLayoutRtl()) {
params.mRight = myWidth - mPaddingRight - params.rightMargin;
params.mLeft = params.mRight - child.getMeasuredWidth();
} else {//true
//0 0 0
params.mLeft = mPaddingLeft + params.leftMargin;
// 0 1080
params.mRight = params.mLeft + child.getMeasuredWidth();
}
}
由上可以得到子view的左右位置分别为0,1080。
再次测量子view并且设置子view的top和bottom:
//新的测量子view的方法
private void measureChild(View child, LayoutParams params, int myWidth, int myHeight) {
//(0,1080,MATCH_PARENT,0,0,0,0,1920)
int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft,
params.mRight, params.width,
params.leftMargin, params.rightMargin,
mPaddingLeft, mPaddingRight,
myWidth);
//(0,0,100dp,0,0,0,0,1080)
int childHeightMeasureSpec = getChildMeasureSpec(params.mTop,
params.mBottom, params.height,
params.topMargin, params.bottomMargin,
mPaddingTop, mPaddingBottom,
myHeight);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
根据getChildMeasureSpec方法得到宽继续为1920,但是高度由于params.height=100dp,所以childsize>0
//childSize=100
if (childSize >= 0) {
// Child wanted an exact size. Give as much as possible.
childSpecMode = MeasureSpec.EXACTLY;
//maxAvilable=1080
if (maxAvailable >= 0) {
// We have a maximum size in this dimension.
//childSpecSize=100
childSpecSize = Math.min(maxAvailable, childSize);
} else {
// We can grow in this dimension.
childSpecSize = childSize;
}
}
最终可知传递给子view的参数为(1920,100),最终计算出来的子view的宽高为100,100
设置top和bottom
private boolean positionChildVertical(View child, LayoutParams params, int myHeight,
boolean wrapContent) {
int[] rules = params.getRules();
if (params.mTop == VALUE_NOT_SET && params.mBottom != VALUE_NOT_SET) {
// Bottom is fixed, but top varies
params.mTop = params.mBottom - child.getMeasuredHeight();
} else if (params.mTop != VALUE_NOT_SET && params.mBottom == VALUE_NOT_SET) {
// Top is fixed, but bottom varies
params.mBottom = params.mTop + child.getMeasuredHeight();
} else if (params.mTop == VALUE_NOT_SET && params.mBottom == VALUE_NOT_SET) {
// Both top and bottom vary
if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) {
if (!wrapContent) {
centerVertical(child, params, myHeight);
} else {
params.mTop = mPaddingTop + params.topMargin;
params.mBottom = params.mTop + child.getMeasuredHeight();
}
return true;
} else {
// 0 0
params.mTop = mPaddingTop + params.topMargin;
// 0 100
params.mBottom = params.mTop + child.getMeasuredHeight();
}
}
return rules[ALIGN_PARENT_BOTTOM] != 0;
}
由上可知子view的top为0,bottom为100。
至此我们就明白RelativeLayout中为何表现为矩形了:
由于测量方法不同,第一次测量出来宽高为父布局的宽高的最小值,所以导致 right为父布局的宽,第二次测量高度为子view高度,所以取最小值 测量处理的结果为子view的高100,最终导致绘制出来一个矩形。
同理当子view的宽高交换以后,第一次测量出来的width为子view的宽度100,第二次虽然子view的高为matchParent,但是子view的宽为100,总体测量去小值,导致最终绘制出来的图形为100*100的正方形。