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

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

文摘摘抄至:株洲新城 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()方法

PDF(Android自定组件详解)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值