Android日记之View的绘制流程(二)
Android日记之View的绘制流程(一)
Measure过程
Measure过程要分情况来看,如果只是原始的View,那么通过measure()
方法就完成了测量的过程。如果是一个ViewGroup,除了完成自己的测量过程外,还要遍历去调用子元素的measure()
,下面分两种情况进行讨论。
View的measure过程
View的measure过程就是会调用View的measure()
方法,它是一个final类型的方法,在measure()
方法还会调用View的onMeasure()
方法,我们看这个方法写了什么。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
代码很少,这里的setMeasuredDimension()
方法会设置View的宽和高的测量值,因此我们只要看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;
}
从源码可以看出,逻辑还是很简单的,对于我们来说,只需要看AT_MOST和EXACTLY这两种情况,简单理解,其实getDefaultSize()
返回的大小就是measureSpec中的specSize,而这个specSize就是View测量后的大小,这里多次提到测量后的大小,是因为View最终得大小是在layout阶段确定的,所以这里必须要区分,但几乎所有情况下View的测量大小和最终大小是相等的。
至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize()
的第一个参数size,即宽和高为getSuggestedMinimunWidth()
和getSuggestedMinimunHeight()
的返回值:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
这里就只分析getSuggestedMinimumWidth()
方法的实现,getSuggestedMinimunHeight()
和它的实现原理是一样的,从代码中可以看出,如果View没有设置背景,那么宽度就为mMinWidth,而mMinWidth对应android:Width这个属性所指定的值。如果不指定,那么mMinWidth则默认为0,如果设置了背景,则View的宽度为max(mMinWidth, mBackground.getMinimumWidth()。那mBackground.getMinimumWidth()
是什么呢?
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
可以看出,getMinimumWidth()
返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,否则就返回0。这里说明一下,ShapeDrawable无原始宽和高,BitmapDrawab有原始宽和高。
从getDefaultSize可以看出,View的宽和高由specSize决定。直接继承View的自定义控件需要重写onMeasure()
方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于match_parent。
ViewGroup的measure过程
对于ViewGroup来说,除了完成自己的mesaure过程以外,还会遍历去调用所有子元素的measure()
方法,各个子元素在递归的去执行这个过程,和View不同的是,ViewGroup时候是一个抽象类,因此它没有重写View的onMeasure()
,但是它提供了一个叫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);
}
measureChildren()
会调用measureChild()
方法,在measureChild()
方法里,就是去除子元素的LayoutParams,然后在通过getChildMeasureSpec()
来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure()
方法进行测量。
我们知道,ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure()
方法需要由各个子类去具体实现,比如LinearLayout、RelativeLayout等,ViewGroup不做统一的onMeasure()
实现,是因为不同的ViewGroup子类有不同的布局特性,导致它们的测量细节个不不相同,比如LinearLayout和RelativeLayout这两者的布局特性显然不同,因为无法做统一,以后会专门写一篇文章来讲解每个layout的布局是怎么实现的。
Layout过程
Layout的作用就是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用layout()
方法,在layout()
方法中onLayout()
方法又会被调用。layout过程相比measure过程就简单多了,layout()
方法确定View本身的位置,而onLayout()
方法则会确定所有子元素的位置,先看View的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 listenersCopy =
(ArrayList)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);
}
}
}
final boolean wasLayoutValid = isLayoutValid();
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if (!wasLayoutValid && isFocused()) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
if (canTakeFocus()) {
// We have a robust focus, so parents should no longer be wanting focus.
clearParentsWantFocus();
} else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
// This is a weird case. Most-likely the user, rather than ViewRootImpl, called
// layout. In this case, there's no guarantee that parent layouts will be evaluated
// and thus the safest action is to clear focus here.
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
clearParentsWantFocus();
} else if (!hasParentWantsFocus()) {
// original requestFocus was likely on this view directly, so just clear focus
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
// otherwise, we let parents handle re-assigning focus during their layout passes.
} else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
View focused = findFocus();
if (focused != null) {
// Try to restore focus as close as possible to our starting focus.
if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
// Give up and clear focus once we've reached the top-most parent which wants
// focus.
focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
}
}
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
首先会通过setFrame()
方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop和mBottom这四个值,View的四个顶点一旦确定,那么View在父容器的位置也就确定了。接着就会调用onLayout()
方法,这个方法的用途是父容器确定子元素的位置,和onMeasure()
类似,onLayout()
的具体实现同样和具体的布局有关,所以View和ViewGroup都没有真正实现onLayout()
方法。
Draw过程
Draw过程就简单很多了,作用就是将View会知道屏幕上面。View的绘制过程遵循着如下几步。
(1)background.draw(canvas)
- 绘制背景
(2)onDraw()
- 绘制自己
(3)dispatchDraw()
- 绘制children
(4)onDrawScrollBars()
- 绘制装饰
绘制流程从ViewRootImpl的performDraw()
开始,performDraw()
方法在类 ViewRootImpl内,其核心代码如下:
private void performDraw() {
boolean canUseAsync = draw(fullRedrawNeeded);
}
private boolean draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
...
mView.draw(canvas);
...
}
然后查看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); //这里进行传递的实现!!!
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;
}
......
}
View的绘制过程的传递是通过dispatchDraw()
来实现的,dispatchDraw()
会遍历调用所有子元素的draw()
方法,如此一来draw事件就一层层的传递了下去。这里补充一下View还有一个特殊的方法叫setWillNotDraw()
,代码如下:
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化,默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个标记位。这个标记位对开发的实际意义是:当我们的自定义控件继承与ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位进行后续的优化。当然如果确定ViewGroup要绘制内容的话,就要关闭这个标记位。
作者:居居居居居居x
链接:https://www.jianshu.com/p/f4afebd50a2b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。