Android UI界面架构
每个Activity包含一个PhoneWindow对象,PhoneWindow设置DecorView为应用窗口的根视图,在里面就是TitleView和ContentView, 平时使用的setContentView()就是设置的ContentView。
View控件
Android中控件基本分为两类View和ViewGroup,ViewGroup作为容器管理View。Android视图,是类似于Dom树的架构,如下图:
如何绘制View
当startActivity启动一个Activity时,会绘制当前Activity的布局。绘制从根视图开始,从上至下遍历整棵视图树,每一个ViewGroup负责让自己的子View被绘制,每一个View负责绘制自己,通过draw()方法,绘制过程分三步走:
- Measure: 测量
- Layout:定位
- Draw:绘制及显示
整个绘制流程是在ViewRoot中的performTraversals()方法展开的。部分源代码如下:
private void performTraversals() {
......
//最外层的根视图的widthMeasureSpec和heightMeasureSpec由来
//lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}
View绘制的流程图如下:
Measure
Measure过程会为一个 View 及所有子节点的 mMeasuredWidth 和 mMeasuredHeight 变量赋值,该值可以通过 getMeasuredWidth()和getMeasuredHeight()方法获得。
onMeasure()方法核心源码如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
//设置View宽高的测量值
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
//measureSpec指的是View测量后的大小
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
//MeasureSpec.UNSPECIFIED一般用来系统的内部测量流程
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//我们主要关注着两种情况,它们返回的是View测量后的大小
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
//如果View没有设置背景,那么返回android:minWidth这个属性的值,这个值可以为0
//如果View设置了背景,那么返回android:minWidth和背景最小宽度两者中的最大值。
protected int getSuggestedMinimumHeight() {
int suggestedMinHeight = mMinHeight;
if (mBGDrawable != null) {
final int bgMinHeight = mBGDrawable.getMinimumHeight();
if (suggestedMinHeight < bgMinHeight) {
suggestedMinHeight = bgMinHeight;
}
}
return suggestedMinHeight;
}
通过源码可以看出,最终的高宽是调用setMeasuredDimension()设定的,如果不重写,默认是直接调用getDefaultSize获取尺寸的。
MeasureSpec是一个32位int值,高2位为测量的模式,低30位为测量的大小。测量的模式可以分为以下三种。
- EXACTLY
精确值模式,当layout_width或layout_height指定为具体数值,或者为match_parent时,系统使用EXACTLY。 - AT_MOST
最大值模式,指定为wrap_content时,控件的尺寸不能超过父控件允许的最大尺寸。 - UNSPECIFIED
不指定测量模式,View想多大就多大。
使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。
Layout
通过onMeasure()方法我们已经知道了View的大小,那么接下来就是确定该View到底在哪个位置显示,这个时候就要调用layout()方法。
public void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
.....
onLayout(changed, l, t, r, b);
.....
}
layout获取四个参数,左,上,右,下坐标,相对于父视图而言。
通过上面performTraversals()方法中的源码:
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
这里可以看到,就是使用了onMeasure()测量的宽和高,作为后面两个参数。
当layout结束以后getWidth()与getHeight()才会返回正确的值。
getWidth/Height() 和 getMeasuredWidth/Height()有什么区别?
下图用于比较形象的展示getHeight()和getMeasuredHeight():
- getHeight(): View在设定好布局后View的高度。
- getMeasuredHeight(): 对View上的內容进行测量后得到的View內容占据的高度。
getWidth()和getMeasuredWidth()与高度一致。
Draw
绘制流程如下:
总结一下步骤其实就是:
- 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
......
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
......
// Step 2, save the canvas' layers
......
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
......
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
......
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
......
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
......
}
Android View常见面试题
- 首次 View 的绘制流程是在什么时候触发的?
- ViewRootImpl 创建的时机?
- ViewRootImpl 和 DecorView 的关系是什么?
- DecorView 的布局是什么样的?
- DecorView 的创建时机?
- setContentView 的流程
- LayoutInflate 的流程
- Activity、PhoneWindow、DecorView、ViewRootImpl 的关系?
- PhoneWindow 的创建时机?
- 如何触发重新绘制?
- requestLayout 和 invalidate 的流程?
- requestLayout 和 invalidate 的区别?