株洲新城 IT教育 李赞红老师 第一章节

时间:2023-02-10 12:22:29

摘要 : 初学者一枚,每一次写的时候,都是从网上直接 复制+粘贴。。完全不为什么要这样写,为什么通过这样就可以实现,也想着要去看懂。可是看一下就不知道怎么下手。完全是一个球形,无法下手。有一些通过各种搜索后,能够知道了表皮意思,可以接下来自己动手的时。就又忘记了怎么下手。希望各位大大介绍一下大致的方向,再此感激不尽。后面考虑一下还是直接做笔记吧!每一去搜索慢慢的收藏也多了。完全找不到自己所需要的在哪里去了,又得重新搜索…

文摘摘抄至:株洲新城 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。
下图所示描述上面各组件之间的关系:株洲新城 IT教育 李赞红老师 第一章节

从上图可以看出:

  • 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涉及到各个组件的关系图(来源于网络)

株洲新城 IT教育 李赞红老师 第一章节

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()方法

PDF(Android自定组件详解)