系列文章:
一、简析Window、Activity、DecorView以及ViewRoot关系
一、android坐标系
android坐标系和数学坐标系不同,所以要单独记录一下。
1、屏幕坐标系
在Android中,将屏幕的左上角的顶点作为Android坐标系的原点,这个原点向右是X轴正方向,原点向下是Y轴正方向。触控事件中,使用MotionEvent.getRawX()和MotionEvent.getRawY()方法获取的坐标就是以这个坐标系下的坐标值。
2、视图坐标系
在日常开发中我们接触最对的就是视图坐标系了,视图坐标系描述的是子控件在父控件中相对位置。视图坐标系是以控件的父控件的左上角为坐标原点,从原点出发水平向右为x轴正方向,垂直向下为y轴正方向。
1)、View提供下面方法,通过它们可以获得View到其父控件(ViewGroup)的距离:
- getTop():获取View自身顶边到其父布局顶边的距离
- getLeft():获取View自身左边到其父布局左边的距离
- getRight():获取View自身右边到其父布局左边的距离
- getBottom():获取View自身底边到其父布局顶边的距离
- getX():返回值为getLeft()+getTranslationX(),当setTranslationX()时getLeft()不变,getX()变
- getY():返回值为getTop()+getTranslationY(),当setTranslationY()时getTop()不变,getY()变
2)、MotionEvent 提供下面方法,可以获取触摸点的坐标
我们看上图那个深蓝色的点,假设就是我们触摸的点,我们知道无论是View还是ViewGroup,最终的点击事件都会由onTouchEvent(MotionEvent event)方法来处理,MotionEvent也提供了各种获取焦点坐标的方法:
- getX():获取点击事件距离控件左边的距离(注意不是相对于父控件距离,是相对于自己左边的距离),即视图坐标
- getY():获取点击事件距离控件顶边的距离(注意不是相对于父控件距离,是相对于自己顶边的距离),即视图坐标
- getRawX():获取点击事件距离整个屏幕左边距离,即绝对坐标
- getRawY():获取点击事件距离整个屏幕顶边的的距离,即绝对坐标
注意:
View中的getX()、getY()方法只是与MotionEvent中的getX()、getY()方法只是重名而已,并不是一个。
二、MeasureSpec类
1 、简介
MeasureSpec其实就是尺寸和模式通过各种位运算计算出的一个整型值。高2位表示测量模式,低30位表示尺寸
2、MeasureSpec类3种测量模式
1)、UNSPECIFIED(0)
不限定。意思就是,子View想要多大,我就可以给你多大,你放心大胆 的measure吧,不用管其他的。也不用管我传递给你的尺寸值。(其实Android高版本中推荐,只要是这个模式,尺寸设置为0)很少使用,只有在ScrollView和NestedScrollView中看到。
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
上面是NestedScrollView的measureChildWithMargins方法,childHeightMeasureSpec 就是使用UNSPECIFIED。因此在NestedScrollView中使用RecyclerView时会导致RecyclerView加载所有item数据,RecyclerView的复用就失效了。
2)、EXACTLY(1<<30)
精确的。父容器已经测量出View需要的宽高,View直接使用父容器给出的宽高即可。match_parent和设置具体的宽高就是EXACTLY
3)、AT_MOST(2<<30)
最多的。父容器指定一个可用大小,View的大小不能大于这个值,具体大小看具体View的实现。wrap_content就是AT_MOST。wrap_content和match_parent传入的尺寸大小是一样的,所以子View在wrap_content时必须根据View中的内容测试自己的需要的宽高,然后调用setMeasuredDimension(width, height)设置。否则效果就和match_parent一样了
3、提供方法
1)、获取模式
MeasureSpec.getMode(widthMeasureSpec);
2)、获取大小
MeasureSpec.getSize(widthMeasureSpec);
3)、尺寸和模式转化为MeasureSpec
MeasureSpec.makeMeasureSpec(size, mode);
三、如何确定View的MeasureSpec?
子View的MeasureSpec由子View的LayoutParams(包含子View的宽高)和父容器的MeasureSpec共同决定。(子view不是设置多大就多大,比如父容器大小小于子View大小。)
ViewGroup测试子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()就是计算子View的MeasureSpec。代码如下:
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);
}
spec表示父View的MeasureSpec,padding表示父View中已经使用的大小,childDimension表示子View的layoutParam中指定的宽或者高。上面代码大致逻辑是根据父View的测量模式和子View的layoutParam中宽或高(wrap_contgent、match_parent以及确定值三种情况)确定MeasureSpec。具体规则如下表:
parentSize是指父控件的剩余大小。
四、View的工作过程
View的工作过程是指measure(测量)、layout(布局)、draw(绘制)三大过程。measure确定View的测量宽高,layout确定View的最终宽高和四个顶点的位置,draw将View绘制在屏幕上。注意:measure确定的是测量宽高,layout确定最终宽高,但几乎所有情况下测量宽高和最终宽高相等,getMeasureHeight是测量高度,getHeight是最终高度。
1、measure过程
setMeasureDimension方法是最终设置View宽高的方法,只有调用了这个方法才把宽高设置到View。
1)、View的测量过程
measure过程由View的measure方法完成,measure方法中会调用onMeasure方法,所以最终测量在onMeasure中完成。onMeasure如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
上面的宽高是通过getDefaultSize获取的,getDefaultSize()方法代码如下:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
UNSPECIFIED的情况可以不考虑。AT_MOST和EXACTLY都是使用父View传入的测量尺寸。因此直接继承View的自定义控件需要重写onMeasue方法,wrap_content时根据View特点自己计算宽高,match_parent和具体值时使用父View传入的测量尺寸。
2)、ViewGroup的测量过程
ViewGroup继承View,没有重写measure()和onMeasure。自定义ViewGroup时要重写onMeasue方法,wrap_content时根据布局特点自己计算宽高,match_parent和具体值时使用父View传入的测量尺寸。除了完成自己的measure,还需要完成子View的measure,在onMeasure中要调用measureChildren。ViewGroup中提供了measureChildren方法测量子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) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
即循环遍历所有子View调用measureChild测量,measureChild代码如下:
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是计算子View的MeasureSpec,child.measure()会调用到View的onMeasure()
2、layout过程
layout过程是ViewGroup确定子View的位置。因此View没有layout过程,View的位置是在ViewGroup中确定的。layout方法确定自己的位置,然后会去调用onLayout确定子View的位置。ViewGroup是一个抽象类,onLayout是其唯一的抽象方法,因此自定义ViewGroup时必须实现onLayout。
layout方法如下:
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
大致流程如下:
1)、通过setFrame方法设定View四个顶点的位置,View在父View中的位置就确定了
2)、调用onLayout,父容器确定子View的位置。具体如何实现和ViewGroup的布局特性有关
3、draw过程
最终绘制过程是在View中,因此直接继承View的自定义View时要实现onDraw,自定义ViewGroup不用实现onDraw。ViewGroup中已经实现了dispatchDraw,ViewGroup在draw方法中调用dispatchDraw绘制子View。
ViewGroup调用draw方法绘制子View,draw方法先绘制自己,然后绘制子View。draw方法的源码如下:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
......
}
大致流程如下:
1)、绘制背景--drawBackground(canvas);
2)、绘制自己的内容--onDraw(canvas);
3)、绘制子View--dispatchDraw(canvas);
4)、绘制装饰foreground or scrollbars--onDrawForeground(canvas);
通过dispatchDraw方法一步一步绘制子View,dispatchDraw在View中是一个空实现,具体实现在ViewGroup中。代码如下:
protected void dispatchDraw(Canvas canvas) {
......
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
......
}
通过drawChild绘制子View
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
总结:
1、自定义ViewGroup和View要重写onMeasue,wrap_content时根据自己特点计算宽高,match_parent和具体值时使用父View传入的测量尺寸。ViewGroup还要在onMeasure中调用measureChildren测量子View的尺寸。
2、ViewGroup要重写onLayout,通过View.layout设置子View的位置。View中不需要重写onLayout。
3、ViewGroup中不需要重写onDraw,直接继承View的需要重写onDraw绘制界面。
五、怎么在Activity中获取View的宽高
在onCreate、onStart、onResume中都无法正确得到某个View的宽高,因为Activity的生命周期和View的measure不是同步执行的。
1、onWindowFocusChanged
onWindowFocusChanged会被调用多次,Activity的窗口得到或者失去焦点时都会调用该方法,即Activity暂停或退出和Activity继续执行或者新建都会调用该方法。因此最好判断得到焦点时获取View的大小,没有焦点获取没有意义。
public void onWindowFocusChanged(boolean hasFocus) {
Log.d(TAG, "onWindowFocusChanged>>" + hasFocus);
if(hasFocus) {
// 获取View的宽高
Log.d(TAG, "onWindowFocusChanged flowLayout>>width" + flowLayout.getWidth() + ", height:" + flowLayout.getHeight()
+ ", measuWidth:" + flowLayout.getMeasuredWidth() + ", measuHeight:" + flowLayout.getMeasuredHeight());
}
}
2、用post将一个runnable放到消息队列的尾部,Looper调用runnable时View已经初始化完成了。
flowLayout.post(new Runnable() {
@Override
public void run() {
// 获取View的宽高
Log.d(TAG, "post flowLayout>>width" + flowLayout.getWidth() + ", height:" + flowLayout.getHeight()
+ ", measuWidth:" + flowLayout.getMeasuredWidth() + ", measuHeight:" + flowLayout.getMeasuredHeight());
}
});
3、ViewTreeObserver
当View树的状态发生改变或者View树内部的View可见性发生改变,onGlobalLayout方法将被回调。但是View状态改变时onGlobalLayout回调多次,所以获取宽高后就调用flowLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
flowLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
flowLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
// 获取View的宽高
Log.d(TAG, "GlobalLayoutListener flowLayout>>width" + flowLayout.getWidth() + ", height:" + flowLayout.getHeight()
+ ", measuWidth:" + flowLayout.getMeasuredWidth() + ", measuHeight:" + flowLayout.getMeasuredHeight());
}
});
4、手动调用view.measure(widthMeasureSpec,heightMeasureSpec)测量后获取
总结:多个View需要获取宽高时建议使用onWindowFocusChanged,一个View时使用ViewTreeObserver
六:怎么在自定义View中获取自己或者子View的宽高?
1、在onSizeChanged方法中获得
这里可以获取自己的测量宽高,也可以获取自己的宽高。但是不能获取子控件的宽高,只能获取子控件的测量宽高,因为还没有调用子View 的layout。下面源码可以验证,setFrame是在onLayout之前调用的,即onSizeChanged在onLayout前调用。
public void layout(int l, int t, int r, int b) {
......
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
......
}
在setFrame 中调用sizeChange
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
......
}
sizeChange中直接嗲用onSizeChanged,具体如下:
private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
rebuildOutline();
}
2、onMeasure
在onMeasure方法的最后获取测量宽高,因为这时自己和子View都完成了测量
七、几个重要方法
1、invalidate:可以触发onDraw方法的调用,必须在主线程调用
2、postInvalidate:可以触发onDraw方法的调用,主线程子线程都可以调用
3、requestLayout: 子View调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制。
4、onFinishInflate: View全部加载完成调用,在自定义ViewGroup时可以获取子View的引用,不能获取子View的宽高。
八、自定义控件支持padding和margin
1、自定义View
不支持padding的问题就是xml中加了padding没有效果,没有其他问题了。但是如果测量没有考虑padding,绘制的时候又考虑了padding就会出错。
自定义View时如果要支持padding的话,需要在onMeasure中wrap_content时考虑padding,在onDraw方法中绘制时处理padding。margin是由父控件实现,所以自定义View不用考虑margin。
1)、onMeasure中wrap_content时处理padding
代码如下。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthSpecMode != MeasureSpec.EXACTLY) {
// wrap_content时
widthMeasureSpec = MeasureSpec.makeMeasureSpec(mDefaultWidth + getPaddingLeft() + getPaddingRight(), MeasureSpec.EXACTLY);
}
if (heightSpecMode != MeasureSpec.EXACTLY) {
// wrap_content时
heightMeasureSpec = MeasureSpec.makeMeasureSpec(mDefaultHeight + getPaddingTop() + getPaddingBottom(), MeasureSpec.EXACTLY);
}
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}
wrap_content计算宽高时要把padding加上,因为padding可以认为是控件的一部分,不加上大小不对。
2)onDraw时处理padding
measureWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
measureHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
mRroundConerRadius = measureHeight * 0.5f;
mCircleButtonRadius = mRroundConerRadius - dp2px(1);
mCircleButtonX = measureWidth - mRroundConerRadius;
mCircleButtonY = getMeasuredHeight() * 0.5f;
left = getPaddingLeft();
top = getPaddingTop();
ondraw时会使用上面的measureWidth和left等参数。
2、自定义ViewGroup
自定义ViewGroup时需要在onMeasure和onLayout方法中处理自身padding和子View的margin。
场景是自定义一个垂直方向的LinearLayout。有下面几种情况:
1)、onMeasure测试时没有考虑margin
测量代码如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 设置为match_parent或者具体值
if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
}
}
上面measureChilren最终会执行ViewGroup中的measureChild,代码如下。
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);
}
measureChild的测量中只计算了padding,没有计算margin。这样会有什么问题呢?
当我们在该控件中添加子控件TextView,宽度是match_parent,TextView中填入达到换行的字数,设置一定的magin。布局文件如下。
<com.study.androidother.CustomView.view.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="text1text1text1text1text1text1text1text1text1text1text1text1text1"
android:layout_marginLeft="20dp"
android:textSize="16dp"/>
</com.study.androidother.CustomView.view.MyLinearLayout>
布局的预览效果如下:
上面红色箭头处就是因为测量时没有计算margin,布局中添加margin后把控件挤到屏幕外面去了,挤出屏幕外的TextView的内容也不能显示了。如果在测量时减去了margin就不会出现上面的效果。
更改测量代码如下。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
int size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
}
}
setMeasuredDimension(widthSize, heightSize);
}
}
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
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);
}
measureChildWithMargins中计算了margin。现在布局文件效果如下。
现在就没有把TextView挤到屏幕外面去,TextView中的所有内容都正常显示出来了。
2)、onMeasure测试时没有考虑padding
测量代码如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
int size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
lp.leftMargin + lp.rightMargin, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
lp.topMargin + lp.bottomMargin, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
setMeasuredDimension(widthSize, heightSize);
}
}
上面代码测量子View时只考虑了margin,没有考虑padding。布局文件预览效果和上面一样。 把TextView控件挤到屏幕外面去了,挤出屏幕外的TextView的内容也不能显示了。
3)、onLayout中怎么考虑padding和margin
自定义一个垂直方向的LinearLayoutde的onLayout代码如下。
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int topPositon = getPaddingTop(); // 考虑padding
int paddingLeft = getPaddingLeft(); // 考虑padding
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 注意:paddingLeft是父控件布局设置的padding。lp.leftMargin是子控件布局设置的。
int childLeft = paddingLeft + lp.leftMargin;
int childTop = topPositon + lp.topMargin;
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
topPositon += lp.topMargin + lp.bottomMargin + child.getMeasuredHeight();
}
}