Activity 的组成结构
Activity对于安卓开发来说,是熟悉的不能再熟悉的,它是安卓四大组件之一,用来做界面显示用的,那么我相信,并不是所有的朋友都对Activity的组成结构有清晰的认识,这里简单聊聊Activity的组成。
实际界面展示的是Activity中的Window来完成的,Activity中有一个PhoneWindow对象,它继承自Window,里面有一个顶级的RootView 叫DecorView,DecorView 是我们创建的所有的View的根,DecorView 由三部分组成 ActionBar、标题区和内容区,其中内容区是我们用的比较多的,比如我们可以通过android.R.id.content 获取到内容区,是一个FrameLayout。
DecorView 继承自FrameLayout,我们在Activity 中调用SetContentView(layoutResID)
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
其实就是传递给PhoneWindow
362 @Override
363 public void More ...setContentView(int layoutResID) {
364 // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
365 // decor, when theme attributes and the like are crystalized. Do not check the feature
366 // before this happens.
367 if (mContentParent == null) {
368 installDecor();
369 } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
370 mContentParent.removeAllViews();
371 }
372
373 if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
374 final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
375 getContext());
376 transitionTo(newScene);
377 } else {
378 mLayoutInflater.inflate(layoutResID, mContentParent);
379 }
380 final Callback cb = getCallback();
381 if (cb != null && !isDestroyed()) {
382 cb.onContentChanged();
383 }
384 }
mContentParent 就是android.R.id.content 表示的那个内容区容器。最终我们setContentView 传进来的View就添加到了mContentParent 上面。
这张图可以形象表面,上面所说的各部分关系:
Activity 类似于一个框架,负责容器生命周期及活动,窗口通过 Window 来管理;
Window 负责窗口管理(实际是子类 PhoneWindow),窗口的绘制和渲染交给 DecorView完成;
DecorView 是 View 树的根,开发人员为 Activity 定义的 layout 将成为 DecorView 的子视图 ContentParent 的子视图;
layout.xml 是开发人员定义的布局文件,最终 inflate 为 DecorView 的子组件;
另外PhoneWinodw 中有两个比较重要的对象WindowManager和ViewRootImpl。
WindowManager 处理触摸事件,ViewRootImpl去完成UI的绘制。
View的绘制
ViewRootImpl 负责 Activity 整个 UI 的绘制,而绘制是 ViewRootImpl 的
performTraversals()方法。
里面有三个方法:
performMeasure():测量组件的大小;
performLayout():用于子组件的定位(放在窗口的什么地方);
performDraw():将组件的外观绘制出来;
测量组件
performMeasure()方法负责组件自身尺寸的测量,我们知道,在 layout 布局文件中,每一个
组件都必须设置 layout_width 和 layout_height 属性。
属性值有三种可选模式:wrap_content、match_parent 和数值,
performMeasure()方法根据设置的模式计算出组件的宽度和高度。大多数情况下模式为 match_parent 和数值的时候是不需要计算的,传过来的就是父容器自己计算好的尺寸或是一个指定的精确值,只有当模式为 wrap_content 的时候才需要根据内容进行尺寸的测量。
private void performMeasure(int childWidthMeasureSpec, int
childHeightMeasureSpec) {
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
}
}
performMeasure方法测量组件调用了View的 measure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
……
onMeasure(widthMeasureSpec, heightMeasureSpec);
……
}
onMeasure()一般我们需要重写这个方法去给View的子类指定宽度和高度。
如果测量的是容器的尺寸,而容器的尺寸又依赖于子组件的大小,所以必须先测量容器中子
组件的大小,不然,测量出来的宽度和高度永远为 0
确定组件的位置
performLayout()方法用于确定子组件的位置,所以,该方法只针对 ViewGroup 容器类。作为容器,必须为容器中的子 View 精确定义位置和大小。
private void performLayout(WindowManager.LayoutParams lp,
int desiredWindowWidth, int desiredWindowHeight){
……
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
……
for (int i = 0; i < numValidRequests; ++i) {
final View view = validLayoutRequesters.get(i);
view.requestLayout();
}
}
代码中的 host 是 View 树中的根视图(DecroView),也就是最外层容器,容器的位置安排在左上角(0, 0),其大小默认会填满 mContentParent 容器。我们重点来看一下 layout()方法的源码
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);
……
}
……
}
在 layout()方法中,在定位之前如果需要重新测量组件的大小,则先调用 onMeasure()方法,接下来执行 setOpticalFrame()或 setFrame()方法确定自身的位置与大小,此时只是保存了相关的值,与具体的绘制无关。随后,onLayout()方法被调用
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
onLayout()方法在这里的作用是当前组件为容器时,负责定位容器中的子组件
绘制组件
performDraw()方法执行组件的绘制功能,组件绘制是一个十分复杂的过程,不仅仅绘制组
件本身,还要绘制背景、滚动,绘制流程:
private void performDraw() {
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
mIsDrawing = true;
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
}
……
}
在 performDraw()方法中调用了 draw()方法:
private void draw(boolean fullRedrawNeeded) {
……
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
……
}
draw()方法又调用了 drawSoftware()方法:
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
……
final Canvas canvas;
final int left = dirty.left;
final int top = dirty.top;
final int right = dirty.right;
final int bottom = dirty.bottom;
canvas = mSurface.lockCanvas(dirty);
if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
mView.draw(canvas);
surface.unlockCanvasAndPost(canvas);
……
}
drawSoftware()方法中调用了 mView 的 draw()方法,前面说过,mView 是 Activity 界面中 View树的根(DecroView),也是一个容器(具体来说就是一个 FrameLayout 布局容器)FrameLayout 类的 draw()方法源码:
public void draw(Canvas canvas) {
super.draw(canvas);
……
final Drawable foreground = mForeground;
foreground.draw(canvas);
}
FrameLayout 类的 draw()方法做了两件事,一是调用父类的 draw()方法绘制自己,二是将前景位图画在了 canvas 上。自然,super.draw(canvas)语句是我们关注的重点,FrameLayout 继承自ViewGroup,遗憾的是 ViewGroup 并没有重写 draw()方法,也就是说,ViewGroup 的绘制完全重用了他的父类 View 的 draw()方法,不过,ViewGroup 中定义了一个名为 dispatchDraw()的方法,该方法在 View 中定义,在 ViewGroup 中实现,至于有什么用,暂且卖个关子,我们先扒开 View的 draw()方法源码看看:
public void draw(Canvas canvas) {
background.draw(canvas);
if (!dirtyOpaque) onDraw(canvas);
dispatchDraw(canvas);
onDrawScrollBars(canvas);
}
View 类的 draw()方法是组件绘制的核心方法,主要做了下面几件事:
绘制背景:background.draw(canvas)
绘制自己:onDraw(canvas)
绘制子视图:dispatchDraw(canvas)
绘制滚动条:onDrawScrollBars(canvas)
background 是一个 Drawable 对象,直接绘制在 Canvas 上,并且与组件要绘制的内容互不干扰,很多时候,这个特征能被某些场景利用,比如后面的“刮刮乐”就是一个很好的范例。
View 只是组件的抽象定义,他自己并不知道自己是什么样子,所以,View 定义了一个空方法 onDraw(),源码如下:
protected void onDraw(Canvas canvas) {
}
和前面讲过的 onMeasure()与 onLayout()一样,onDraw()方法同样是预留给子类扩展的功能
接口,用于绘制组件自身。组件的外观由该方法来决定。
dispatchDraw()方法也是一个空方法,源码如下:
protected void dispatchDraw(Canvas canvas) {
}
该方法服务于容器组件,容器中的子组件必须通过 dispatchDraw()方法进行绘制,所以,View虽然没有实现该方法但他的子类 ViewGroup 实现了该方法:
protected void dispatchDraw(Canvas canvas) {
……
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
……
}
在 dispatchDraw()方法中,循环遍历每一个子组件,并调用 drawChild()方法绘制子组件,而子组件又调用 View 的 draw()方法绘制自己:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
组件的绘制也是一个递归的过程,说到底 Activity 的 UI 界面的根一定是容器,根容器绘制
结束后开始绘制子组件,子组件如果是容器继续往下递归绘制,否则将子组件绘制出来……直所有的组件正确绘制为止。
总体来说,UI 界面的绘制从开始到结束要经历几个过程:
测量大小,回调 onMeasure()方法
组件定位,回调 onLayout()方法
组件绘制,回调 onDraw()方法
概括描述组件的绘制过程就是,首先通过PerformMeasure方法测量组件的大小,接PerformLayout来定位组件的位置,最后调用PerformDraw方法来绘制组件。
谢谢认真观读本文的每一位小伙伴,衷心欢迎小伙伴给我指出文中的错误,也欢迎小伙伴与我交流学习。
欢迎爱学习的小伙伴加群一起进步:230274309