上一篇文章引出了performTraversals,在内部分别调用了performMeasure,performLayout,performDraw三个方法。这三个方法分别有调用了mView.measure,mView.layout,mView.draw(canvas)三个对应的方法,因此我们找打View中对应的方法。
一、onMeasure
如下measure方法,省略了其他的代码:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
可以看出measure方法是被final修饰,是不可重写了,但是onMeasure方法却是public的,里面传递了widthMeasureSpec,heightMeasureSpec两个参数,这两个参数来自于父容器,所有宽高的测量也受父容器的影响。下面找到父容器ViewGroup看父容器和子View的layoutparams如何影响View的测量。
1、MeasureSpect之三种模式
MeasureSpect由于int是32位的,用高两位表示mode,低30位表示size。共有三种情况,取值分别为MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, MeasureSpec.AT_MOST。
- MeasureSpec.EXACTLY:精确模式
- MeasureSpec.AT_MOST:至多模式,有多大给多大,但不能超过父容器,相当于wrap_content
- MeasureSpec.UNSPECIFIED:未指定尺寸模式,相当于match_parent
2、三种模式如何受父容器影响
子View的MeasureSpec由父View的MeasureSpec和子View本身的LayoutPramas共同决定,在ViewGroup的getChildMeasureSpec方法中实现:
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) {
//(1)父容器精确模式
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
//子View设置精确模式
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
//子View设置精确模式
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
//子View设置至多模式
resultMode = MeasureSpec.AT_MOST;
}
break;
//(2)父容器至多模式
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
//子View设置精确模式
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
//子View设置至多模式
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
//子View设置至多模式
resultMode = MeasureSpec.AT_MOST;
}
break;
//(3)父容器不确定模式
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
resultSize = childDimension;
//子View设置精确模式
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
//子View设置不确定模式
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
//子View设置不确定模式
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
总结:
(1) 父类MeasureSpec.EXACTLY,子类:
a.具体尺寸,或者LayoutParams == MATCH_PARENT,则为EXACTLY
b.LayoutParams == WRAP_CONTENT,则为AT_MOST
(2) 父类MeasureSpec.AT_MOST,子类:
a.具体尺寸,则为EXACTLY
b.LayoutParams == WRAP_CONTENT或者LayoutParams == MATCH_PARENT,则为AT_MOST
(3)父类MeasureSpec.UNSPECIFIED,子类:
a.具体尺寸,则为EXACTLY
b.LayoutParams == WRAP_CONTENT或者LayoutParams == MATCH_PARENT,则为UNSPECIFIED
从上面的总结上可以看出来,当字View尺寸固定时,不管父容器是什么模式,子类都是精确模式。
在measure方法中,有一个非常重要的方法setMeasuredDimension,当尺寸宽高变化的时候都需要调用这个方法。总结measure流程如下:
View: performTraversals ——> performMeasure ——> mView.measure ——> onMeasure ——> setMeasuredDimension
ViewGroup: ViewGroup.measureChild —— 遍历子View——> View.onMeasure ——> setMeasuredDimension
二、onLayout
View中:空实现:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
ViewGroup中是抽象的:
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
以RelativeLayout为例,看看实现:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = getChildCount();
//遍历View
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
RelativeLayout.LayoutParams st =
(RelativeLayout.LayoutParams) child.getLayoutParams();
//设置子View的测量
child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
}
}
}
三、onDraw
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);
drawAutofilledHighlight(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);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
- 绘制背景
- 需要的话,保存画布
- 绘制内容,空实现onDraw,一般需要重写
- 绘制子View,View空实现dispatchDraw,一般需要重写
- 如果需要的话绘制锯齿边缘
- 绘制滚动条
wiew空实现dispatchDraw,ViewGroup需要实现,内部调用:ViewGroup ——> drawChild ——> child.draw,最后还是调用View的Draw。
四、自定义控件继承自View
需要重写measure方法进行测量,设梁实际尺寸之后需要设置setMeasuredDimension,宽高的测量可你仿照源码:
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;
}
作为setMeasuredDimension的参数进行传递。view的自定义比较简单,这里不做单独分析。详情可以参考之前的文章:
自定义EditText:https://blog.csdn.net/yoonerloop/article/details/61616065
自定义指示器指示器Indictor:https://blog.csdn.net/yoonerloop/article/details/51940612
四、自定义控件继承自ViewGroup
在onMeasure方法中需要测量子控件大小,还需要测量自身大小,然后调用setMeasuredDimension设置自身的宽高,在测量子view是可用使用ViewGrou提供的测量子view的方法,也可以自定义测量规则,最后在onLayout方法中对子view进行布局。
1、计算child的测量规则
getChildMeasureSpec
2、将计算出来的测量规则传递给子View进行测量
child.measure
3、测量完成后获取子View的尺寸
child.getChildMeasureSize
4、VIewGroup根据自身Padding和子View的尺寸设置尺寸
setSize
5、尺寸变化后调用setMeasuredDimension
setMeasuredDimension(width, height);
下一篇文章将自定义一个简单的ViewGroup进行实战,实现FlowLayout效果。