新來同事在學(xué)習(xí)自定義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:{//如果沒有指定大小邻邮,就設(shè)置為默認(rèn)大小
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中重新設(shè)置了view寬高筒严,但是他神奇的發(fā)現(xiàn)Linerlayout中放入自定view表現(xiàn)達(dá)到預(yù)期丹泉,是一個正方形。
<?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時鸭蛙,寬高分別交換表現(xiàn)不一致摹恨,width為match_parent 時畫出來是矩形,height為match_parent時又是能達(dá)到預(yù)期的正方形娶视。這一下給我問懵了晒哄,我也沒認(rèn)真看過布局的源碼一時半會兒還真覺得很神奇,講不出來為什么肪获。但是覺得挺有意思就決定看看源碼分析分析寝凌。
1.LinearLayout
子view如何布局是父布局的onLayout方法決定的,方法如下:
@Override
//對子view進(jìn)行布局
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);
//默認(rèn)情況下走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 即右邊和底部都是根
據(jù)子組件自己測量的值來的
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 是根據(jù)子組件的測量結(jié)果來布局的较木,所以接下看看測量方法。
既然是長寬表現(xiàn)問題寒锚,那么就直接去看看view的onMeasure方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
根據(jù)方向不同走不同方法劫映,我們就看默認(rèn)的垂直布局的吧!
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 調(diào)用measureChildWithMargins 最終調(diào)用我們自定義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 根據(jù)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的參數(shù)刹前,myView中重寫onMeasure方法 導(dǎo)致寬高相同并且取最小的泳赋,所以最終子view測量出來的width=height=100。
至此了解到LinearLayout表現(xiàn)達(dá)到預(yù)期的原因了:LinearLayout最終布局時是使用的子View自己測量出來的寬高喇喉,子view又重寫了測量方法祖今,導(dǎo)致寬高相等,所以顯示出來是預(yù)期的正方形拣技。
2.RelativeLayout中的表現(xiàn)
然而在RelativeLayout中設(shè)置:layout_width=match_parent height=100dp 顯示結(jié)果卻是一個高度為100dp的矩形千诬,未能達(dá)到預(yù)期的正方形。
參照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);
//設(shè)置子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);
//設(shè)置子viewbottom的方法
if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
offsetVerticalAxis = true;
}
...
setMeasuredDimension(width, height);
}
源碼內(nèi)容很多徐绑,我們忽略掉不重要的部分,主要是獲取父布局的寬高莫辨,測量子view傲茄,給子view設(shè)置左右位置方便繪制,再次測量子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的參數(shù)為(1920,1080)盘榨,根據(jù)子view重寫方法可以得到測量結(jié)果:(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并且設(shè)置子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);
}
根據(jù)getChildMeasureSpec方法得到寬繼續(xù)為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的參數(shù)為(1920,100),最終計算出來的子view的寬高為100,100
設(shè)置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中為何表現(xiàn)為矩形了:
由于測量方法不同查乒,第一次測量出來寬高為父布局的寬高的最小值,所以導(dǎo)致 right為父布局的寬萍歉,第二次測量高度為子view高度侣颂,所以取最小值 測量處理的結(jié)果為子view的高100档桃,最終導(dǎo)致繪制出來一個矩形枪孩。
同理當(dāng)子view的寬高交換以后,第一次測量出來的width為子view的寬度100藻肄,第二次雖然子view的高為matchParent蔑舞,但是子view的寬為100,總體測量去小值嘹屯,導(dǎo)致最終繪制出來的圖形為100*100的正方形攻询。