view的绘制顺序是 measure layout draw。但是具体内部实现原理却不是很清楚,今天就对这整套流程来进行源码分析。
1.measure测量流程
a.起点ViewRootImpl中的performMeasure()方法
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
}
这两个参数,childWidthMeasureSpec和childHeightMeasureSpec
分别来源于
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
通过两个参数,得到根view的测量规格。
系统将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec(规格),然后在onMeasure中根据这个MeasureSpec来确定view的测量宽高。
我们通常在xml文件里面 会给布局添加宽高对应的模式,也就是match_parent和wrap_content再或者是自定义的大小,它的作用,告诉父容器,是要根据父容器来适配 还是要根据内容来适配。
先根据对应的模式,再加上对应的值 ,来确定最终的宽高。
总的来说就是:显示模式+实际值,来确定measure的值
高的显示模式 match_parent和值
宽的显示模式match_parent和值
我们再对这个传进来的参数进行分析
mWidth 在构造中就会赋值为 -1,之后具体的赋值暂时不管。
lp.width 这里的lp代表WindowManager.LayoutParams lp = mWindowAttributes;
资源文件,xml的信息,也就是Decorview中默认加载的那个xml
GetRootMeasureSpec()方法实现 规则产生对应的规格对象,onMeasure里面是根据measureSpec来确定高度的。
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;
}
而这个MeasureSpec实际上是有三种规格
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//int是4个字节,8个字符,十六进制就是32位。
//0x代表16进制,16进制的3 ,转换成10进制, 3*16的0次方,还是3。再转换成2进制
//3==>0000 0000 0000 0000 0000 0000 0000 0000 00 11
//左移30, 变成
//110000 0000 0000 0000 0000 0000 0000 0000 00
最高位是符号位,代表是负数,同时,计算机存储的都是补码,所以需要取反,加1。得到原码。
//110000 0000 0000 0000 0000 0000 0000 0000 00 补码
//101111 1111 1111 1111 1111 1111 1111 1111 11 (取反,首位因为是负数,所以不取反,其他取反)
//0 00000 0000 0000 0000 0000 0000 0000 0000 01(加1)
// 1 01111 1111 1111 1111 1111 1111 1111 1111 11
//+ 0 00000 0000 0000 0000 0000 0000 0000 0000 01
//最终的值为 11 0000 0000 0000 0000 0000 0000 0000 0000 00
//最高位是符号位,不参与计算,代表负数,也就是2的三十次方。就是-2的三十次方,得到-1073741824
//这方面基础较差的朋友可以看下《深入理解计算机系统》
//这样做的好处就是二进制计算速度快,而且一个值比两个值占的空间小。
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//自己自定义,最终的值是0
public static final int EXACTLY = 1 << MODE_SHIFT;
//对应match_parent,最终值为2的30次方
public static final int AT_MOST = 2 << MODE_SHIFT;
//对应wrap_content,最终值为2的31次方(得到补码的值)
}
//就是将大小和Spec进行打包。
//实际上这种计算规则就是 模式+具体数值--》规格
//数据格式32位,左两位代表模式,右30代表具体数值
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//这里分别提供解包的mode和size功能
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
得到根布局的规格,对应关系分别是
MeasureSpec.EXACTLY<==> match_parent
MeasureSpec.AT_MOST<==>wrap_content
MeasureSpec.UNSPECIFIED <==>常用于可滑动的view(如listview或者Recycleview等)
说白了也就是一种转换规则
要么跟父容器的大小 match-parent,
要么就是子控件的大小wrapContent,
要么就是自己自定义的大小。
同时这里需要理解,最初的xml的父容器如果是match_parent,它的parent实际上是DecorView默认加载进去的xml。
这个值会给到onMeasure,onMeasure是根据这个规格来确定view的宽高。
就是转换规则+具体大小,才是真实的宽高。
宽高得到交给performMeasure()方法,再会给到 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
从view.measure中会调用onMeasure()方法,内部setMeasureDimension(),该方法用于保存测量结果.这个只是给view来调用的。
总结:
最关键的其实是这个onMeasure(),我们所有的系统组件,无论是LinearLayout还是framelayout,都是重新的onMeasure这个方法而已。本质都是view,测量完之后,再onLayout进行摆放,最后再绘制而已,在onMeasure()中写一些业务代码。
2.layout流程
开始:performLayout(lp, mWidth, mHeight);//资源,宽,高
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {
final View host = mView;//这里的view实际上就是DecorView
if (host == null) {
return;
}
host.layout(0, 0, host.getMeasuredWidth(),
host.getMeasuredHeight());
}
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);//这里调用了onLayout
}
而这个onLayout()方法的实现是个空的。
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
//提供给外部,加业务代码
}
总结:顶层的view调用layout,然后公开onLayout出去让别人重写,添加自身的业务,可以以FrameLayout的onLayout方法为例,内部自己去调用子View的layout方法,然后将子View点算完给他,让他自己去摆放
所有的布局摆放,实际上就是每个view都有个layout去排版,排版会调用onLayout
流式布局来验证这些结论,但是这里只是一个demo,只是用于这些知识点,实际用于项目的 ,还需要另外进行计算。
注意事项
在7.0之后和7.0之前系统对于源码由一定程度的改变
7.0会导致onLayout可能会调用两次。
7.0的performTraversals() 内部有这样一行代码,会导致onMeasure()调用两次
performDraw();
} else {
if (isViewVisible) {
// Try again
scheduleTraversals();//这里又会重新开始绘制流程(需要画一个流程图)
}
ondraw内部又会调用 if (animating)
{
mFullRedrawNeeded = true;
scheduleTraversals();
}
onMeasure,onMeasure,onLayout
onMeasure,onLayout,onMeasure,onLayout
onMeasure会调用两次。因为源码内部流程是重新走了一遍流程,具体是为什么 以及怎么写的,不太清楚。
3.performDraw()流程==>drawSoftware()==>draw()==>ondraw()
draw()方法中的
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;//实际上就是一个画布
final Rect dirty = mDirty;//定一个矩阵,实际上就是在屏幕中去确认绘制范围。
//中间一大堆矩阵大小范围的源码略
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) {
final int left = dirty.left;
final int top = dirty.top;
final int right = dirty.right;
final int bottom = dirty.bottom;
final Canvas canvas;
canvas = mSurface.lockCanvas(dirty);//这里是调用底层C的代码,实际上的画布是Surface,而canvas实际上是类似于一个代理者,决定了图形的位置,形状等特性
mView.draw(canvas);
final Paint p = scrollabilityCache.paint;//paint决定了色彩和样式
}
mView.draw(canvas);才调用了ondraw
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)
*/
绘制的步骤:1.画background
2.如果必要的话,保存canvas,用于渐变
(图形在绘制时,实际上是一层层叠加的,先保存当前层,第二次绘画,就重新在上面叠加一层,
涉及到这样的步骤。所以我们在定位,如果定在某一层,则会把下面的层级给覆盖掉)
3.画view的内容
4.画子view
5.如果必要,绘制渐变恢复
6.绘制装饰(例如滚动条)
// skip step 2 & 5 if possible (common case)
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
//Step 5
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)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
step1:drawBackground()源码分析:
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();//设置背景边框
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);//为原点就开始绘制
} else {
canvas.translate(scrollX, scrollY);//不是原点就回到原点进行绘制
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
step3: onDraw() 源码分析:
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
//实际上就是对外提供方法
}
step4:dispatchDraw()源码分析,绘画子view
/**
* 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的,最典型的就是ViewGroup的体系,如listview
}
step5: drawAutofilledHighlight()
//恢复图层,理解成save和restore就好了。。实际上是涉及到底层的OpenGL以及优化层面的东西,图形的绘制都是GPU来处理的
private void drawAutofilledHighlight(@NonNull Canvas canvas) {
if (isAutofilled()) {
Drawable autofilledHighlight = getAutofilledDrawable();
if (autofilledHighlight != null) {
autofilledHighlight.setBounds(0, 0, getWidth(), getHeight());
autofilledHighlight.draw(canvas);
}
}
}
step 6和7就是 绘制view的装饰
FlowLayout源码分析帮助理解measureSpec