在android开发过程中相信很多人对View树并不陌生,本文着重介绍android中view树是如何绘制。
什么是View树
下面通过一个图示简单描述View树
通过上述这个View树的绘制,我们大致能够猜想出整个View树的绘制过程其实是一个View的递归绘制。
View绘制主要是通过 measure、layout、draw这三个方法进行绘制的,而我们通过源码知道view绘制的实现主要是在ViewRootImpl该类中实现。在改类中View的绘制流程是从ViewRootImpl的performTraversals()方法开始的。
该方法太长,这里我只截取了其中关于View绘制流程的代码;
private void performTraversals() {
....
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
performDraw();
...
}
下面我们先通过一个图是简单描述View绘制的大致流程
View 绘制流程第一步 measure
measure在View绘制中主要作用是用于测量,通过上述图示,我们知道measure是在perfromMeasure方法内进行调用的。那么我们先来看ViewRootImpl中 performMeasure中主要做了什么。
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(childWidthMeasureSpec, childHeightMeasureSpec);
可以看到具体的测量过程是在mView的measure方法内实现,该方法的具体实现可以看到,
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
....
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
.....
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
....
}
通过上述这端代码我们可以看出,内部 cacheIndex < 0 || sIgnoreMeasureCache 条件成立的情况下会直接调用
onMeasure(widthMeasureSpec, heightMeasureSpec);
否则 调用
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
而对于View的测量我们知道实质上是调用了 onMeasure 这个方法;
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在这个方法内部我们可以看到器内部的参数分别表示 measuredWidth, measuredHeight
由此我们可以看出View的测量是通过 getDefaultSize 方法实现的。下面我们来详细看一下 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的绘制过程,对于ViewGroup 我们知道除了绘制自身之外,还会去变量其内部子view然后调用子view的measure方法进行绘制。
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(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);
}
我们可以看出其是直接调用了ViewGroup内部子view的measure方法。
至此View的测量(measure)方法我们大致了解。
下面我们以一个图示说明View的测量过程
View 绘制流程第二步 layout
layout的作用主要是ViewGroup用来确定子元素的位置,当ViewGroup位置确定之后它在onLayout方法中会遍历所有子元素并调用其layoug方法来确定View本身的位置。
下面我们通过api中的源码对layout过程进行详解。
我们知道当measure执行完之后会执行相应的layout过程。
在viewRootImpl类中我们在performLayout方法中可以看到
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
...
final View host = mView;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
内部直接执行来layout方法,在该方法内部,
public void layout(int l, int t, int r, int b) {
...
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);
...
}
}
}
..
}
我们可以看到 changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED 当该判断成立时,内部其是是直接执行了 onLayout(changed, l, t, r, b);
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
该方法我们在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);
也是一个空实现方法,由此可见layout方法的具体实现是根据具体不同的布局实现的。
所以到此我们可以很清晰的了解,layout的执行过程与measure相似,但是也存在不同。
下面我们同样通过一个图示来理解layout的执行过程。
View 绘制第三步 draw
draw的作用主要是用来将view绘制到屏幕上,这个方法相对于measure以及layout 比较简单,下面我们来看一下android api中是如何实现draw方法的。
在ViewRootImpl类中
private void performDraw() {
...
draw(fullRedrawNeeded);
...
}
之后在draw方法内
private void draw(boolean fullRedrawNeeded) {
....
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
....
}
通过drawSoftware 方法内部我们可以看出内部直接调用了mView.draw(..)方法
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
...
mView.draw(canvas);
...
}
接下来我们只需要看mView中draw方法内部的具体作用
通过 draw方法,我们可以看出绘制view主要分这么几步,
1.绘制背景
2.绘制自己
3.绘制子元素
4.绘制装饰
public void draw(Canvas canvas) {
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
...
if (!verticalEdges && !horizontalEdges) {
// Step 2, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 3, draw the children
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 4, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
}
下面我们主要看第三步 是如何绘制 子元素。
protected void dispatchDraw(Canvas canvas) {
}
我们可以看到在view类中dispatchDraw 方法实质是一个空实现,这里我们可以想到作为一个具体的view如果不是容器类型,那么它本身是没有子元素的,所以我们该方法的实现只会在容器类型的控件中,下面我们看容器类型元素的父类ViewGroup
在该类中我们找到了这个方法的具体实现。
protected void dispatchDraw(Canvas canvas) {
....
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
}
通过上述代码我们其事可以看到,内部对容器的子元素进行遍历,同时调用了drawChild(…)该方法。
而在drawChild方法中;
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
直接调用了 draw方法。
下面我们同样通过一个图示对draw方法对过程进行描绘,
至此本文对View的绘制流程已讲解完成,还望各位看官不吝赐教。