欢迎关注作者的github https://github.com/BudSpore
本系列文章都有点长,首先AndroidUI显示的大致流程,并结合项目对视图,流畅度进行性能优化,最后整合一个大项目,认真读完,您一定有所收获。
Android视图显示整体流程是先绘制(CPU的工作)再渲染(GPU的工作),本篇文章先讲CPU做的部分事情,即View绘制,该文章源码都在Java层。
文章目录
下面是Android屏幕组成架构
Android屏幕架构
下面对图中以及Android视图里的概念进行详细的介绍
DecorView
DecorView是一个应用窗口的根容器,它是整个页面的根View,也是一个FrameLayout布局,DecorView有2个子View,第一个子View就是屏幕这个部分,看图:
第二个子View是一个垂直LinearLayout,包含两个子元素,一个是TitleView(ActionBar的容器),它就是我们看到的标题栏,另一个是ContentView(窗口内容的容器)。
ContentView是一个FrameLayout(android.R.id.content),
我们在Activity的生命周期的起点——onCreate回调方法里(如下图)使用setContentView就是成了它的子View。每个Activity都与一个PhoneWindow相关联,用户界面则由PhoneWindow所承载。注意,setContentView方法最后会调用到绘制的起点函数,后面将详细讲解哦~
Canvas(给View用的画布)
Canvas位于Java层,ViewGroup会把自己的Canvas拆分给子View。View会在onDraw方法里将图形数据绘制在它获得的Canvas上,在View类里通过建立这个Canvas类来构建绘画的基础,把Canvas想象成一个画布,笔(Paint)把View画在画布上,然后再把所有数据传递给Surface(下篇文章会讲到),Canvas是View树和Surface的连接点,android中的UI元素通过调用Canvas类来构建的。
Canvas类处理“draw“的调用,当绘制(draw)内容时需要4个基本组件,保持像素的Bitmap,处理绘制调用 的canvas(写入Bitmap,bitmap用于存储surface内容),绘制 的内容(如,Rect,Path,text,Bitmap)和一个笔paint(描述颜色和样式)。
Window
应用通过 Window Manager创建一个window,Window Manager 为每一个window创建一个surface,并把该surface传递给应用以便应用在上面绘制,一个应用有很多Window。
Window是屏幕上用于绘制各种UI元素(比如Button,TextView)及响应用户输入事件(键盘事件)的一个矩形区域,它独立绘制,不与其他界面产生影。在Android系统中,每个Window是独占一个Surface实例的显示区域,每个窗口的Surface由WindowManagerService分配。
每个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系,对于Activity来说,ViewRootImpl是连接WindowManager和DecorView的纽带,绘制的入口是由ViewRootImpl的performTraversals方法来发起Measure,Layout,Draw等流程的,当然performTraversals函数调用之前也有很多方法要做,这里不仔细分析。
ViewRoot
从上一段得知,WindowManagerGlobal里面有一个mRoots,也就是ViewRoot,它主要用来管理窗口的根 view,并跟 WindowManagerService 交互。
WindowManager
管理窗口的一些状态属性(view 的增加,删除,窗口位置,更新等等),它管理window和WindowManagerService交互,由ViewRoot完成。主要的方法有:addview(),updateViewLayout();removeView()。
WindowManager是一个接口,WindowManagerImpl 是它的实现类。
WindowManager 里面维护了三个变量:mViews(根view),mRoots(viewroot),mParams(一些相关变量),由 WindowManagerGlobal 维护它们。
WindowManagerService
对系统中的所有窗口进行管理,它和WindowManager区别是WindowManager管理的是单独的窗口,而WindowManagerService需要管理所有的窗口。
Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时创建 ViewRootImpl 对象并将ViewRootImpl和 DecorView 建立关联。
PhoneWindow
它是Activity和整个View系统交互的接口,是Window的具体实现。
MeasureSpac(规格测量)
下面的测量过程用到了MeasureSpac,MeasureSpec是一个int类型的值,由高2位的规格模式和低30位的具体尺寸,由父View的MeasureSpec和子View的LayoutParams(LayoutParams就是我们在xml写的时候设置的layout_width和layout_height 转化而来的)通过计算得出一个针对子View的测量规格。
好了,枯燥的概念讲完了,下面结合一个实例来详细讲解Android视图绘制的过程。
案例实战
看下面的activity_main的XML代码
效果如图所示
分析上面的视图:
View树的整体结构:
为了方便,我把header设置为Gone,去掉ActionBar的测量过程
View的绘制分为measure、layout、draw 过程,绘制的View节点从根节点——DecorView开始。 绘制的起点就是上面讲的performTraversals函数:
函数做的执行过程主要是根据之前设置的状态,判断是否重新计算视图大小(measure)、是否重新放置视图的位置(layout)、以及是否重绘 (draw),其核心也就是通过判断来选择顺序执行这三个方法中的哪个.
绘制的起点
private void performTraversals() {
......
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
//mWidth和mHeight代表屏幕的宽高,lp是WindowManager.LayoutParams,它的lp.width和lp.height的默认值是MATCH_PARENT,
......
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
......
performDraw();
......
=—= 这个函数太长了
}
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 = View.MeasureSpec.makeMeasureSpec(windowSize, View.MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = View.MeasureSpec.makeMeasureSpec(windowSize, View.MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = View.MeasureSpec.makeMeasureSpec(rootDimension, View.MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
performTraversals传入参数后,getRootMeasureSpec函数里的switch case(张老师反复强调switch---case是一条语句)走的是MATCH_PARENT,使用MeasureSpec.makeMeasureSpec方法组装一个MeasureSpec,MeasureSpec的specMode等于EXACTLY,specSize等于windowSize,屏幕的宽高,也就是为何根视图总是全屏的原因。
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);
// }
}
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
因为DecorView 是一个FrameLayout 那么接下来会进入FrameLayout 的measure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
....
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//基本思想就是父View把自己的MeasureSpec
// 传给子View结合子View自己的LayoutParams 算出子View 的MeasureSpec,然后继续往下传,
// 传递叶子节点,叶子节点没有子View,根据传下来的这个MeasureSpec测量自己就好了。
//注意DecorView的子节点(View)有2个。
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
....
....
}
}
.....
.....
//所有的孩子测量之后,经过一系类的计算之后通过setMeasuredDimension设置自己的宽高,
//对于FrameLayout 可能用最大的字View的大小,对于LinearLayout,可能是高度的累加,
//具体测量的原理去看看源码。总的来说,父View是等所有的子View测量结束之后,再来测量自己。
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
....
}
DecorView的第一个子节点是ViewRoot,它继承自LinearLayout,LinearLayout继承自ViewGroup,(跟你在XML里写的LinearLayout布局没关系)所以后面的measure函数调用的是LinearLayout类里的
再次附上View树
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//根据父View的测量规格和父View自己的Padding,
// 还有子View的Margin和已经用掉的空间大小(widthUsed),就能算出子View的MeasureSpec
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);
//通过父View的MeasureSpec和子View的自己LayoutParams的计算,算出子View的MeasureSpec,然后父容器传递给子容器的
// 然后让子View用这个MeasureSpec(一个测量要求,比如不能超过多大)去测量自己,如果子View是ViewGroup 那还会递归往下测量。
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
大家注意下这个:
父节点得到自己的MeasureSpac后,调用 child.measure 方法(因为它是 FrameLayout 只有一个直接子类),传入自己的MeasureSpac,开始测量子节点的MeasureSpac。
如果child 是 viewgroup ,测量流程基本上如下:得到父 view 的MeasureSpac,算出自己的MeasureSpac,调用 measureChildren,多次调用 measureChild,进而调用 child.measure
如果child 是 view,测量流程基本上如下:得到父 view 的 MeasureSpec,算出自己的MeasureSpac,设置自己的宽高(setMeasureDimension),并结束测量。
这样就讲清楚了是如何得到MeasureSpac的了。
而怎么得到子View的MeasureSpac,请看下面的图(省略了UPSPECIFIED模式: 父容器对于子容器没有任何限制,子容器想要多大就多大):
// spec参数 表示父View的MeasureSpec
// padding参数 父View的Padding+子View的Margin,父View的大小减去这些边距,才能精确算出
// 子View的MeasureSpec的size
// childDimension参数 表示该子View内部LayoutParams属性的值(lp.width或者lp.height)
// 可以是wrap_content、match_parent、一个精确指(an exactly size),
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec); //获得父View的mode
int specSize = MeasureSpec.getSize(spec); //获得父View的大小
//父View的大小-自己的Padding-子View的margin,得到值才是子View的大小。
int size = Math.max(0, specSize - padding);
int resultSize = 0; //初始化值,最后通过这个两个值生成子View的MeasureSpec
int resultMode = 0; //初始化值,最后通过这个两个值生成子View的MeasureSpec
switch (specMode) {
// Parent has imposed an exact size on us
//1、父View是EXACTLY的 !
case MeasureSpec.EXACTLY:
//1.1、子View的width或height是个精确值 (an exactly size)
/*
子view是确定值,那么控件最后展示就是那个具体的值,与父View无关
*/
if (childDimension >= 0) {
resultSize = childDimension; //size为精确值
resultMode = MeasureSpec.EXACTLY; //mode为 EXACTLY 。
}
//1.2、子View的width或height为 MATCH_PARENT/FILL_PARENT
//如果一个View的MeasureSpec 是EXACTLY,那么它的size 是多大,最后展示到屏幕就一定是那么大
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size; //size为父视图大小
resultMode = MeasureSpec.EXACTLY; //mode为 EXACTLY 。
}
//1.3、子View的width或height为 WRAP_CONTENT
/*
我们还不知道具体子View的大小是多少,
要等到child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 调用的时候
才去真正测量子View 自己content的大小
*/
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size; //size为父视图大小
resultMode = MeasureSpec.AT_MOST; //mode为AT_MOST 。
}
break;
// Parent has imposed a maximum size on us
//2、父View是AT_MOST的 !
case MeasureSpec.AT_MOST:
//2.1、子View的width或height是个精确值 (an exactly size)
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension; //size为精确值
resultMode = MeasureSpec.EXACTLY; //mode为 EXACTLY 。
}
//2.2、子View的width或height为 MATCH_PARENT/FILL_PARENT
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; //size为父视图大小
resultMode = MeasureSpec.AT_MOST; //mode为AT_MOST
}
//2.3、子View的width或height为 WRAP_CONTENT
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size; //size为父视图大小
resultMode = MeasureSpec.AT_MOST; //mode为AT_MOST
}
break;
// Parent asked to see how big we want to be
//3、父View是UNSPECIFIED的 !
case MeasureSpec.UNSPECIFIED:
//3.1、子View的width或height是个精确值 (an exactly size)
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension; //size为精确值
resultMode = MeasureSpec.EXACTLY; //mode为 EXACTLY
}
//3.2、子View的width或height为 MATCH_PARENT/FILL_PARENT
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = 0; //size为0! ,其值未定
resultMode = MeasureSpec.UNSPECIFIED; //mode为 UNSPECIFIED
}
//3.3、子View的width或height为 WRAP_CONTENT
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0; //size为0! ,其值未定
resultMode = MeasureSpec.UNSPECIFIED; //mode为 UNSPECIFIED
}
break;
}
//根据上面逻辑条件获取的mode和size构建MeasureSpec对象。
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec)会调用到LinearLayout的onMeasure函数,我们来看源码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);//垂直测量
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);//水平测量
}
}
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
...
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
....
//这个函数会调用measureChildWithMargins,然后调用child.measure,因为我去掉了ActionBar,
//所以ViewRoot的子View是content(FrameLayout布局),
//child.measure进入FrameLayout的onMeasure函数
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
...
//所有的孩子测量之后,经过一系类的计算之后通过setMeasuredDimension设置自己的宽高,
//对于LinearLayout自己的测量,可能是高度的累加
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
}
ViewRoot 是系统的View,它的LayoutParams默认都是match_parent,根据我们文章最开始MeasureSpec 的计算规则,ViewRoot 的MeasureSpec mode应该等于EXACTLY(DecorView MeasureSpec 的mode是EXACTLY,ViewRoot的layoutparams 是match_parent),size 也等于DecorView的size
对于content的lp.width lp.height都是系统设定的match_parent
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
.....
for (int i = 0; i < count; i++) {
...关键代码
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
...
}
}
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
.....
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed,
lp.height);
....
}
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
...
int specSize = MeasureSpec.getSize(spec); //获得父View的大小
int size = Math.max(0, specSize - padding);
...
resultSize = size;//父View的大小
resultMode = MeasureSpec.EXACTLY;
...
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
由于ViewRoot 的mPaddingBottom=100px(这个可能和状态栏的高度有关,我们测量的最后会发现id/statusBarBackground的View的高度刚好等于100px,ViewRoot 是系统的View的它的Padding 我们没法改变,所以通过 Math.max(0, specSize - padding)计算出来Content(android.R.id.content) 的MeasureSpec 的高度用屏幕高度减去100px (假设结果是2460),它的宽高的mode 根据算出来也是EXACTLY(ViewRoot 是EXACTLY和android.R.id.content 是match_parent)。所以Content(android.R.id.content)的MeasureSpec高度少了100px。
接下来测量content的子节点,即XML里的LinearLayout
同理,父View(content)的MeasureSpec+子View(linear)的LayoutParams就得出了子View的宽高,这里注意的是XML里设置了margintop=200px,所以通过 Math.max(0, specSize - padding)让linear的高度用减少200px
1、id/linear的heightMeasureSpec 的mode=AT_MOST,因为id/linear 的LayoutParams 的layout_height="wrap_content"
2、id/linear的heightMeasureSpec 的size 少了200px, 由上面的代码
padding=mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed;
int size = Math.max(0, specSize - padding);
由于id/linear 的 android:layout_marginTop="50dp" 使得lp.topMargin=200px (本设备的density=4,px=4*pd),在计算后id/linear的heightMeasureSpec 的size 少了200px。(布局代码前面已给出,可自行查看id/linear 控件xml中设置的属性)
同理,对于TextView来说,linear的paddingButtom=70dp,70×4=280px,对于TextView来说,paddingTop=280,看图:
然后遍历到WebView,高度是一个确定值,所以最后的高度就是其对应值
附整体的MeasureSpec传递过程
当遍历到叶子节点的时候,就测量叶子节点自身,TextView继承自View,就调用View类的onMeasure,WebView继承自AbsoluteLayout,就调用AbsoluteLayout的onMeasure
id/linear 的子View的高度都计算完毕了,接下来id/linear就通过所有子View的测量结果计算自己的高宽,id/linear是LinearLayout,它的高度是子View的高度总和+自身padding.
最终算出id/linear出来后,id/content 就要根据它唯一的子View id/linear 的测量结果和自己的之前算出的MeasureSpec一起来测量自己的结果,
在FrameLayout的for循环退出后,测量自己
而对于ViewRoot来讲,它是LinearLayout,for循环也结束了
接下来测量ViewRoot,刚才讲到,它的测量就是子view的累加和和自己的padding,过程在onMeasure函数里
然后再测量id/statusBarBackground,虽然不知道id/statusBarBackground 是什么,但是调试的过程中,测出的它的高度=100px, 和 id/content 的paddingTop 刚好相等。在最后DecorView退出for循环,测量自己 的高宽最终整个测量过程结束。所有的View的大小测量完毕。所有的getMeasureWidth 和 getMeasureWidth 都已经有值了。
DecorView添加到窗口的过程:(感兴趣可以了解一下,这不是重点)
布局的基本思想也是由根View开始,递归地完成整个视图树的布局工作。
接下来进入的是:
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
别忘了View树
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
...
final View host = mView;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}
我们把对decorView的layout()方法的调用作为布局整个控件树的起点,实际上调用的是View类的layout()方法,源码如下:
/**
* l为本View左边缘与父View左边缘的距离
t为本View上边缘与父View上边缘的距离
r为本View右边缘与父View左边缘的距离
b为本View下边缘与父View上边缘的距离
*/
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;
}
...
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);
...
}
}
setFrame(l, t, r, b) 为mLeft 、mTop、mRight、mBottom赋值,然后基本就能确定View自己在父视图的位置了,这几个值构成的 矩形区域就是该View显示的位置,当初始化完毕后,ViewGroup的布局流程也就完成了,这里的具体位置都是相对与父视图的位置,然后在setFrame()方法中会判断View的位置是否发生了改变,若发生了改变,则需要对子View调用onLayout()方法进行重新布局,由于普通View( 非ViewGroup)不含子View,View类的onLayout()方法为空。ViewGroup的onLayout函数是抽象方法,每个子类实现它的方式都不相同。
View类的onLayout函数
这是一个空实现,主要作用是在我们的自定义View中重写该方法,实现自定义的布局逻辑。
我们以decorView,也就是FrameLayout的onLayout()方法为例,它继承自ViewGroup,要重写onLayout函数,那我们就根据它来分析ViewGroup的布局过程:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//把父容器的位置参数传递进去
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
/**根据上面函数
* 传入的四个值分别是0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()
*DecorView的左上位置为0,然后宽高为它的测量宽高
*/
void layoutChildren(int left, int top, int right, int bottom,
boolean forceLeftGravity) {
final int count = getChildCount();
/*
parentLeft 表示当前View为其子View显示区域指定的一个左边界,
即子View显示区域的左边缘到父View的左边缘的距离,它由父容器的padding和Foreground决定
*/
final int parentLeft = getPaddingLeftWithForeground();
/*
parentRight 表示当前View为其子View显示区域指定的一个右边界,
即子View显示区域的右边缘到父View的右边缘的距离
*/
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
//以上四个值确立子View的显示区域
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;//子View的左边缘距父View左边缘的距离
int childTop;//子View的上边缘距父View上边缘的距离
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)
//若子View是容器View,则会递归地对其子View进行布局。
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
上面的源码涉及到android:layout_gravity属性,记住,你的orientation设置为horizontal,横向排列则只能竖向居中,当你设置为vertical竖向排列则只能横向居中。(LinearLayout如果设置 android:orientation="vertical",那么android:layout_gravity的设置只在水平方向生效,android:orientation="horizontal",那么android:layout_gravity属性只在垂直方向生效)
比如我举一个例子
显示效果如图
回到我们的主话题中来,以上就是首先先获取父容器的padding值,然后遍历其每一个子View,根据子View的layout_gravity属性、子View的测量宽高、父容器的padding值、来确定子View的布局参数,然后调用child.layout方法,把布局流程从父容器传递到子元素。如果子View是一个ViewGroup,那么就会重复以上步骤,如果是一个View,那么会直接调用View的layout方法,根据以上分析,在该方法内部会设置view的四个布局参数,接着调用onLayout的空方法。
在for循环过程中,会布局到LinearLayout,child.layout函数会调用到LinearLayout类的onLayout函数,我给出源码
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
//获得子视图可以用的宽度,顺便也把子视图左边沿的位置计算出来。
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
//根据父视图中的gravity属性,决定子视图的起始位置。
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
//遍历所有子视图,为它们分配位置
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
//还会调用child.layout()为子视图设置布局位置.
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
布局到TextView,同样会调用到TextView类的onLayout方法
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mDeferScroll >= 0) {
int curs = mDeferScroll;
mDeferScroll = -1;
bringPointIntoView(Math.min(curs, mText.length()));
}
}
同理WebView也布局完毕,上面的layoutChildren函数里的for循环结束,所有View就布局完毕。
绘制第三步——绘制
到了View绘制的最后一步
performDraw();
private void performDraw() {
...
final boolean fullRedrawNeeded = mFullRedrawNeeded;
//fullRedrawNeeded参数判断是否需要重新绘制全部视图,
//如果是第一次绘制视图,那么绘制所有的视图,
mFullRedrawNeeded = false;
//绘画逻辑都在这个函数里实现
draw(fullRedrawNeeded);
....
if (mSurfaceHolder != null && mSurface.isValid()) {
mSurfaceHolderCallback.surfaceRedrawNeeded(mSurfaceHolder);
SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
if (callbacks != null) {
for (SurfaceHolder.Callback c : callbacks) {
if (c instanceof SurfaceHolder.Callback2) {
((SurfaceHolder.Callback2)c).surfaceRedrawNeeded(
mSurfaceHolder);
}
}
}
}
....
}
/*
*/
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
...
final Rect dirty = mDirty;//表示重绘区域
if (mSurfaceHolder != null) {
// The app owns the surface, we won't draw.
dirty.setEmpty();
if (animating) {
if (mScroller != null) {
mScroller.abortAnimation();
}
disposeResizeBuffer();
}
return;
}
...
//如果fullRedrawNeeded为真,则把dirty区域置为整个屏幕,表示整个视图都需要绘制
//第一次绘制流程,需要绘制所有视图
if (fullRedrawNeeded) {
mAttachInfo.mIgnoreDirtyState = true;
dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty))
{
return;
}
...
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
// Draw with software renderer.
final Canvas canvas;
...
//还记得文章开始讲的Surface的工作流程吗
/*- create a bitmap
- attach a canvas to it
- do the rendering into that canvas
- lockCanvas
- draw your bitmap into the backbuffer
- unlockAndPost
*/
canvas = mSurface.lockCanvas(dirty);
...
if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
...
canvas.translate(-xoff, -yoff);
...
canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
attachInfo.mSetIgnoreDirtyState = false;
//decorView就是起点,也就是View.draw()方法
mView.draw(canvas);
...
surface.unlockCanvasAndPost(canvas);
...
}
终于到了绘画的逻辑实现函数了
public void draw(Canvas canvas) {
/*我们首先来看一开始的标记位dirtyOpaque,该标记位的作用是判断当前View是否是透明的,
如果View是透明的,那么根据下面的逻辑可以看出,将不会执行一些步骤,
比如绘制背景、绘制内容等。这样很容易理解,
因为一个View既然是透明的,那就没必要绘制它了
*/
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;
/*
* 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
...
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);
// 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);
// we're done...
return;
}
/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
...
}
我对上面的步骤翻译一下
1.对View的背景进行绘制
2.保存当前的图层信息(可跳过)
3.绘制View自身的内容
4.对View的子View进行绘制(如果有子View)
5.绘制View的褪色的边缘,类似于阴影效果(可跳过)
6.绘制View的滚动条
第一步是绘画背景
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;//mBackground是该View的背景参数,比如背景颜色
if (background == null) {
return;
}
//mRight - mLeft, mBottom - mTop layout确定的四个点来设置背景的绘制区域
setBackgroundBounds();
// Attempt to use a display list if requested.
if (canvas.isHardwareAccelerated() && mAttachInfo != null
&& mAttachInfo.mHardwareRenderer != null) {
mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
final RenderNode renderNode = mBackgroundRenderNode;
if (renderNode != null && renderNode.isValid()) {
setBackgroundRenderNodeProperties(renderNode);
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
return;
}
}
//获取当前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);
//调用Drawable的draw() 把背景图片画到画布上
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
跳过第2步
第3步,绘画自己View内容
onDraw(canvas) 方法是view用来draw 自己的,具体如何绘制,颜色线条什么样式就需要子View自己去实现,View类的onDraw(canvas) 是空实现,ViewGroup 也没有实现,每个View的内容是各不相同的,所以需要由子类去实现具体逻辑。
对于decorView来说,由于它是容器View,所以它本身并没有什么要绘制的,关键在下面.
第4步,绘画当前View的子View
对于View类来讲,它就是叶子节点,没有子节点,所以它的dispatchDraw(canvas)空
所以调用的是ViewGroup类的dispatchDraw(canvas)方法
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) {
//关键代码,依次调用drawChild()方法来绘制子View
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);
}
}
...
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
drawChild()的核心过程就是为子视图分配合适的cavas剪切区,剪切区的大小正是由layout过程决定的,而剪切区的位置取决于滚动值以及子视图当前的动画。设置完剪切区后就会调用子视图的draw()函数进行具体的绘制了。
首先对canvas进行了一系列变换,以变换到将要被绘制的View的坐标系下。完成对canvas的变换后,便会调用View.draw(Canvas)方法进行实际的绘制工作,此时传入的canvas为经过变换的,在将被绘制View的坐标系下的canvas。
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...
/*首先判断是否已经有缓存,即之前是否已经绘制过一次了,
如果没有,则会调用draw(canvas)方法,开始正常的绘制,即上面所说的六个步骤,
否则利用缓存来显示。
*/
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);
}
}
}
这一步就是ViewGroup绘制过程,它对子View进行了绘制,而子View又会调用自身的draw方法来绘制自身,这样不断遍历子View及子View的不断对自身的绘制,从而使得View树完成绘制。
跳过第5步
第六步,绘画View的滚动条
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的绘制过程就完成了,
感谢UIT学长对文章的审阅,我对文章的细节再次进行了删减优化,特别感谢老姐对文章的纠错,作者将不断完善该篇文章,希望读者能提出宝贵意见。
下一篇文章我将会讲解AndroidUI渲染。