view的绘制 是学习自定义View的第一步,一直感觉挺难的,其实确实挺难的 哈哈哈哈,
打算将自己学习到的写下来,记录一下,以后也好查看和回忆 ,
虽说是原创,但是自己也是通过别人的博客或者书籍学习到的,所以有冒犯的地方,可以不留情面的告诉我,
View的绘制呢分三步 Measure(测量view的宽高)Layout(确定View的位置) Draw(绘制View)
还有一点,android的view分为两类,View 和 Viewgroup, 两种view的绘制步骤是一样的 但是实现不一样
这三步呢 也分别对应着三个方法,下面一一来分析一下
测量
在分析测量的时候 我们需要先了解一个类 MeasureSpec,MeasureSpec是View的一个内部类,我们看一下代码
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public @interface MeasureSpecMode {}
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {//这个方法比较重点,父控件计算子View的宽高后是通过这个方法给MeasureSpec赋值的
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);
StringBuilder sb = new StringBuilder("MeasureSpec: ");
if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");
sb.append(size);
return sb.toString();
}
}
代码量也没有很多,说白了 这个类有一个32位的int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(测量大小),
SpecMode测量模式呢 有分为三种
UNSPECIFIDE unSpecifide 未指定的 view想多大就是多大 父容器不指定大小 一般用不着 都是系统用
EXACTLY exactly 精准的 对应于xml里的match_parent和精准值(100dp) 父容器测出子View的SpecSize
AT_MOST at_most 对应于xml里的wrap_content ,view的SpecSize(大小)是父容器指定的 ,并且子View不能大于这个值
每一个view都有对应的一个MersureSpec ,通过makeMeasureSpec来设置,通过getMode/getSize来获取模式和大小
----------------------------------------------------分割线--------------------------------------------------------------------
先说View的measure
//view类 省略了不必要的代码
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
在这一步 首先measure是final类型的 所以不能重写,接下来计算了View 宽和高的 MesureSepc ,并传给了onMeasure方法,
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
一步步分析
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
什么意思呢 ?得到宽的size,就是说 如果没有背景 就用mMinWidth;有背景 对比一下找到mMinWidth和背景的minimumwidth()之间较大的那个值
public static int getDefaultSize(int size, int measureSpec) {
int result = size;//在getsuggestedMinmimWidth 中得到的size
int specMode = MeasureSpec.getMode(measureSpec);//
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
//如果mode是不指定大小,那就用背景大小和mMinWidth中比较小的那个size 上面说了 不指定大小模式 一般不使用 所以一般不会用到这个size
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST://如果在xml里设置为 wrap-content
case MeasureSpec.EXACTLY://如果在xml中设置为固定的值 (100dp)或者 match_parent
result = specSize; //以上情况下 都会使用原本的specSize
break;
}
return result;
}
根据宽的SpecMode 得到宽具体值,
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;//将测量好的宽赋值给View的常量
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
这两步把上面计算好的宽和高 存了起来
下面看一下Viewgroup,viewGroup是实现View的,本身是没有measure和OnMeasure方法的,是为了让子类去实现的
但是ViewGroup有measureChildren()
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);
}
}
}
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);
}
//根据父控件的测量规格和大小, padding , 和父控件的宽/高 ,得到子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);
}
可以看出,在遍measurechildren里历子View ,在measureChile里 计算出子View的大小 ,并调用子view的measure方法,
我们也可以看一下linearLayout的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) {
//代码很多 不知道你有没有耐心看 反正我是没有,但是也可以看出来,在viewgroup的实现类中 也是遍历子view 并调用子view的measure方法的
if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
for (int i = 0; i < count; i++) {
child.measure(
MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(largestChildHeight,
MeasureSpec.EXACTLY));
}
}
}
}
}
----------------------------------------------------分割线--------------------------------------------------------------------
测量完成后 就该确定位置了,
还是先看View的layout
public void layout(int l, int t, int r, int b) {
//暂时并不明白啥意思,但是可以看出 在测量完成之后 的 layout方法里 ,还是可能会走测量方法
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);
//省略不必要的代码
}
}
重点看setFrame方法,(setOpticalFrame最终也是调用了setFrame方法)
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (DBG) {
Log.d("View", this + " View.setFrame(" + left + "," + top + ","
+ right + "," + bottom + ")");
}
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;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);//重点看着,在这一步 是为view设置了四个顶点坐标
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
}
return changed;
}
也就是说 view在layout里执行了setframe方法,在setFrame里 已经确定了view的四个坐标
然后才执行了onMeasure方法
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
我们看到onLayout方法是一个空方法 其实是为了让子类去实现的
在看一些viewGroup的layout方法
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
可以看出,在viewGroup的layout中 先执行了父类的layout,也就是view的layout,同时onLayout也是空实现,那么我们看一下linearLayout的onLayout
@Override
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++) {
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
可以看出,viewGroup的实现类,他自己的父类已经确定好了他的位置,然后他根据自己的需求 在遍历子view 并确定了子view
的位置
View的实现类,基本没有重写onLayout;或者就是重写后也没有实际的作用,因为他们的位置 已经在父控件给确定了
----------------------------------------------------分割线--------------------------------------------------------------------
测量完了 也确定好位置了,那么就开始绘画了,draw闪亮登场
老规矩 从view的draw入手
public void draw(Canvas canvas) {
/*
* 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 如果需要的话 绘制背景
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;
}
// Step 2, save the canvas' layers 保存画布图层
//省略第2步一系列代码
// Step 3, draw the content //绘制view
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children 绘制子view
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
//如果需要的话 绘制渐变效果并恢复图层
//省略第5步一系列代码
// Step 6, draw decorations (foreground, scrollbars) 绘制装饰 (前景,滚动条)
onDrawForeground(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
}
可以看见 draw的时候 有一系列的绘制,我们重点关注 第3步和第4步,来看看
protected void onDraw(Canvas canvas) {
}
protected void dispatchDraw(Canvas canvas) {
}
又都是空实现,根据以往经验 得知肯定又是让子view重写呢,因为view他自己也不知道该画啥啊
来看一下viewGruop,ViewGroup没有重新onDraw方法 因为他也不知道自己画啥,但是他实现了dispatch方法
protected void dispatchDraw(Canvas canvas) {
for (int i = 0; i < childrenCount; i++) {
more |= drawChild(canvas, transientChild, drawingTime);
}
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
可以看出 也是遍历子view,让子view调用自身的draw方法去绘制自己,
举个栗子,还是看linearLayout的onDraw方法
@Override
protected void onDraw(Canvas canvas) {
if (mDivider == null) {
return;
}
if (mOrientation == VERTICAL) {
drawDividersVertical(canvas);
} else {
drawDividersHorizontal(canvas);
}
}
void drawDividersVertical(Canvas canvas) {
final int count = getVirtualChildCount();
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
if (hasDividerBeforeChildAt(i)) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int top = child.getTop() - lp.topMargin - mDividerHeight;
drawHorizontalDivider(canvas, top);
}
}
}
if (hasDividerBeforeChildAt(count)) {
final View child = getLastNonGoneChild();
int bottom = 0;
if (child == null) {
bottom = getHeight() - getPaddingBottom() - mDividerHeight;
} else {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
bottom = child.getBottom() + lp.bottomMargin;
}
drawHorizontalDivider(canvas, bottom);
}
}
void drawHorizontalDivider(Canvas canvas, int top) {
mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,
getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);
mDivider.draw(canvas);
}
根据自身设置的方向去绘画自己
好了 完了