注册

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
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

1 个评论

都是精华啊

要回复文章请先登录注册