Android View绘制流程详解
在Android开发中,理解View的绘制流程对于优化应用性能和解决UI相关问题至关重要。本文将按照正确的时序详细解析从Activity启动到测量(Measure)、布局(Layout)、绘制(Draw)的完整流程。
引言
Android应用的用户界面是由一个个View组成的,从简单的Button、TextView到复杂的自定义View,它们都需要经过一系列的流程才能呈现在用户面前。了解这些流程不仅有助于我们编写高效的UI代码,还能帮助我们解决一些棘手的界面问题。
View的绘制流程主要分为三个阶段:
- 测量阶段(Measure):确定View的大小
- 布局阶段(Layout):确定View的位置
- 绘制阶段(Draw):将View绘制到屏幕上
这三个阶段由ViewRootImpl统一管理,并按照一定的顺序执行。接下来我们将按照正确的时序深入探讨每个阶段的具体实现。
1. Activity启动流程
1.1 ActivityThread中的关键方法
在Activity的启动过程中,ActivityThread中的handleLaunchActivity、performLaunchActivity、handleResumeActivity这3个主要的方法完成了Activity的创建到启动工作。
handleLaunchActivity()
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
if (localLOGV) Slog.v(
TAG, "Handling launch of " + r);
//分析1 : 这里是创建Activity,并调用了Activity的onCreate()和onStart()
Activity a = performLaunchActivity(r, customIntent);
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
Bundle oldState = r.state;
//分析2 : 这里调用Activity的onResume()
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed);
}
....
}
performLaunchActivity()
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
......
//分析1 : 这里底层是通过反射来创建的Activity实例
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
//底层也是通过反射构建Application,如果已经构建则不会重复构建,毕竟一个进程只能有一个Application
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
if (activity != null) {
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
+ r.activityInfo.name + " with config " + config);
//分析2 : 在这里实例化了PhoneWindow,并将该Activity设置为PhoneWindow的Callback回调,还初始化了WindowManager
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config);
//分析3 : 间接调用了Activity的performCreate方法,间接调用了Activity的onCreate方法.
mInstrumentation.callActivityOnCreate(activity, r.state);
//分析4: 这里和上面onCreate过程差不多,调用Activity的onStart方法
if (!r.activity.mFinished) {
activity.performStart();
r.stopped = false;
}
....
}
}
handleResumeActivity()
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
.....
//分析1 : 在其内部调用Activity的onResume方法
r = performResumeActivity(token, clearHide, reason);
.....
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
//获取WindowManager
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
if (a.mVisibleFromClient) {
.....
//分析2 : WindowManager添加DecorView
wm.addView(decor, l);
...
}
.....
}
在handleResumeActivity方法中,WindowManager会添加DecorView,这是触发View绘制的关键步骤。
2. setContentView()方法执行过程
当我们在Activity中调用setContentView()方法时,实际调用的是Window的setContentView()方法:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
2.1 PhoneWindow中的setContentView()
Window是一个抽象类,其唯一实现类是PhoneWindow。在PhoneWindow中,setContentView()方法主要完成以下工作:
- 创建DecorView
- 根据主题选择合适的布局
- 将我们设置的布局添加到content容器中
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
2.2 DecorView的创建
installDecor()方法负责创建DecorView:
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
}
DecorView继承于FrameLayout,是Activity的根布局。generateLayout()方法会根据主题选择不同的布局,但都会包含一个ID为com.android.internal.R.id.content的容器组件,我们通过setContentView()设置的布局会被添加到这个容器中。
3. View绘制的触发时机
3.1 WindowManagerImpl的addView()
当Activity执行到onResume之后,会调用WindowManager的addView方法将DecorView添加到窗口中:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
我们看到,方法里面将逻辑交给了mGlobal,mGlobal是WindowManagerGlobal,WindowManagerGlobal是全局单例。WindowManagerImpl的方法都是由WindowManagerGlobal完成的。
3.2 WindowManagerGlobal的addView()
在WindowManagerGlobal中,会创建ViewRootImpl并调用其setView()方法:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
....
ViewRootImpl root;
synchronized (mLock) {
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
.....
root.setView(view, wparams, panelParentView);
}
}
在这里我们看到了,实例化ViewRootImpl,然后建立ViewRootImpl与View的联系。跟着进入setView方法:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
.....
requestLayout();
.....
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//检查线程合法性
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
......
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
....
performTraversals();
....
}
}
一路下来,我们来到了熟悉的方法面前:performTraversals方法。这是整个View绘制流程的入口。
4. View绘制核心流程 - performTraversals
绘制的入口是performTraversals()方法:
private void performTraversals() {
//分析1 : 这里面会调用performMeasure开始测量流程
measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
//分析2 : 开始布局流程
performLayout(lp, mWidth, mHeight);
//分析3 : 开始绘画流程
performDraw();
}
4.1 performTraversals()方法详解
private void performTraversals() {
// 获取MeasureSpec
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width, lp.privateFlags);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height,
lp.privateFlags);
// 测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 布局
performLayout(lp, mWidth, mHeight);
// 绘制
performDraw();
}
整个View绘制流程按照"测量(Measure) -> 布局(Layout) -> 绘制(Draw)"的顺序执行。
5. 测量流程(Measure)
5.1 performMeasure()方法
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
测量过程从根View开始,递归遍历整个View树,计算每个View的尺寸。
5.2 View的measure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
//调用onMeasure方法
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}
measure方法里面有一些检测是否需要重新onMeasure的代码,被略去了。
onMeasure是View里面的方法,ViewGroup是一个抽象类并且没有重写onMeasure。因为onMeasure方法的实现,每个都是不一样的,比如LinearLayout和FrameLayout的onMeasure方法肯定是实现逻辑不一样的。
因为DecorView是FrameLayout,所以我们看看FrameLayout中的onMeasure。
5.3 FrameLayout的onMeasure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//分析1 : 遍历所有子控件,测量每个子控件的大小
//参数1:View控件
//参数2:宽MeasureSpec
//参数3:父容器在宽度上已经用了多少了,因为FrameLayout的规则是:前面已经放置的View并不会影响后面放置View的宽高,是直接覆盖到上一个View上的.所以这里传0
//参数4:高MeasureSpec
//参数5:父容器在高度上已经用了多少了
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
}
}
......
//分析2 : 测量完所有的子控件的大小之后,才知道自己的大小 这很符合FrameLayout的规则嘛
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
......
}
FrameLayout的onMeasure方法中会遍历所有子控件,然后进行所有子控件的大小测量。最后才来设置自己的大小。注意,onMeasure方法的入参MeasureSpec是从父容器传过来的,意思就是给你个参考,你自己看着办吧。
5.4 measureChildWithMargins方法
在测量子控件大小的时候会调用ViewGroup的measureChildWithMargins方法:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//获取子控件的LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//分析1: 计算子控件在宽上的MeasureSpec
//参数1:父容器的MeasureSpec
//参数2:这里官方的入参名称是padding,从下面这个传值的形式来看,显然是子控件在宽上不能利用的空间(ViewGroup的左右两边padding+子控件的左右margin+父容器在宽度上已经使用了并且不能再使用的空间)
//参数3:子控件想要的宽度
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//分析2: 将measure过程传递给子控件 如果子控件又是一个ViewGroup,那么继续向下传递
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
在measureChildWithMargins方法里我们首先是看到根据子控件的LayoutParams和父容器的MeasureSpec计算子控件的MeasureSpec,然后将计算出的MeasureSpec通过子控件的measure方法传递下去。如果子控件又是一个ViewGroup,那么它又会重复的measure流程,一直向下传递这个过程,直接最后的那个是View为止。因为View没有子控件,它就不能向下传递了。
5.5 MeasureSpec介绍
在测量过程中,Android使用MeasureSpec类来封装测量规格。MeasureSpec包含两个信息:
- 测量模式(Mode):UNSPECIFIED、EXACTLY、AT_MOST
- 测量大小(Size):具体的尺寸值
MeasureSpec代表的是32位的int值,它的高2位是SpecMode(也是一个int),低30位是SpecSize(也是一个int),SpecMode是测量模式,SpecSize是测量大小。MeasureSpec相当于是两者的结合。系统封装了如何从MeasureSpec中提取SpecMode和SpecSize,也封装了用SpecMode和SpecSize组合成MeasureSpec。
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
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(int size,int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
SpecMode有3种,分别是:
UNSPECIFIED:父容器对View不会有任何限制,要多大给多大,一般是用在系统内部使用,我们开发的APP用不到。EXACTLY:这种情况对应于match_parent和具体数值这两种模式,父容器已经检测出View需要的精确大小。AT_MOST:这种情况对应于wrap_content,父容器指定了一个最大值,View不能超过这个值。
测量过程通过measure()方法传递MeasureSpec,每个View根据自己的LayoutParams和父容器的MeasureSpec来决定自己的测量规格。
5.6 MeasureSpec的创建规则
MeasureSpec的创建规则如下:
- 如果View的宽高是固定的值,那么不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY
- 如果View的宽高是wrap_content,那么不管父容器的MeasureSpec是EXACTLY还是AT_MOST,最终View的MeasureSpec都是AT_MOST,而且View最终的大小不能超过父容器的剩余空间
- 如果View的宽高是match_parent,那么要分两种情况:
- 如果父容器是EXACTLY,那么View就是EXACTLY
- 如果父容器是AT_MOST,那么View也是AT_MOST
5.7 获取根View的MeasureSpec
measure是从上往下执行的,widthMeasureSpec和heightMeasureSpec通常情况下是由父容器传递给子视图的。但是最外层的根视图,怎么拿到MeasureSpec呢?在执行performMeasure方法之前,我们需要拿到最外层的视图的MeasureSpec:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
//如果是MATCH_PARENT,那么就是EXACTLY
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
//如果是WRAP_CONTENT,就是AT_MOST
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
//如果是固定的值,也是EXACTLY
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
最外层的根视图的MeasureSpec只由自己的LayoutParams决定,做自己的主人,舒服。
5.8 getChildMeasureSpec方法
//这里来自ViewGroup的getChildMeasureSpec方法,无删减
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//根据父容器的MeasureSpec获取父容器的SpecMode和SpecSize
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//剩下的size
int size = Math.max(0, specSize - padding);
//最终的size和mode
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
//父容器有一个确定的大小
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//子控件也是确定的大小,那么最终的大小就是子控件设置的大小,SpecMode为EXACTLY
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.
//子控件想要自己定义大小,但是不能超过剩余空间 size
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);
}
这段代码对应着下面这段总结:
- 如果View的宽高是固定的值,那么不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY
- 如果View的宽高是
wrap_content,那么不管父容器的MeasureSpec是EXACTLY还是AT_MOST,最终View的MeasureSpec都是AT_MOST,这里暂时不用管UNSPECIFIED(我们用不到)。而且View最终的大小不能超过父容器的剩余空间 - 如果View的宽高是
match_parent,那么要分两种情况:- 如果父容器是EXACTLY,那么View就是EXACTLY
- 如果父容器是
AT_MOST,那么View也是AT_MOST
5.9 measure小结
从ViewRootImpl的performTraversals方法开始进入View的绘制过程,performTraversals方法里面会有一个performMeasure方法。这个performMeasure方法是专门拿来测量View的大小的。而且会遍历整个View树,全部进行测量。
在performMeasure里面会调用measure方法,然后measure会调用onMeasure方法,而在onMeasure方法中则会对所有的子元素进行measure过程。这相当于完成了一次从父元素到子元素的measure传递过程,如果子元素是一个ViewGroup,那么继续向下传递,直到所有的View都已测量完成。测量完成之后,我们可以根据getMeasureHeight和getMeasureWidth方法获取该View的高度和宽度。
6. 布局流程(Layout)
6.1 performLayout()方法
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
.....
//这里的host其实是根视图(DecorView)
//参数:left,top,right,bottom 这些位置都是相对于父容器而言的
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
.....
}
在performLayout方法里面调用了DecorView的layout方法,然后我发现:layout方法其实是View这个父类里面的,然后ViewGroup继承了View之后重写了一下,只是调了一下super.layout(l, t, r, b);,相当于实现还是在View的layout里面。而且ViewGroup的layout方法是final修饰的,意味着子类不能再重写这个方法了。
6.2 View的layout()方法
//以下是View的layout方法
public void layout(int l, int t, int r, int b) {
......
onLayout(changed, l, t, r, b);
......
}
layout方法其实就是调用onLayout方法,如果这里子控件是一个View的话,那么onLayout其实是空实现。onLayout在ViewGroup是一个抽象方法,如果是一个ViewGroup的话,比如FrameLayout,那么onLayout是需要自己实现的。
//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);
因为我们的根视图是DecorView,也就是FrameLayout,那么我们来看一下FrameLayout的onLayout实现:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//布局子控件 我没看懂这个changed参数是拿来干什么的,好像并没有用上(这里已经是FrameLayout的onLayout方法的全部代码了)
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
//最左侧
final int parentLeft = getPaddingLeftWithForeground();
//最右侧
final int parentRight = right - left - getPaddingRightWithForeground();
//最顶部
final int parentTop = getPaddingTopWithForeground();
//最底部
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//因为已经measure流程走完了,所以这里是能通过getMeasuredWidth方法获取测量宽度的
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
//实际子控件的left
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
//水平居中
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT: //子控件在父容器的最右侧
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT: //子控件在父容器的最左侧
default:
childLeft = parentLeft + lp.leftMargin;
}
//竖直方向上的gravity
switch (verticalGravity) {
case Gravity.TOP: //位于父容器的顶部
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL: //垂直居中
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM: //位于父容器底部
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
//最后给这个子控件一个最终的left,top,right,bottom值
//把这个子控件放在这里
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
不同的ViewGroup的实现类的onLayout方法实现是不一样的,是根据自身情况来决定将子控件放在那里的,比如FrameLayout和LinearLayout的onLayout是不一样的实现,但是onLayout这个方法最终是将各个子控件有条不紊的放在对应的位置上。
我们看到在onLayout方法的最后,调用了子控件的layout方法,其实就是将layout流程向下进行传递了。如果子控件还是ViewGroup的话,那么它又会对它自己所有的子控件进行布局,放置。最后一层一层的往下,直到全部都layout完成。每个View都知道自己的left,top,right,bottom。这个时候是可以通过View的getWidth和getHeight来获取最终的宽高的。
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
6.3 layout小结
layout主要是为了确定该控件以及其子控件的位置和大小。在performLayout中,主要是确定每个控件的left+top+right+bottom,performLayout之后它们的位置就已经被确定了,就只剩下最后一步绘制了。
7. 绘制流程(Draw)
7.1 performDraw()方法
private void performDraw() {
draw(fullRedrawNeeded);
}
还是从ViewRootImpl的performTraversals方法开始分析:
private void performTraversals() {
//开始绘画流程
performDraw();
}
private void performDraw() {
......
draw(fullRedrawNeeded);
......
}
private void draw(boolean fullRedrawNeeded){
.....
drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty);
.....
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
......
mView.draw(canvas);
......
}
随着方法的调用深入,发现来到了View的draw方法。
7.2 View的draw()方法
public void draw(Canvas canvas) {
.....
/*
注意了这是官方给的注释,谷歌工程师还真是贴心,把draw步骤写的详详细细,给力,点赞
* 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
//1. 绘制背景
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
//3. 绘制自己的内容
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
//4. 绘制子控件 如果是View的话这个方法是空实现,如果是ViewGroup则绘制子控件
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)
//6. 绘制装饰和前景
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
//7. 绘制默认焦点高亮显示
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
.....
}
draw的基本步骤如下:
- 绘制背景
- 绘制控件自己本身的内容
- 绘制子控件
- 绘制装饰(比如滚动条)和前景
这里简单提一下dispatchDraw方法,在这个方法里面会去调用drawChild方法,在drawChild里面会调用子控件的draw方法,这相当于完成了draw的传递过程,通知子控件去绘制它自己。然后如果子控件是ViewGroup,它又会重复上面这个递推。
7.3 Draw流程详解
绘制流程是View显示到屏幕上的最后一步,它决定了View的最终外观:
- 绘制背景 (
drawBackground):绘制View的背景色或背景图片 - 保存画布状态:保存当前Canvas的状态,便于后续恢复
- 绘制内容 (
onDraw):这是开发者最常重写的方法,用于绘制View的具体内容 - 绘制子View (
dispatchDraw):对于ViewGroup,需要遍历绘制所有子View - 绘制装饰 (
onDrawForeground):绘制滚动条、前景等装饰元素 - 恢复画布状态:恢复之前保存的Canvas状态
需要注意的是,绘制顺序遵循"后绘制的覆盖先绘制的"原则,因此子View会覆盖父View的内容。
draw的流程比测量和布局要简单一些,但是需要注意的是,View绘制过程是通过dispatchDraw来传递的。
8. 总结
Android View的绘制流程按照正确的时序可以概括为以下步骤:
- Activity启动阶段:Activity通过ActivityThread的handleLaunchActivity、performLaunchActivity、handleResumeActivity方法完成创建和启动
- setContentView阶段:在Activity的onCreate方法中调用setContentView,创建PhoneWindow和DecorView,并将布局添加到content容器中
- 添加到WindowManager阶段:Activity启动时,在onResume之后通过WindowManager将DecorView添加到系统窗口管理器中
- 触发绘制阶段:通过ViewRootImpl触发测量、布局、绘制流程
- 测量阶段:从根View开始递归计算每个View的尺寸
- 布局阶段:确定每个View在屏幕上的位置
- 绘制阶段:将View内容绘制到屏幕上
理解这个流程有助于我们:
- 优化布局性能,减少不必要的measure/layout
- 自定义View时正确实现measure、layout、draw方法
- 解决UI相关的问题,如布局错乱、绘制异常等
通过深入理解View的绘制机制,开发者可以更好地进行性能调优和自定义控件开发,从而构建更流畅、用户体验更好的Android应用。