对于Android开发者来说,View无疑是开发中经常接触的,包括它的事件分发机制、测量、布局、绘制流程等,如果要自定义一个View,那么应该对以上流程有所了解、研究。本系列文章将会为大家带来View的工作流程详细解析。在深入接触View的测量、布局、绘制这三个流程之前,我们从Activity入手,看看从Activity创建后到View的正式工作之前,所要经历的步骤,包括Activity、DecorView、Window的关联部分,解释View怎么从Window->DecorView->Activity->View的绘制整个线性流程。如下图,需要绘制的View需要在Acvitity的View创建起来之后才开始走测量、布局、绘制等生命流程,Activity的View实际上就是图示的ContentView,即Activity调用setContentView(layoutID)设置的View。ContentView属于DecorView的子View,DecorView属于PhoneWindow的子View,PhoneWindow是Window的一个实例,用于显示一个窗体信息,整个流程显示的明明白白,下面将会详细分析。
1、Window
Window表示一个窗口的概念,Android手机中所有的视图都是通过Window来呈现的,像常用的Activity,Dialog,PopupWindow,Toast,他们的视图都是附加在Window上的,所以可以这么说:Window是View的直接管理者。
分类
- Window有三种类型,分别是应用Window、子Window、系统window。应用Window对应着一个Activity。子Window不能单独存在,他需要附属在特定的父Window之中,比如常见的一些Dialog。系统Window是要声明权限才能创建的Window,比如Toast和系统状态栏。
- Window是分层的,每个Window都有对应的z-ordered,层级大的会覆盖在层级小的Window上面。在三类window中,应用Window的层级范围是1-99,子Window的层级范围是1000-1999,系统Window的层级范围是2000-2999,这些层级范围对应这WindowManager.LayoutParam的type参数。当我们需要使用系统Window时,需要声明权限。
PhoneWindow
- Window的唯一实现类是PhoneWindow ,在启动Activity的attach方法中被创建,Activity中setContentView实际上是调用 PhoneWindow 的setContentView 方法。并且 PhoneWindow 中包含着成员变量 DecorView。如上图所以的页面,实际上你看到的是一个PhoneWindow,它内部负责显示的View就是DecorView,下面我们将揭开这个DecorView的真面目。
2、DecorView
通过上面的解释我们知道,在Activity创建过程中,调用setContentView(layoutID)设置的View,最后将会以子View的形式设置给DecorView的某个子View,这三个DecorView究竟是什么样的结构?什么是DecorView?
什么是DecorView
- DecorView是整个ViewTree的最顶层View,它是一个FrameLayout布局,代表了整个应用的界面,一般包括TitleBar和mContentParent。
- DecorView的子元素mContentParent就是ActivitysetContentView(layoutID)设置的View。
- PhoneWindow中创建了一个DecroView,其中创建的过程中可能根据Theme不同,加载不同的布局格式,例如有没有Title,或有没有ActionBar等,然后再向mContentParent中加入子View,即Activity中设置的布局。
DecorView的布局
我们先从源码级别分析Activity的setContentView(layoutID)都做了啥?
一、setContentView的显示过程
1、Activity的setContentView调用了PhoneWindow的setContentView
//PhoneWindow --> setContentView()
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
//1.初始化 , 创建DecorView对象和mContentParent对象
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();//Activity转场动画相关
}
//2.填充Layout
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,getContext());
transitionTo(newScene);//Activity转场动画相关
} else {
//将Activity设置的布局文件,加载到mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
...
}
2、installDecor():创建DecorView对象和mContentParent对象
//PhoneWindow --> setContentView() --> installDecor()
private void installDecor() {
if (mDecor == null) {
//调用该方法创建new一个DecorView
mDecor = generateDecor();
}else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
//根据主题theme设置对应的xml布局文件以及Feature(包括style,layout,转场动画,属性等)到DecorView中。
//并将mContentParent绑定至id为ID_ANDROID_CONTENT(com.android.internal.R.id.content)的ViewGroup
//mContentParent在DecorView添加的xml文件中
mContentParent = generateLayout(mDecor);
...
}
}
3、generateDecor()—创建DecorView
//PhoneWindow --> setContentView() --> generateDecor()
protected DecorView generateDecor(){
return new DecorView(getContext(), -1);
}
4、generateLayout(mDecor);—创建mContentParent
//PhoneWindow --> setContentView() -->generateLayout()
protected ViewGroup generateLayout(DecorView decor) {
//获取当前的主题,加载默认资源和布局
TypedArray a = getWindowStyle();
...
//根据theme的设定,找到对应的Feature(包括style,layout,转场动画,属性等)
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);//无titleBar
}
...
if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {
setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));//设置全屏
}
if (a.getBoolean(R.styleable.Window_windowTranslucentStatus,false)) {
setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS & (~getForcedWindowFlags()));//透明状态栏
}
//根据当前主题,设定不同的Feature
...
int layoutResource;
int features = getLocalFeatures();
...
if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
layoutResource = R.layout.screen_title;
} else if(){
...
} else {//无titleBar
layoutResource = R.layout.screen_simple;
}
mDecor.startChanging();
//将布局layout,添加至DecorView中
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//从布局中获取`ID_ANDROID_CONTENT`,并关联至contentParent
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
...
//配置完成,DecorView根据已有属性调整布局状态
mDecor.finishChanging();
return contentParent;
}
二、过程分析
从上面的分析我们知道,DecorView会根据每个Activity都是有自己资源ID形式的布局。在填充资源layout时候,会根据不同的feature来选择不同的布局。
大概有如下几种。
R.layout.screen_title_icons
R.layout.screen_progress
R.layout.screen_custom_title
R.layout.screen_action_bar
R.layout.screen_simple_overlay_action_mode;
R.layout.screen_simple
下面我们拿两个布局参考进行分析
- screen_simple.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
- screen_simple_overlay_action_mode.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
</FrameLayout>
无一例外, 无论看几个都有一个id为content 的 FrameLayout。ViewStub用于懒加载actionBar,而id为@android:id/content的FrameLayout,此FrameLayout就是contentView。我们在Activity中调用setContentView方法,设置布局,最终就是添加到该FrameLayout中。分析到这里,整个Activity需要显示的View创建好了,那么如何显示?
- ActivityThread的handleResumeActivit
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
wm.addView(decor, l);
}
}
我们可以看到DecorView通过wm添加到系统中显示出来。到此,DecorView的分析就完了。看到这里,不知道你有没有一种豁然开朗或者有没有一些想法?比如下面的代码我们可以拿到视图最顶级的View,也知道contentView 实际上是个FrameLayout,我们可以给他添加各种View,实现类似Dialog的逻辑,不过动画要自己实现。这种做法是可以替代重量型的Dialog的,因为Dialog使用崩溃概率会增加,维护难度会有所困难。目前大部分开源框架StatusBar的沉浸方案就是使用contentView 中add一个状态栏的方案实现最终效果。更多的想法,需要自己去挖掘了~~
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
ViewGroup contentView = activity.findViewById(android.R.id.content);
3、View的绘制
由上面分析可知:id为“content”的FrameLayout是我们的布局文件加载显示的区域,更确切地说是我们activity的setContentView()方法设置的视图显示的区域。他是个ViewGroup,是个ViewTree,它的绘制将依赖每个子View的绘制。Android中的任何一个布局、任何一个控件包括我们自定义的控件其实都是直接或间接继承自View实现的,所以说这些View应该都具有相同的绘制流程与机制才能显示到屏幕上(可能每个控件的具体绘制逻辑有差异, 但是主流程都是一样的)。每一个View的绘制过程都必须经历三个最主要的过程,也就是measure()、layout()和draw()。那么,整个Android的UI绘制机制是从哪里开始的即入口在哪里呢?答案就是ViewRootImpl类的performTraversals()方法。
回顾DecorView的添加流程
- Activity的attach方法会构造一个PhoneWindow实例
- 我们在onCreate里通过setContentView将我们的xml添加到了DecorView
- ActivityThread在后续过程中会将DecorView添加到Activity的窗口中,也就是添加到PhoneWindow
- WindowManagerGlobal通过ViewRootImpl的setView方法将DecorView传递到ViewRootImpl进行绘制
我们从源码的角度来看下setView方法
//遍历的接口回调
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
//ViewRootImpl -> setView()
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
……
requestLayout();;
}
}
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
//遍历Activity的根布局DecorView里的每一个View。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
可以看到这里post了一个mTraversalRunnable,我们看看这个runnable做了啥事
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
//开始遍历
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//正式绘制的入口
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
performTraversals()
首先我们看这个方法名,perform是执行的意思,而Traversals是遍历循环的意思;所以这个方法看方法名就知道他是在遍历Activity的根布局DecorView里(或者其它窗口比如Dialog)的每一个View。
private void performTraversals() {
...
if (!mStopped) {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); // 1
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
if (didLayout) {
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
}
if (!cancelDraw && !newSurface) {
if (!skipDraw || mReportNextDraw) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
}
}
...
}
我们看到它里面主要执行了三个方法,分别是performMeasure、performLayout、performDraw这三个方法,在这三个方法内部又会分别调用measure、layout、draw这三个方法来进行不同的流程。我们先来看看performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)这个方法,它传入两个参数,分别是childWidthMeasureSpec和childHeightMeasure,那么这两个参数代表什么意思呢?要想了解这两个参数的意思,我们就要先了解MeasureSpec。
MeasureSpec
官方文档对MeasureSpec类的描述:A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode.它的意思就是说,该类封装了一个View的规格尺寸,包括View的宽和高的信息。
要注意,MeasureSpec并不是指View的测量宽高,是根据MeasueSpec而测出测量宽高。在Measure流程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure方法中根据这个MeasureSpec来确定View的测量宽高。MeasureSpec代表一个32位的int值,高2位表示SpecMode,低30位表示SpecSize,而SpecSize是指在某种SpecMode下的规格大小。
SpecMode三种模式
- UNSPECIFIED = 0 << MODE_SHIFT:即: 00000000 00000000 00000000 00000000 父容器不对子View有任何限制,子View要多大给多大,也就是说子View的大小可以超过父容器的大小,例如ListView、ScrollView。
- EXACTLY =1<< MODE_SHIFT:即: 01000000 00000000 00000000 00000000父容器已经测量出子View所需要的固定大小,不会再变了,即MeasureSpec中封装的SpecSize,对应于LayoutParams中的match_parent属性和设置的固定dp值。
- AT_MOST =2 << MODE_SHIFT:即: 10000000 00000000 00000000 00000000父窗口限定了一个最大值给子View即SpecSize,对应于LayoutParams中的wrap_content,自适应大小。
//用户自定义View - > 获取SpecSize和SpecMode
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//宽度的模式
int mWidthModle = MeasureSpec.getMode(widthMeasureSpec);
//宽度大小
int mWidthSize = MeasureSpec.getSize(widthMeasureSpec);
//高度的模式
int mHeightModle = MeasureSpec.getMode(heightMeasureSpec);
//高度大小
int mHeightSize = MeasureSpec.getSize(heightMeasureSpec);
//如果明确大小,直接设置大小
if (mWidthModle == MeasureSpec.EXACTLY) {
...
}
...
}
View的测量流程(Measure)
我们在使用View时是直接设置LayoutParams,但是在View测量的时候,系统会将LayoutParams在父容器的约束下进行相对应的MeasureSpec,然后在根据这个MeasureSpec来确定View的测量后的宽高,由此可见,MeasureSpec不是LayoutParams唯一决定的,还需要父容器一起来决定,在进一步决定View的宽高。但是顶级View,也就是上文我们分析到的DecorView和普通的View的MeasureSpec计算有些区别,对于DecorView,其MeasureSpec是由屏幕的尺寸和LayoutParams决定的,而DecorView的默认LayoutParams就是match_parent(在初始化DecorView时可知),对于普通View来说,其MeasureSpec是由父容器的MeasureSpec和自身的LayoutParams决定。在 performTraversals() 方法中有如下一段:
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
我们再来看下getRootMeasureSpec方法的实现。
/**
* @param windowSize The available width or height of the window
* @param rootDimension The layout params for one dimension (width or height) of the window.
*
* @return The measure spec to use to measure the root view.
*/
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
}
return measureSpec;
}
通过上述代码,对于DecorView来说就是走第一个case,也就是屏幕的尺寸。对于普通View来说,也就是我们Activity显示布局的根View是一个ViewGroup,我们再来看下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,这里的mView就是DecorView,也就是说,从顶级View开始了测量流程,那么我们直接进入measure流程。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
...
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) {
...
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
...
}
}
}
可以看到,它在内部调用了onMeasure方法,由于DecorView是FrameLayout子类,因此它实际上调用的是DecorView#onMeasure方法。在该方法内部,主要是进行了一些判断,这里不展开来看了,到最后会调用到super.onMeasure方法,即FrameLayout#onMeasure方法。由于不同的ViewGroup有着不同的性质,那么它们的onMeasure必然是不同的,因此这里选择了FrameLayout的onMeasure方法来进行分析。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取当前布局内的子View数量
int count = getChildCount();
//判断当前布局的宽高是否是match_parent模式或者指定一个精确的大小,如果是则置measureMatchParent为false.
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
//遍历所有类型不为GONE的子View
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//对每一个子View进行测量
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//寻找子View中宽高的最大者,因为如果FrameLayout是wrap_content属性,那么它的大小取决于子View中的最大者
maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
//如果FrameLayout是wrap_content模式,那么往mMatchParentChildren中添加
//宽或者高为match_parent的子View,因为该子View的最终测量大小会受到FrameLayout的最终测量大小影响
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
// 最大最小宽高还要加上padding的值
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// 检查我们的最小高度和宽度
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// 检查我们前景的最小高度和宽度
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
//保存测量结果
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
//子View中设置为match_parent的个数
count = mMatchParentChildren.size();
//只有FrameLayout的模式为wrap_content的时候才会执行下列语句
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//对FrameLayout的宽度规格设置,因为这会影响子View的测量
final int childWidthMeasureSpec;
/**
* 如果子View的宽度是match_parent属性,那么对当前FrameLayout的MeasureSpec修改:
* 把widthMeasureSpec的宽度规格修改为:总宽度 - padding - margin,这样做的意思是:
* 对于子View来说,如果要match_parent,那么它可以覆盖的范围是FrameLayout的测量宽度
* 减去padding和margin后剩下的空间。
* 以下两点的结论,可以查看getChildMeasureSpec()方法:
* 如果子View的宽度是一个确定的值,比如50dp,那么FrameLayout的widthMeasureSpec的宽度规格修改为:
* SpecSize为子View的宽度,即50dp,SpecMode为EXACTLY模式
* 如果子View的宽度是wrap_content属性,那么FrameLayout的widthMeasureSpec的宽度规格修改为:
* SpecSize为子View的宽度减去padding减去margin,SpecMode为AT_MOST模式
*/
if (lp.width == LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
//同理对高度进行相同的处理,这里省略...
//对于这部分的子View需要重新进行measure过程
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
这里简单总结一下:首先,FrameLayout根据它的MeasureSpec来对每一个子View进行测量,即调用measureChildWithMargin方法。对于每一个测量完成的子View,会寻找其中最大的宽高,那么FrameLayout的测量宽高会受到这个子View的最大宽高的影响(wrap_content模式),接着调用setMeasureDimension方法,把FrameLayout的测量宽高保存。最后则是特殊情况的处理,即当FrameLayout为wrap_content属性时,如果其子View是match_parent属性的话,则要重新设置FrameLayout的测量规格,然后重新对该部分View测量。最后执行代码:child.measure方法,然后在measure方法,会调用onMeasure方法。我们自定义View需要操作的就是这个onMeasure。到此,绘制流程已经从ViewGroup转移到子View中了,具体的测量过程大同小异,读者自定查看源码。
最后简单概括一下整个流程:测量始于DecorView,通过不断的遍历子View的measure方法,根据ViewGroup的MeasureSpec及子View的LayoutParams来决定子View的MeasureSpec,进一步获取子View的测量宽高,然后逐层返回,不断保存ViewGroup的测量宽高。
View的布局流程(Layout)
前面提到,三大流程始于ViewRootImpl#performTraversals方法,在该方法内通过调用performMeasure、performLayout、performDraw这三个方法来进行measure、layout、draw流程,那么我们就从performLayout方法开始说,我们先看它的源码。
ViewRootImpl --> performLayout
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
Log.v(TAG, "Laying out " + host + " to (" +host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
//DecorView开始layout,从左上角(0,0)坐标开始
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
//省略...
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}
由上面的代码可以看出,直接调用了host.layout方法,host也就是DecorView,属于FrameLayout,那么对于DecorView来说,调用layout方法,就是对它自身进行布局,最终确定自身的位置以及子View(如果有)的位置。显然,DecorView的左上位置为0,然后宽高为它的测量宽高。DecorView继承View,最终走的是View的layout逻辑。
View --> layout
/**
* 通过这个方法为View及其所有的子View分配位置
* 派生类不应该重写这个方法,而应该重写onLayout方法
* 并且应该在重写的onLayout方法中完成对子View的布局
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
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;
// ① 通过setOpticalFrame或setFrame为View设置坐标,并判断位置是否发生改变
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// ② 如果位置发生了改变,就调用onLayout方法完成布局逻辑
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<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);
}
}
}
.......
}
①号代码,调用了setFrame方法(setOpticalFrame最终也是调用setFrame),并把四个位置信息传递进去,这个方法用于确定View的四个顶点的位置,即初始化mLeft,mRight,mTop,mBottom这四个值,这个结束之后,DecorView就确定位置了,下一步要确定子View的位置了,也就是onLayout的逻辑。
View --> setFrame
protected boolean setFrame(int left, int top, int right, int bottom) {
//省略...
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
//省略...
return changed;
}
可以看出,它对mLeft、mTop、mRight、mBottom这四个值进行了初始化,对于每一个View,包括ViewGroup来说,以上四个值保存了View的位置信息,所以这四个值是最终宽高,也即是说,如果要得到View的位置信息,那么就应该在layout方法完成后调用getLeft()、getTop()等方法来取得最终宽高,如果是在此之前调用相应的方法,只能得到0的结果,所以一般我们是在onLayout方法中获取View的宽高信息。
onLayout
View基类的layout方法和measure不同,并没有使用final修饰,但注释中也清清楚楚地写着View的派生类不应该重写这个方法,而应该重写onLayout方法,来处理子View的布局方式。如果子类是View则可以不用处理onLayout,交给父类处理即可。但是如果子类是ViewGroup,是必须重写的onLayout方法中完成对子View的布局逻辑,因为ViewGroup中的onLayout是abstract修饰的。
- View --> onLayout
/**
* 当此视图应为其每个子视图指定大小和位置时,从布局调用
* 带有子级的派生类应重写此方法并对其每个子级调用布局。
* @param changed This is a new size or position for this view
* @param left Left position, relative to parent
* @param top Top position, relative to parent
* @param right Right position, relative to parent
* @param bottom Bottom position, relative to parent
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
当派生View集成的是View的话,onLayout是空方法,无需处理子View的布局,因为没有子View。
- ViewGroup --> onLayout
//abstract 修饰,让派生类必须要重写这个方法,在内部处理子View的布局
@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
当派生View继承的是ViewGroup的话,onLayout是abstract 方法,必须要重写而且处理子View。由于不同的布局容器的onMeasure方法均有不同的实现,因此不可能对所有布局方式都说一次,另外上一篇文章是用FrameLayout#onMeasure进行讲解的,那么现在也对FrameLayout#onLayout方法进行讲解。
//FrameLayout.java
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//把父容器的位置参数传递进去
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();
//以下四个值会影响到子View的布局参数
//parentLeft由父容器的padding和Foreground决定
final int parentLeft = getPaddingLeftWithForeground();
//parentRight由父容器的width和padding和Foreground决定
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();
//获取子View的测量宽高
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
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;
//当子View设置了水平方向的layout_gravity属性时,根据不同的属性设置不同的childLeft
//childLeft表示子View的 左上角坐标X值
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
/* 水平居中,由于子View要在水平中间的位置显示,因此,要先计算出以下:
* (parentRight - parentLeft -width)/2 此时得出的是父容器减去子View宽度后的
* 剩余空间的一半,那么再加上parentLeft后,就是子View初始左上角横坐标(此时正好位于中间位置),
* 假如子View还受到margin约束,由于leftMargin使子View右偏而rightMargin使子View左偏,所以最后
* 是 +leftMargin -rightMargin .
*/
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
//水平居右,子View左上角横坐标等于 parentRight 减去子View的测量宽度 减去 margin
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
//如果没设置水平方向的layout_gravity,那么它默认是水平居左
//水平居左,子View的左上角横坐标等于 parentLeft 加上子View的magin值
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
//当子View设置了竖直方向的layout_gravity时,根据不同的属性设置同的childTop
//childTop表示子View的 左上角坐标的Y值
//分析方法同上
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;
}
//对子元素进行布局,左上角坐标为(childLeft,childTop),右下角坐标为(childLeft+width,childTop+height)
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
先梳理一下以上逻辑:首先先获取父容器的padding值,然后遍历其每一个子View,根据子View的layout_gravity属性、子View的测量宽高、父容器的padding值、来确定子View的布局参数,然后调用child.layout方法,把布局流程从父容器传递到子元素。那么到目前为止,View的布局流程就已经全部分析完了。
View的绘制流程(Draw)
前面提到,三大工作流程始于ViewRootImpl#performTraversals,在这个方法内部会分别调用performMeasure,performLayout,performDraw三个方法来分别完成测量,布局,绘制流程。那么我们现在先从performDraw方法看起。
//ViewRootImpl.java
private void performDraw() {
...
final boolean fullRedrawNeeded = mFullRedrawNeeded;
try {
//判断是否需要重新绘制全部视图,如果是第一次绘制视图,那么显然应该绘制所以的视图
//如果由于某些原因,导致了视图重绘,那么就没有必要绘制所有视图
draw(fullRedrawNeeded);
} finally {
...
}
...
}
private void draw(boolean fullRedrawNeeded) {
...
//获取mDirty,该值表示需要重绘的区域
final Rect dirty = mDirty;
...
//如果fullRedrawNeeded为真,则把dirty区域置为整个屏幕,表示整个视图都需要绘制
//第一次绘制流程,需要绘制所有视图
if (fullRedrawNeeded) {
mAttachInfo.mIgnoreDirtyState = true;
dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
...
//把相关参数传递进去,包括dirty区域
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) {
// 用软件渲染器绘制。
final Canvas canvas;
try {
final int left = dirty.left;
final int top = dirty.top;
final int right = dirty.right;
final int bottom = dirty.bottom;
//锁定canvas区域,由dirty区域决定
canvas = mSurface.lockCanvas(dirty);
// The dirty rectangle can be modified by Surface.lockCanvas()
//noinspection ConstantConditions
if (left != dirty.left || top != dirty.top || right != dirty.right || bottom != dirty.bottom) {
attachInfo.mIgnoreDirtyState = true;
}
canvas.setDensity(mDensity);
}
try {
if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
dirty.setEmpty();
mIsAnimating = false;
attachInfo.mDrawingTime = SystemClock.uptimeMillis();
mView.mPrivateFlags |= View.PFLAG_DRAWN;
try {
canvas.translate(-xoff, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
attachInfo.mSetIgnoreDirtyState = false;
//正式开始绘制
mView.draw(canvas);
}
}
return true;
}
首先是实例化了Canvas对象,然后锁定该canvas的区域,由dirty区域决定,接着对canvas进行一系列的属性赋值,最后调用了mView.draw(canvas)方法,前面分析过,mView就是DecorView,也就是说从DecorView开始绘制,前面所做的一切工作都是准备工作,而现在则是正式开始绘制流程。
View的绘制
由于ViewGroup没有重写draw方法,因此所有的View都是调用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;
/*
* 绘制遍历执行必须按适当顺序执行的几个绘制步骤
* 1. 绘制背景
* 2. 如有必要,保存画布层以备褪色
* 3. 绘图视图的内容
* 4. 绘制子对象
* 5. 如有必要,绘制淡入淡出的边并恢复层
* 6. 绘制装饰(例如滚动条)
*/
// 步骤1,如果需要,绘制背景
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 如果可能,跳过步骤2和5(常见情况)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// 第三步,画出内容
if (!dirtyOpaque) onDraw(canvas);
// 第四步,画子View
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// 步骤6,绘制装饰(前景,滚动条)
onDrawForeground(canvas);
// we're done...
return;
}
...
}
可以看到,draw过程比较复杂,但是逻辑十分清晰,而官方注释也清楚地说明了每一步的做法。我们首先来看一开始的标记位dirtyOpaque,该标记位的作用是判断当前View是否是透明的,如果View是透明的,那么根据下面的逻辑可以看出,将不会执行一些步骤,比如绘制背景、绘制内容等。这样很容易理解,因为一个View既然是透明的,那就没必要绘制它了。接着是绘制流程的六个步骤,这里先小结这六个步骤分别是什么,然后再展开来讲。
绘制流程的六个步骤
1、对View的背景进行绘制
2、保存当前的图层信息(可跳过)
3、绘制View的内容
4、对View的子View进行绘制(如果有子View)
5、绘制View的褪色的边缘,类似于阴影效果(可跳过)
6、绘制View的装饰(例如:滚动条)
其中第2步和第5步是可以跳过的,我们这里不做分析,我们重点来分析其它步骤。
Step 1 :绘制背景
private void drawBackground(Canvas canvas) {
//mBackground是该View的背景参数,比如背景颜色
final Drawable background = mBackground;
if (background == null) {
return;
}
//根据View四个布局参数来确定背景的边界
setBackgroundBounds();
// 硬件加速绘制
...
//获取当前View的mScrollX和mScrollY值
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
//如果scrollX和scrollY有值,则对canvas的坐标进行偏移,再绘制背景
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
可以看出,这里考虑到了view的偏移参数,scrollX和scrollY,绘制背景在偏移后的view中绘制。
Step 3:绘制内容
/**
* 执行此操作来绘制图形。
* @param canvas 将在其上绘制背景的画布
*/
protected void onDraw(Canvas canvas) {
}
这里调用了View -> onDraw方法,View中该方法是一个空实现,因为不同的View有着不同的内容,这需要我们自己去实现,即在自定义View中重写该方法来实现。
Step 4: 绘制子View
//View
/**
* 由draw调用以绘制子视图。这可能被派生类重写,
* 以便在绘制其子级之前(但在绘制其自己的视图之后)获得控制权。
* @param canvas 将在其上绘制背景的画布
*/
protected void dispatchDraw(Canvas canvas) {
}
View中该方法是空实现,因为他没有子视图,而在ViewGroup中重写了这个方法,下面我们来看看。
//ViewGroup
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
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);
}
}
..
}
这里简单说明一下,里面主要遍历了所以子View,每个子View都调用了drawChild这个方法,我们找到这个方法。
//View -> drawChild
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
//View -> draw
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...
if (!drawingWithDrawingCache) {
if (drawingWithRenderNode) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
} else {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
draw(canvas);
}
}
} else if (cache != null) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
if (layerType == LAYER_TYPE_NONE) {
// no layer paint, use temporary paint to draw bitmap
Paint cachePaint = parent.mCachePaint;
if (cachePaint == null) {
cachePaint = new Paint();
cachePaint.setDither(false);
parent.mCachePaint = cachePaint;
}
cachePaint.setAlpha((int) (alpha * 255));
canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
} else {
// use layer paint to draw the bitmap, merging the two alphas, but also restore
int layerPaintAlpha = mLayerPaint.getAlpha();
mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
mLayerPaint.setAlpha(layerPaintAlpha);
}
}
}
我们主要来看核心部分,首先判断是否已经有缓存,即之前是否已经绘制过一次了,如果没有,则会调用draw(canvas)方法,开始正常的绘制,即上面所说的六个步骤,否则利用缓存来显示。这一步也可以归纳为ViewGroup绘制过程,它对子View进行了绘制,而子View又会调用自身的draw方法来绘制自身,这样不断遍历子View及子View的不断对自身的绘制,从而使得View树完成绘制。
Step 6: 绘制装饰
所谓的绘制装饰,就是指View除了背景、内容、子View的其余部分,例如滚动条等,我们看View -> onDrawForeground。
public void onDrawForeground(Canvas canvas) {
onDrawScrollIndicators(canvas);
onDrawScrollBars(canvas);
final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
if (foreground != null) {
if (mForegroundInfo.mBoundsChanged) {
mForegroundInfo.mBoundsChanged = false;
final Rect selfBounds = mForegroundInfo.mSelfBounds;
final Rect overlayBounds = mForegroundInfo.mOverlayBounds;
if (mForegroundInfo.mInsidePadding) {
selfBounds.set(0, 0, getWidth(), getHeight());
} else {
selfBounds.set(getPaddingLeft(), getPaddingTop(),
getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
}
final int ld = getLayoutDirection();
Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
foreground.setBounds(overlayBounds);
}
foreground.draw(canvas);
}
}
和一般的绘制流程非常相似,都是先设定绘制区域,然后利用canvas进行绘制。那么,到目前为止,View的绘制流程也讲述完毕了,希望这篇文章对你们起到帮助作用,谢谢你们的阅读。
结尾
有句话说的不错,好记忆不如烂笔头。Android学习过程中,最好的方式就是记录,记多了看多了不知不觉就成专家了。而写博客就是帮自己梳理知识点的最好的方式,你可以尝试去多读几篇类似的文章,然后自己梳理记下来,写出自己的博客,你会受益匪浅而且极易深刻,不信你试试!!共勉吧~