您的位置:首页 > 移动开发 > Android开发

Android中View的相关知识(7)

2017-06-20 15:07 316 查看

Android中View的相关知识(7)

在前几章节,我们从源码分析了从窗口系统机制到一个View加载到手机屏幕的过程。我们接着分析View的绘制过程~

我们知道VIew的绘制是由ViewRootImpl.performTraversals();函数具体完成的。那么我们就来看看这个方法:

private void performTraversals(){
...
//省略一些代码
//1.测量
performMeasure();
//2.布局
performLayout();
//3.绘制
performDraw();

}


里面的详细代码很繁琐,这里我们简化,每个View的绘制最终都会走到performTraversals();方法的内部,并依次调用者3个方法。所以咱们就顺着这3个方法进行深入。我们先画个图,加深下印象~



View的绘制:1. performMeasure();

measure的过程就是计算View的宽和高。(每个View的控件的实际宽高都是由父视图和本身视图决定的)我们看看源码是怎么样的:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}


方法内部又是调用了mView.measure();方法:

/*This is called to find out how big a view should be.
*The parent supplies constraint information in  the width and height parameters.
*The actual measurement work of a view is performed in onMeasure(int, int), called by this method.
*Therefore, only onMeasure(int, int) can and must be overridden by subclasses.
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
//省略了一些代码
onMeasure(widthMeasureSpec, heightMeasureSpec);
...

}


我们将注释给出,注释的意思是这个方法的作用就是查明这个View到底有多大,真实的测量工作是在onMeasure();中完成的,因此也只有onMeasure();方法能被子类重写。

measure();方法接受两个参数,widthMeausreSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。

说到这里,我们先来聊聊MeasureSpec:

MeasureSpec指的是View的规格+尺寸;测量的过程中父容器会根据自己的MeasureSpec和子View的LaytouParams转换成对应的MeasureSpec;

来看看MeasureSpec类:

public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
//Measure specification mode: The parent has not imposed any constraint on the child. It can be whatever size it wants.
public static final int UNSPECIFIED = 0 << MODE_SHIFT;

//Measure specification mode: The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be.

public static final int EXACTLY     = 1 << MODE_SHIFT;

//Measure specification mode: The child can be as large as it wants up to the specified size.

public static final int AT_MOST     = 2 << MODE_SHIFT;

/*Creates a measure specification based on the supplied size and mode. The mode must always be one of the following:
*     android.view.View.MeasureSpec.UNSPECIFIED
*     android.view.View.MeasureSpec.EXACTLY
*     android.view.View.MeasureSpec.AT_MOST
*/
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}

//Extracts the mode from the supplied measure specification.
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}

//Extracts the size from the supplied measure specification.
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}

static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(0, UNSPECIFIED);
}
int size = getSize(measureSpec) + 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);
}
//Returns a String representation of the specified measure specification.
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();
}
}


这里我将MeasureSpec类中的几个重要的方法都列了出来。一一进行分析解释:我把英文注释加上,英文好的童鞋,可以自己理解理解,本人英语很烂,


MeasureSpec的值是由specSize和specMode共同组成的,specSize指视图的大小,specMode指视图的规格,specMode一共有3个类型:

1. UNSPECIFIED

表示父视图和子View没有约束关系,开发人员可以将视图按照自己的意愿设置成随意大小,没有限制。(这种情况很少见,不推荐)

2. EXACTLY

表示父视图希望子视图的大小应该是由specSize的值来决定的;即父容器已经知道View精确大小是SpecSize, 或者限制View大小就是SpecSize(系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。)

3. AT_MOST

表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小的去设置这个值,并保证不超过specSize;即父容器指定View最大size是SpecSize, 一般在LayoutParams中是wrap_content或match_parent时使用.(系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。)

整理下可以得到如下的表格:



接下来就是MeasureSpec类中的几个方法,都很好理解:

makeMeasureSpec();创建MeasureSpec的方法;

getMode();获得MeasureSpec的Mode的方法;getSize();获得MeasureSpec的Size的方法;

adjust();调整mode和size达到合法值。并返回合法的MeasureSpec;

toString();把MeasureSpec里面的Mode和Size变成字符串返回,便于打印日志进行查看。

那么,可能有童鞋会好奇performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec);这两个参数是从哪里获得的呢?通常情况下,这两个值是由父视图经过计算传递给子视图的,父视图会在一定的程度上觉得子视图的大小,但是,最外层的根视图的这两个值,又是从哪里来的呢?观察performTraversals()方法可以发现如下代码:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);


getRootMeasureSpec();一看得到的是根视图的MeasureSpec;其中lp.width和lp.height在创建ViewGroup实例的时候就被赋值了,它们都等于MATCH_PARENT。我们继续到了getRootMeasureSpec();方法:

private int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}


可以看到,这里使用了MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpec,当rootDimension参数等于MATCH_PARENT的时候,MeasureSpec的specMode就等于EXACTLY,当rootDimension等于WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST。并且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。

分析完了MeasureSpec这个类,就有了前提的知识,接下来我们继续来看View.onMeasure();方法吧:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}


里面就两个方法setMeasuredDimension();和getDefaultSize(); setMeasuredDimension();方法顾名思义就是存储和设置测量到的宽高。还是会调用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;
}


这里传入的measureSpec是一直从measure()方法中传递过来的。然后调用MeasureSpec.getMode()方法可以解析出specMode,调用MeasureSpec.getSize()方法可以解析出specSize。接下来进行判断,如果specMode等于AT_MOST或EXACTLY就返回specSize,这也是系统默认的行为。之后会在onMeasure()方法中调用setMeasuredDimension()方法来设定测量出的大小,这样一次measure过程就结束了。

一个界面的展示可能涉及多次的measure;因为一个布局一般会包括多个子视图;每个视图都需要measure; 于是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);
}
}
}


可以看到,首先方法遍历布局下的整个视图,然后依次对每个子视图进行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获得子视图的MeasureSpec, 最后调用measure();并把计算出的MeasureSpec传递进去,之后的流程又和之前的一样了,我们最后看一下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);
...
//省略了一些代码
switch (specMode) {
case MeasureSpec.EXACTLY:
...
case MeasureSpec.AT_MOST:
...
case MeasureSpec.UNSPECIFIED:
...

}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

}


我们可以看到,上述的方法3个参数:

spec:父容器的MeasureSpec(这个未必是父容器的measure方法传入的MeasureSpec, 也可以根据情况构造一个);

padding: 父容器中已经被占用的空间, 如FrameLayout的padding值, LinearLayout前面View占据的空间等;

childDimension: 子元素期望的size(或wrap_content/match_parent);

然后根据传入的MeasureSpec的Mode进行判断和计算。最后返回得出的MeasureSpec;完成~

老规矩,画个图:



View的绘制:2. performLayout();

布局的过程是为了将整个根据子视图的大小以及布局参数将View树放在合适的位置上。

首先进入performLayout():

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
...
//省略一些代码
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}


这个方法中,又会辗转调用host.layout()方法,开始View树的布局,继而回调给View或者ViewGroup中的layout()方法。

如果是ViewGroup最终还是会调用View的layout()方法。

先看看ViewGroup的layout():

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;
}
}


可以看到,它在内部会调用super.layout();转到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);
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;
}


而在layout()方法内部,又调用了onLayout()方法,这个方法是个空方法,需要其子类进行实现。(不同的布局有不同的onLayout())

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}


我们就来看看LinearLayout:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical();
} else {
layoutHorizontal();
}
}


可以看到,对于LinearLayout,如果是竖直排列,调用layoutVertical();要不然就调用layoutHorizontal();当然这里方法细节我们就不再深入了,只要知道在最终调用onLayout()方法时,其具体子类需要具体实现。

画个图~



View的绘制:3. performDraw();

分析完前面的两个步骤,就到了draw()的过程,这里才是真正对视图进行绘制的地方。

private void performDraw() {
...
//省略了一些代码
final boolean fullRedrawNeeded = mFullRedrawNeeded;
...
draw(fullRedrawNeeded);
...
}


内部调用draw():方法:

private void draw(boolean fullRedrawNeeded) {
...
//省略了一些代码
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
...
}


它的内部又是调用drawSoftware();方法:

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
...
//省略一些代码
...
mView.draw(canvas);
...

}


从这里,调用了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
......
if (!dirtyOpaque) {
drawBackground(canvas);
}

// skip step 2 & 5 if possible (common case)
......

// Step 2, save the canvas' layers
......
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
......

// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children
dispatchDraw(canvas);

// Step 5, draw the fade effect and restore layers
......
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
......

// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
......
}


这里,为了更清楚的分析View.draw();我们将代码的细节省略,按步骤进行分析。从流程来看,整个的draw过程分为了6步,我们一步步进行分析:

1.对View的背景的绘制:

调用View.drawBackground();来绘制视图的背景:

private void drawBackground(Canvas canvas) {
//1.
final Drawable background = mBackground;
......
//2.
if (mBackgroundSizeChanged) {
background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
......
//3.
background.draw(canvas);
......
}


方法大概分为3个阶段:1.获取xml中通过android:background属性或者代码中setBackgroundColor()、setBackgroundResource()等方法进行赋值的背景Drawable;2.根据layout过程确定的View位置来设置背景的绘制区域;3.调用Drawable的draw()方法来完成背景的绘制工作。当然具体的绘制背景的操作肯定在Drawable.draw();方法里,这里我们就不在深入了。

2.如果需要的话,存储画布层为padding做准备:(一般这个步骤可以省略)

//1.
int paddingLeft = mPaddingLeft;
final boolean offsetRequired = isPaddingOffsetRequired();
//2.
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
//2.
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
...
//3.
int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}


方法大概有3个阶段:1.根据isPaddingOffsetRequired();方法判断是否需要offset;2.如果需要,更新相关的left、top、right、bottom;3.判断相应的left、top、right、bottom是否进行了更新,将相应更新后的值进行saveLayer();

3.对View的内容进行绘制:

这里调用了View的onDraw()方法:

protected void onDraw(Canvas canvas) {
}


这是个空方法,因为每个View的内容都是不一样的,没有个统一的步骤,所以需要由其子类具体去实现。

4.对当前View的所有子View进行绘制,如果当前的View没有子View就不需要进行绘制。

我们来看View的dispatchDraw();方法

/**
* Called by draw to draw the child views. This may be overridden
* by derived classes to gain control just before its children are drawn
* (but after its own view has been drawn).
* @param canvas the canvas on which to draw the view
*/
protected void dispatchDraw(Canvas canvas) {

}


它也是个空方法,从注释中我们可以知道,如果View包含子View,就需要重写它,所以我们来看ViewGroup的这个方法:

protected void dispatchDraw(Canvas canvas) {
......
//1.
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
......
//2.
for (int i = 0; i < childrenCount; i++) {
......
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
......
//3.
// Draw any disappearing views that have animations
if (mDisappearingChildren != null) {
......
for (int i = disappearingCount; i >= 0; i--) {
......
more |= drawChild(canvas, child, drawingTime);
}
}
......
}


方法大概也有3个阶段:1.获取子View的数目,并且将所有的子VIew存入View数组。2.循环遍历那些显示的子View进行drawChild();3.循环遍历那些已经消失的子View进行drawChild();(为finish这些View做准备)

从这3个阶段中,可以看到,其中又调用了drawChild();的方法:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}


可见drawChild()方法又调用了子View的draw()方法,所以说ViewGroup类已经为我们重写了dispatchDraw()的功能实现,我们一般不需要重写该方法,但可以重载父类函数实现具体的功能。

5.如果需要的话,绘制View的fadding,并且更新存储它的画布层。(一般不需要,步骤省略)

//1.
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
...
//2.
canvas.restoreToCount(saveCount);


根据之前的drawTop、drawBottom、drawLeft、drawRight(后3个在代码中省略,跟drawTop判断差不多)判断是否需要重新的绘制;2.将重新绘制的view存储在相应位置。

6.对View的滚动条进行绘制。

这里调用了View的onDrawScrollBars()方法

/**
* <p>Request the drawing of the horizontal and the vertical scrollbar. The
* scrollbars are painted only if they have been awakened first.</p>
*
* @param canvas the canvas on which to draw the scrollbars
*
* @see #awakenScrollBars(int)
*/
protected final void onDrawScrollBars(Canvas canvas) {
//里面的代码省略
......
}


因为每个View都需要这样的流程,可以知道其实每个View都是由滚动条的,只是一般情况下没有显示而已,绘制ScrollBars分析不是我们这篇的重点,所以暂时不做分析。

至此,整个的performDraw();过程也就分析完了:

画个图~:



好了,至此,View的绘制的整个流程也就完了,在下一章节我们再加一些View绘制过程中的小细节
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android view