摘要 : 初学者一枚,每一次写的时候,都是从网上直接 复制+粘贴。。完全不为什么要这样写,为什么通过这样就可以实现,也想着要去看懂。可是看一下就不知道怎么下手。完全是一个球形,无法下手。有一些通过各种搜索后,能够知道了表皮意思,可以接下来自己动手的时。就又忘记了怎么下手。希望各位大大介绍一下大致的方向,再此感激不尽。后面考虑一下还是直接做笔记吧!每一去搜索慢慢的收藏也多了。完全找不到自己所需要的在哪里去了,又得重新搜索…
文摘摘抄至:株洲新城 IT教育 李赞红老师。非常感谢老师。想过摘抄一边的方式去让自己记住一些知识点。如果不适合,我会立刻删除
第一章 View的绘图流程
1.1、概述
Android中组件必须是View直接子类或间接子类,其中View有一个名为ViewGroup的子类。用于定义容器组件类(FrameLayout、LinearLayout都是是ViewGroup的子类)。二者的职责定义非常清晰,如果组件中还有子组件。就一定是从ViewGroup类继承,否则从View类继承。View 类定义了组件相关的通用功能,并打通了组件在Activity整个活动周期中的绘制流程和效果等任督二脉。通过 OOP构建出基本的运行框架。
1.2、Activity 的组成结构
Activity 代表一个窗口,事实上,这里的“窗口”是由 Activity的成员变量 mWindow来表示的,mWindow本质上是一个PhoneWindow 对象。PhoneWindow继承至 Window抽象类。负责窗口的管理。但是,PhoneWindow 并不是用来呈现界面效果,呈现界面是由 PhoneWindow 管理的 DecorView对象来完成的。DecorView 类是 FrameLayout的子类,也是整个 View树的“根”。DecorView由三部分构成:ActionBar、标题区和内容区。在 源码 platforms/android-21/data/res/layout 的目录下有一个名为 screen_title.xml 的布局文件,该布局文件是常见的窗口风格定义文件,打开后可以看到如下定义:<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:fitsSystemWindows="true">
<!-- Popout bar for action modes -->
<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:layout_width="match_parent"
android:layout_height="?android:attr/windowTitleSize"
style="?android:attr/windowTitleBackgroundStyle">
<TextView android:id="@android:id/title"
style="?android:attr/windowTitleStyle"
android:background="@null"
android:fadingEdge="horizontal"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<FrameLayout android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
从代码真看出,ActionBar 由 ViewStub标签定义,内容区包含了两个 FrameLayout标签,分别代表标题栏和正文区。id为 @android:id/content 的FrameLayout被 inflate 成名为 mContentParent 的FrameLayout对象。在 Activity的 onCreate() 方法中调用 setContentView方法加载的布局终将转化成 mContentParent 的子 View。下图所示描述上面各组件之间的关系:
从上图可以看出:
- Activity 类似于一个框架,负责容器的生命周期及活动,窗口通过 Window 来管理
- Window 负责窗口管理(实际是子类 PhoneWindow),窗口的绘制和渲染交给 DecorView完成
- DecorView 是 View的数的根,我们为 Activity定义的 layout 将转成 DecorView的子类视图 ContentParent 的子视图
- layout.xml是我们定义的布局文件,最终 inflate为 DecorView的子组件
需要说明的是,PhoneWindow 类还关联一个 名为 mWindowManager 的 windowmanager对象,windowmanager 会创建一个 ViewRootImpl 对象来和 WindowManagerService 进行沟通,windowmanagerservice 能获取触摸事件、键盘事件和轨迹球事件,并通过 ViewRootImpl 将事件分发给各个 Activity。另外,ViewRootImpl 还负责Activity 整个 GUI的绘制。
下图所示是 Activity涉及到各个组件的关系图(来源于网络)
1.3、View 树的绘制流程
上文提到,ViewRootImpl负责 Activity整个 GUI的绘制,而绘制是从 ViewRootImpl的 performTraversals()方法开始。该方法是由 private 修饰,控制着 View树的绘制流程,禁止被重写。通过查看 ViewRootImpl类,在performTraversals()中,提取出三行关键代码
private void performTraversals(){
......
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
......
performDraw();
......
}
- performMeasure()方法测量组件的大小
- performLayout()方法用于子组件的定位(放在窗口的什么位置)
- performDraw()方法就是将组件的外观绘制出来
1.3.1 测量组件大小
performMeasure() 方法负责组件的自身尺寸的测量,在layout 布局文件中,每一个组件都必须设置 layout_width 和 layout_height属性,属性值有三种可选模式:wrap_content、match_parent 和 数值。preformMeasure() 方法根矩设置的模式计算出组件的宽度和高度。事实上,大多数情况下模式为 match_parent 和 数值的时候不需要计算的,传过来的就是父容器自己计算好的尺寸或是一个指定的精确值,只有当模式是 wrap_content 的时候,才需要根矩内容进行尺寸的测量。prefromMeasure()方法的源码摘录如下:
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);
}
}
对象 mView是 View树的根视图,代码中调用了 mView的 measure()方法,我们进入该方法的源代码如下:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}
忽略了其与代码,只剩下了 onMeasure()这一行,onMeasure()方法是为组件尺寸的测量留的功能接口。当然,也定义了默认的实现,默认的实现并没有太多的意义,在绝大部分情况下,onMEasure()方法必须重写。
如果测量的是容器的尺寸,而容器的尺寸有依赖于子组件的大小,所以必须先测量容器中子类组件的大小。不然,测量出来的宽度和高度永远为 0.编程的时候往往容易忽略。
Android 中使用的单词 measure来计算组件的大小,背后其实颇有讲究。measure是“测量、评定”之意。说明其结果只能起参考作用,并不是一定非使用该值不可。组件真正的大小最终是由setFrame()方法决定的,该方法一般情况下回参考 measure出来的尺寸。
1.3.2 确定子组件的位置
preformLayout()方法用于确定子组件的位置。所以,该方法只针对 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树中根视图(DecorView),也就是最外层容器,容器的位置安排在左上角(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);
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()方法在这里的作用是当前组件为容器时,负责定位容器中的子组件。这其实是一个递归的过程,如果子组件也是一个容器,该容器依然负责它的子组件的定位。依此类推,直到所有的组件都定位完成为止。也就是说:“从顶层的DecorView开始定位,像多米罗骨牌一样从上往下驱动,最后每一个组件都放到它对应该出现的位置上。”onLayout()方法和上节的 onMeasure()方法一样,是为开发人员预留的功能扩展接口,自定义容器时,该方法必须重写。
1.3.3 绘制组件
preformDraw()方法执行执行组件的绘制功能,组件的绘制是一个十分复杂的过程。不仅仅绘制组件本身,还要绘制背景、滚动条,好消息是每一个组件只需要负责自身的绘制。而且一般来说,容器组件不需要绘制,ViewGroup已经做了大量的工作。通过源码整理出的绘图流程如下: 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;
try {
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()开始,代码似乎越来越平易近人越来越接地气了。绘制组件是通过 Canvas类完成的,该类定义了若干个绘制图像的方案。通过 Paint类配置绘制参数,便能绘制出各种图案效果。为了提高绘图的性能,使用了 Surface技术,sureface提供了一套双缓存机制,能大大的加快绘图效率。而我们绘图是需要的 Canvas对象也由是 Surface创建的。
drawSoftware()方法中调用了 mView的 draw()方法。前面说过,mView是 ACtivity界面中 View树的根(DecorView),也是一个容器(具体来说就是一个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的 drwa()方法源码看看:
public void draw(Canvas canvas) {
......
drawBackground(Canvas canvas)
......
if (!dirtyOpaque) onDraw(canvas);
......
dispatchDraw(canvas);
onDrawScrollBars(canvas);
......
}
View 类的 draw()方法是组件绘制的核心方法,主要做了下面几件事情:
- 绘制背景:background.draw(canvas)
- 绘制自己 :onDraw(canvas)
- 绘制子视图 :dispatchDraw(canvas);
- 绘制滚动条 :onDrawScrollBars(canvas);
backgroud是一个 Drawable对象,直接绘制在 Canvas上,并且与组件要绘制的内容互补干扰。跟多时候,这个特征能被某些场景利用。比如后面的“刮刮乐”就是一个很好的范例。
View 只是组件的抽象定义,它自己并不知道自己长神马样子。所以,View定义了一个空 onDraw(),如下 :
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}
和前面的 onMeasure() 与 onLayout()一样,onDraw()方法同样是预留给子类扩展的功能接口。用于绘制组件自身,组件的外观有该方法来决定。
dispatchDraw()方法也是一个空方法,如下:
/**
* 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) {
}
该方法服务容器组件,容器中的子组件必须通过 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()方法