本文讲的是从布局加载、activity启动、绘制流程的讲解需要对照源码来看,如果有什么错误也请大家见谅!
每当我们启动一个activity之后,我们之前在xul里面写的标签对布局就会按照我们想要的样式呈现在屏幕上,android是如何将xml会知道屏幕上的呢?对于ui的绘制,我们就会有三个疑问:
- android是如何将xml布局加载进activity绘制的window上面的?
- 布局是在什么时候开始绘制的?
- ui的绘制流程是怎样的?
针对上面的疑问我们一探究竟吧!
目录
[TOC]
将xml布局加载到window里面的过程
通常我们将写好的xml布局都会在activity的onCreate()方法里面调用setContentView(int layoutResID)来引入我们的布局,它的目的就是将xml布局放入到window的decorView下面一个叫content的FrameLayout控件下面作为它的子View。
我们就进入setContentView()方法里面进行分析吧!
code_1:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
上面的getWindow()是获取当前activity所持有的window,它是一个抽象类主要是呈现ui及ui相关的控制处理的。它有一个唯一子类PhoneWindow,而我们这里得到的window就是PhoneWindow。
Window里面会有一个内部类DecorView(以前),在后面的版本把它抽出来单独成一个类了(26版本是这样的了)
code_2:
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
private static final String TAG = "DecorView";
……
}
PhoneWindow会持有这个DecorView,也就是我们所说的根View.
我们到PhoneWindow的setContentView(int layoutResID)里面来
code_3:
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
//这里是安装DecorView的,跟我们app为window配置的样式,来安装哪一种布局,通常根据我们app设置的样式来安装,也可以根据activity布局文件里面配置的样式来安装。
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 {
//将上面安装到decorview的布局里面的名叫content的控件(FrameLayout)添加子空间,也就是我们写的xml布局。
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
根据上面的代码说明,我们来看看installDecor()具体做了那些事情,贴代码如下:
code_4:
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//生成一个DecorView,根View.
mDecor = generateDecor(-1);
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
//mContentParent就是我们所说的名叫content的那个控件
//generateLayout(mDecor)里面给mDecor绑定了之前设定样式的对应系统布局,并把系统布局的里面名叫content控件返回
mContentParent = generateLayout(mDecor);
……
//获取actionbar
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
R.id.decor_content_parent);
……
} else {
mTitleView = findViewById(R.id.title);
if (mTitleView != null) {
if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
final View titleContainer = findViewById(R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);
} else {
mTitleView.setVisibility(View.GONE);
}
mContentParent.setForeground(null);
} else {
mTitleView.setText(mTitle);
}
}
}
}
……
}
}
上面代码中我们可以看到第87行,根据描述我们可以确定为什么当我们要隐藏actionbar时
requestWindowFeature(Window.FEATURE_NO_TITLE)必须要在放在setContentView()之前了吧!
generateLayout(mDecor)方法里面的代码据不去深究了。
给mDercor绑定的具体系统样式的布局,我挑选一个最简单的给大家展示一下,叫做R.layout.screen_simple。
code_5:
<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">
<!--这是一个actionbar的懒加载布局 -->
<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>
我们的布局就是添加到content上面的。
我们的decorView安装完成之后,我再回到setContentView(int layoutResID)里面来,也就是代码code_3里面的64行, mLayoutInflater.inflate(layoutResID, mContentParent)将我们的xml添加到content里面去。这个方法的也是我们经常会用到的,它大概是这样的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iQn3LTi9-1576064334422)(http://note.youdao.com/noteshare?id=b46292d34c78592cd623d0964418dc62&sub=6D6E1121C3C54B37BC972651ED9EA331)]link
在activity的启动流程中的什么时候开始绘制的
首先我们来看一看ActivityThread这个类,它是整个程序的主入口,我们可以看到有个main()方法。
code_6:
public static void main(String[] args) {
……
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
主函数开启了一个looper,让我们的主线程一直保持运行。我们再来看看sMainThreadHandler接受了那些消息和处理吧,sMainThreadHandler是一个继承Handler的H类,由于代码过长就不贴出来,它里面处理了很多逻辑,比如启动application、activity、service等等。这个H应该好好深入研究一下的。
我们现在就看一下H里面启动activity的部分吧。
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
//启动activity
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
……
}
}
handleLaunchActivity()方法里面会执行的流程我画一个时序图,根据这个时序图我们可以看到绘制流程是在onResume()之后执行的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T7zZJiin-1576064334423)(http://note.youdao.com/noteshare?id=b47b8a3fe1bbe01530a9d9f30296964b)]link
在这之后,WindowManagerGlobal会将Decorview和ViewRootImpl进行关联,调用requestLayout()发起绘制,流程如下。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6s9z2CXq-1576064334424)(http://note.youdao.com/noteshare?id=a74922eb7d38758c66ca9c115abf3a5e)]link
这里有个注意点:requestLayout() 和 invalidate() 都是重新绘制,他们有什么区别呢?
调用控件的requestLayout(),它回去寻找父节点一直递归调用到ViewRootImpl的requestLayout()来对整个布局进行重回,ViewRootImpl和所有ViewGroup都是实现了ViewParent的。调用控件的invalidate()只是重绘控件自己和它的子控件。
根据上面的图就不贴代码出来了,重点就是知道绘制的时机。
ui的具体绘制流程
通过前面的了解,ViewRootImpl的performTraversals()就是正真绘制的地方了,它里面包括了测量、摆放和画控件,这个方法有点长,有700多行。我们能在里面找到performMeasure()、performLayout ()、 performDraw()方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4oRaaRXQ-1576064334424)(http://note.youdao.com/noteshare?id=c414e72fb1c5d0e144c07bd668c5d15a&sub=7A5ACD702EC841D194026C9340F66C04)]link
1. 测量
控件的测量最终目的就是测量它的宽高,测量规律就是先遍历测量子控件的宽高然后再来测量自己的宽高。
在测量之前我们需要了解一个测量规格MesasureSpec,它是一个处理32位二进制的int数值的一个工具类,它把32位二进制的int值分成了两部分Size和Mode,低30位就代表测量尺寸,高2位就到测量模式。
低30位的Size代表可供参考的测量值,Mode有3种模式,如下:
MeasureSpec.EXACTLY 代表Size是精确的值,比如精确的数字100dp,match_parent等。
MeasureSpec.AT_MOST 代表Size是最大可参考的值,比如warp_content.
MeasureSpec.UNSPECIFIED 代表Size是不确定的值,通常子控件是不会参考这个值的,根据自己实际情况来绘制
MeasureSpec的作用是View树在遍历测量的时候,父容器告诉子控件他的大小和模式,子容器根据根据实际的情况来对自身进行测量。
我们来看一看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;
/*
*将尺寸和大小封装一个int值
*/
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//获取mode值
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
//获取size值
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);
}
}
可以看出来,MeasureSpec提供了存取size和mode的方法。关于如何组装int值可以看下面的计算示例。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jxypfuiw-1576064334424)(http://note.youdao.com/noteshare?id=3dd9ecdd517408d2f597c2f1e0753199&sub=A6DA687588644BA3B4B8E2200E81E249)]link
View和ViewGroup在测量的时候是有区别的,view只需要测量本身,而ViewGroup是需要测量子控件和自己的。所以ViewGroup就提供能三个测量子控件的方法measureChildren(int widthMeasureSpec, int heightMeasureSpec)、
measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec)、
measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed),在我们自定义控件的时候可供选用。
我们来看一下这三个方法的代码。
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);
}
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
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);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measureChildren()方法是对子控件进行遍历测量;measureChild()是测量子控件,measureChildWithMargins()也是测量子控件,但是它将子控件的margin也作为子控件的宽高的一部分。
我们在上面代码中发现一个方法getChildMeasureSpec(int spec, int padding, int childDimension),它是去计算子控件MeasureSpec的值的。我们点进去看一下。
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);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
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.
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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
我这里有个计算子控件的MeasureSpec的规则表,我们可以来好好理解一下。这个表就是根据上面代码遵循的规则。我们写自定义控件的时候也尽量遵循这样的规则。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2IKf5eXF-1576064334424)(http://note.youdao.com/noteshare?id=822340c1dfcafefc0ec44bc0adf2c553&sub=24786B581D9343DEBF2CEB016BD50D78)]link
父控件测量完子控件之后再来测量自己,然后将测量的只给mMeasuredWidth、mMeasuredHeight,完成这个操作的方法就是setMeasuredDimension(int measuredWidth, int measuredHeight),切记一定要在本控件测量完成之后调用这个方法,看了代码之后我们就知道了当测量完成之后我们调用getMeasuredHeight()、getMeasuredWidth()才会有值。根据测量规则我们可以看看FrameLayout和LinearLayout是如何测量的吧。
2. 摆放
控件的摆放最终目的就是设置它的左上右下的坐标值,摆放规律就是先设置自己的坐标值,然后再遍历设置子空间的坐标值。
摆放流程就是先执行自己的layout()方法设置自己的坐上右下位置,然后再去调用onLayout(),layout()方法是是被ViewGroup类final了的。
我们分析两个问题,我们自定义控件的时候通常会添加一个onSizeChanged(int w, int h, int oldw, int oldh)那这个方法是在什么时候执行的呢?
我们在layout的时候会去设置mLeft、mTop、mRight、mBottom,在设置之前先判断新的宽高值和以前的宽高值有没有变化,如果有变化就会在设置完值之后去调用onSizeChanged()方法。
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
invalidate(sizeChanged);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
……
}
return changed;
}
所以onSizeChanged()方法是在设置上下左右的坐标值之后,如果宽高有变化就调用,在onlayout()方法之前执行。
onlayout()通常是去摆放子控件的方法,根据自身控件需要去定义子控件如何摆放,一定要在里面调用子控件的layout()进行摆放。通常view是不需要重写这个方法的。
我们调用getWidth()、getHeight()的时候,就一定要在摆放之后才能得到正确的值,大家看看这个两个方法的源码就知道了。
大家可能会有疑问,真正的绘制流程是在onResume()之后开始绘制的。我如果想要获取view的宽高怎么办呢?总的找个合适的地方去获取吧!
我们可以这样来获取控件的宽高,看下面代码,他们都是在执行ViewRootImpl的performTraversals()方法的时候调用的,第一种是在执行draw()之后执行的;第二种是在draw()之前layout()之后执行;第三种方法查看源码没发现它跟view的绘制流程有什么关系,看过前面acitivity的启动流程的同学知道,activity的启动流程是通过主线程handler机制来发起的,而view.post(new Runnable())也是通过主线程的looper来分发处理的。也就是说当activity的启动流程完成之后,view.post()的Runnable才能从消息队列里面拿出来执行,这个时候view的绘制已经完成了。
//第一种
myListView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
myListView.getWidth();
myListView.getHeight();
}
});
//d第二种
myListView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
myListView.getWidth();
myListView.getHeight();
return true;
}
});
//第三种
myListView.post(new Runnable() {
@Override
public void run() {
myListView.getWidth();
myListView.getHeight();
}
});
摆放也就大概这个样子了,我们可以看看FrameLayout和LinearLayout的摆放源码。
3. 绘制
在绘制流程里面绘制应该是最简单的,虽然真正绘制的时候很复杂。在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
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);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// we're done...
return;
}
……
}
绘制分了4步:
- 画背景
- 画自己
- 画子控件
- 画装饰,比如scrollbar
所以画控件的话只需要画自己就行了,在onDraw()把自己要实现的效果画上去就行了。如何画控件就不是本文的内容了