1、view的绘制流程

当 Activity 接收到焦点的时候,它会被请求绘制布局,该请求由 Android framework 处理.绘制是从根节点开始,对布局树进行 measure 和 draw。整个 View 树的绘图流程在ViewRootImpl类的performTraversals()函数展开,该函数所做的工作可简单概况为是否需要重新计算视图大小(performMeasure)、是否需要重新安置视图的位置(performLayout)、以及是否需要重绘(而performDraw),流程图如下:

图中host为的ViewRootImpl全局变量mView

总体来说,UI界面的绘制从开始到结束要经历几个过程:

  • 测量大小,回调 onMeasure()方法
  • 组件定位,回调 onLayout()方法
  • 组件绘制,回调 onDraw()方法

整个绘制流程函数链调用如下:

需要说明的是,如果用户主动调用 request,只会出发 measure 和 layout 过程,而不会执行 draw 过程。接下来详细介绍各个绘制过程。

2、测量大小

performMeasure()方法负责view自身尺寸的测量。我们知道,在layout布局文件中,每一个view都必须设置layout_width和layout_height属性,属性值有三种可选模式:wrap_content、match_parent和数值,performMeasure()方法根据设置的模式计算出组件的宽度和高度。事实上,大多数情况下模式为 match_parent 和数值的时候是不需要计算的,传过来的就是父容器自己计算好的尺寸或是一个指定的精确值,只有当模式为wrap_content的时候才需要根据内容进行尺寸的测量。

performMeasure()方法的完整代码:

    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树的根视图,performMeasure()最终调用了mView的measure()方法,我们进入该方法的源代码:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ……
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ……
}

忽略了其余的代码,只剩下了onMeasure这一行,onMeasure()方法是为组件尺寸的测量预留的功能接口,当然,也定义了默认的实现,默认实现并没有太多意义,在绝大部分情况下,onMeasure()方法必须重写。

如果测量的是容器的尺寸,而容器的尺寸又依赖于子组件的大小,所以必须先测量容器中子组件的大小,不然,测量出来的宽度和高度永远为0。

measure 过程会为一个 View 及所有子节点的mMeasuredWidthmMeasuredHeight 变量赋值,该值可以通过getMeasuredWidth()getMeasuredHeight()方法获得。而且这两个值必须在父视图约束范围之内,这样才可以保证所有的父视图都接收所有子视图的测量。如果子视图对于Measure得到的大小不满意的时候,父视图会介入并设置测量规则进行第二次measure。比如,父视图可以先根据未给定的 尺寸去测量每一个子视图,如果最终子视图的未约束尺寸太大或者太小的时候,父视图就会使用一个确切的大小再次对子视图进行 measure。

2.1 measure 过程传递尺寸的两个类

  • ViewGroup.LayoutParams (View 自身的布局参数)
  • MeasureSpecs 类(父视图对子视图的测量要求)

ViewGroup.LayoutParams这个类我们很常见,就是用来指定视图的高度和宽度等参数。对于每个视图的 height 和 width,你有以下选择:

  • 具体值
  • MATCH_PARENT 表示子视图希望和父视图一样大(不包含 padding 值)
  • WRAP_CONTENT 表示视图为正好能包裹其内容大小(包含 padding 值)

ViewGroup 的子类有其对应的 ViewGroup.LayoutParams 的子类。比如 RelativeLayout 拥有的ViewGroup.LayoutParams的子类RelativeLayoutParams。

有时我们需要使用 view.getLayoutParams()方法获取一个视图LayoutParams,然后进行强转,但由于不知道其具体类型,可能会导致强转错误。其实该方法得到的就是其所在父视图类型的LayoutParams,比如View的父控件为RelativeLayout,那么得到的 LayoutParams 类型就为 RelativeLayoutParams。

MeasureSpecs测量规格,包含测量要求和尺寸的信息,有三种模式:

  • UNSPECIFIED

    父视图不对子视图有任何约束,它可以达到所期望的任意尺寸。比如 ListView、ScrollView,一般自定义 View 中用不到,
  • EXACTLY

    父视图为子视图指定一个确切的尺寸,而且无论子视图期望多大,它都必须在该指定大小的边界内,对应的属性为 match_parent 或具体值,比如 100dp,父控件可以通过MeasureSpec.getSize(measureSpec)直接得到子控件的尺寸。
  • AT_MOST

    父视图为子视图指定一个最大尺寸。子视图必须确保它自己所有子视图可以适应在该尺寸范围内,对应的属性为 wrap_content,这种模式下,父控件无法确定子 View 的尺寸,只能由子控件自己根据需求去计算自己的尺寸,这种模式就是我们自定义视图需要实现测量逻辑的情况。

2.3measure 核心方法

measure(int widthMeasureSpec,intheightMeasureSpec)

该方法定义在View类中,为final类型,不可被复写,但measure调用链最终会回调 View/ViewGroup 对象的onMeasure()方法,因此自定义视图时,只需要复写onMeasure()方法即可。

onMeasure(int widthMeasureSpec,intheightMeasureSpec)

该方法就是我们自定义视图中实现测量逻辑的方法,该方法的参数是父视图对子视图的width和height的测量要求。在我们自身的自定义视图中,要做的就是根据该 widthMeasureSpec和heightMeasureSpec计算视图的width和height,不同的模式处理方式不同。

setMeasuredDimension()

测量阶段终极方法,在onMeasure(intwidthMeasureSpec,intheightMeasureSpec)方法中调用,将计算得到的尺寸,传递给该方法,测量阶段即结束。该方法也是必须要调用的方法,否则会报异常。在我们在自定义视图的时候,不需要关心系统复杂的Measure过程的,只需调用setMeasuredDimension()设置根据MeasureSpec计算得到的尺寸即可,你可以参考ViewPagerIndicator 的 onMeasure 方法。

3、确定子view的位置

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()方法中,在定位之前如果需要重新测量view的大小,则先调用onMeasure()方法,接下来执行setOpticalFrame()或setFrame()方法确定自身的位置与大小,此时只是保存了相关的值,与具体的绘制无关。随后,onLayout()方法被调用,该方法是空方法。

onLayout()方法在这里的作用是当前组件为ViewGroup时,负责定位ViewGroup中的子组件,这其实是一个递归的过程,如果子view也是一个ViewGroup,该ViewGroup依然要负责他的子vew的定位,依此类推,直到所有的view都定位完成为止。也就是说,从最顶层的DecorView开始定位,像多米诺骨牌一样,从上往下驱动,最后每一个view都放到了他应该出现的位置上。onLayout()方法和上节的onMeasure()方法一样,是为开发人员预留的功能扩展接口,自定义ViewGroup时,该方法必须重写。

首先要明确的是,子视图的具体位置都是相对于父视图而言的。View的onLayout方法为空实现,而 ViewGroup的onLayout为abstract,因此,如果自定义的View 要继承 ViewGroup 时,必须实现onLayout 函数。

在 layout 过程中,子视图会调用getMeasuredWidth()和getMeasuredHeight()方法获取到measure过程得到的mMeasuredWidth和mMeasuredHeight,作为自己的width和height。然后调用每一个子视图的layout(l,t,r,b)函数,来确定每个子视图在父视图中的位置。

4、绘制组件

performDraw()方法执行view的绘制功能,view的绘制是一个十分复杂的过程,不仅仅绘制view本身,还要绘制背景、滚动条,好消息是每个view只需要负责自身的绘制,而且一般来说,ViewGroup不需要绘制。

和measure和layout一样,draw过程也是在ViewRoot的performTraversals()的内部发起的,其调用顺序在measure()和layout()之后,同样的,performTraversals()发起的draw过程最终会调用到mView的draw()函数,这里的mView对于Actiity来说就是PhoneWindow.DecorView。

下面将转到mView.draw(),之前提到mView.draw()调用的就是View.java的默认实现,View类中的draw函数体现了View绘制的核心流程,因此我们下面重点来看下View.java中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
    ...
        background.draw(canvas);
    ...
        // skip step 2 & 5 if possible (common case)
    ...
        // Step 2, save the canvas' layers
    ...
        if (solidColor == 0) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;  

            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);
            canvas.drawRect(left, top, right, top + length, p);
        }
    ...
        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
    }

对于View.java和ViewGroup.java,onDraw()默认都是空实现,因为具体View本身长什么样子是由View的设计者来决定的,默认不显示任何东西。

View.java中dispatchDraw()默认为空实现,因为其不包含子视图,而ViewGroup重载了dispatchDraw()来对其子视图进行绘制,通常应用程序不应该对dispatchDraw()进行重载,其默认实现体现了View系统绘制的流程。那么,接下来我们继续分析下ViewGroup中dispatchDraw()的具体流程:

@Override
protected void dispatchDraw(Canvas canvas) {
    ...  

    if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
        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);
            }
        }
    } else {
        for (int i = 0; i < count; i++) {
            final View child = children[getChildDrawingOrder(count, i)];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
    }
     ......
} 

dispatchDraw()的核心代码就是通过for循环调用drawChild()对ViewGroup的每个子视图进行绘制,上述代码中如果FLAG_USE_CHILD_DRAWING_ORDER为true,则子视图的绘制顺序通过getChildDrawingOrder来决定,默认的绘制顺序即是子视图加入ViewGroup的顺序,而我们可以重载getChildDrawingOrder函数来更改默认的绘制顺序,这会影响到子视图之间的重叠关系。

drawChild()的核心过程就是为子视图分配合适的cavas剪切区,剪切区的大小正是由layout过程决定的,而剪切区的位置取决于滚动值以及子视图当前的动画。设置完剪切区后就会调用子视图的draw()函数进行具体的绘制,如果子视图的包含SKIP_DRAW标识,那么仅调用dispatchDraw(),即跳过子视图本身的绘制,但要绘制视图可能包含的字视图。完成了dispatchDraw()过程后,View系统会调用onDrawScrollBars()来绘制滚动条。

view的绘制最终是通过Canvas类完成的,该类定义了若干个绘制图形的方法,通过Paint类配置绘制参数,便能绘制出各种图案效果。有时候为了提高绘图的性能,使用了Surface技术,Surface提供了一套双缓存机制,能大大加快绘图效率,而我们绘图时需要的 Canvas 对象也由是 Surface创建的。

View 类的 draw()方法是组件绘制的核心方法,主要做了下面几件事:

  • 绘制背景:background.draw(canvas)
  • 绘制自己:onDraw(canvas)
  • 绘制子视图:dispatchDraw(canvas)
  • 绘制滚动条:onDrawScrollBars(canvas)

组件的绘制也是一个递归的过程,说到底Activity的UI界面的根一定是容器,根容器绘制结束后开始绘制子组件,子组件如果是容器继续往下递归绘制,否则将子组件绘制出来……直到所有的组件正确绘制为止。

自定义view:view的绘制流程的更多相关文章

  1. Android View 的添加绘制流程 (二)

    概述 上一篇 Android DecorView 与 Activity 绑定原理分析 分析了在调用 setContentView 之后,DecorView 是如何与 activity 关联在一起的,最 ...

  2. android View层的绘制流程

    还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原理,记不记得 ...

  3. 【朝花夕拾】Android自定义View篇之(一)View绘制流程

    前言 转载请申明转自[https://www.cnblogs.com/andy-songwei/p/10955062.html]谢谢! 自定义View.多线程.网络,被认为是Android开发者必须牢 ...

  4. 深入理解 Android 之 View 的绘制流程

    概述 本篇文章会从源码(基于Android 6.0)角度分析Android中View的绘制流程,侧重于对整体流程的分析,对一些难以理解的点加以重点阐述,目的是把View绘制的整个流程把握好,而对于特定 ...

  5. Android中View绘制流程以及invalidate()等相关方法分析

    [原文]http://blog.csdn.net/qinjuning 整个View树的绘图流程是在ViewRoot.java类的performTraversals()函数展开的,该函数做的执行过程可简 ...

  6. Android视图绘制流程完全解析,带你一步步深入了解View(二)

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/16330267 在上一篇文章中,我带着大家一起剖析了一下LayoutInflater ...

  7. View (三) 视图绘制流程完全解析

    相 信每个Android程序员都知道,我们每天的开发工作当中都在不停地跟View打交道,Android中的任何一个布局.任何一个控件其实都是直接或间 接继承自View的,如TextView.Butto ...

  8. Android中View绘制流程以及invalidate()等相关方法分析(转载的文章,出处在正文已表明)

    转载请注明出处:http://blog.csdn.net/qinjuning 前言: 本文是我读<Android内核剖析>第13章----View工作原理总结而成的,在此膜拜下作者 .同时 ...

  9. 【view绘制流程】理解

    一.概述 View的绘制是从上往下一层层迭代下来的.DecorView-->ViewGroup(--->ViewGroup)-->View ,按照这个流程从上往下,依次measure ...

  10. 【转】深入理解Android之View的绘制流程

    概述 本篇文章会从源码(基于Android 6.0)角度分析Android中View的绘制流程,侧重于对整体流程的分析,对一些难以理解的点加以重点阐述,目的是把View绘制的整个流程把握好,而对于特定 ...

随机推荐

  1. ubuntu如何释放内存

    答: step 1: 以最高权限同步所有的缓存到磁盘中 sync sync step2: 执行以下命令指示内核对内存进行调整 echo 3 > /proc/sys/vm/drop_caches ...

  2. layui和bootstrap 对比

    layui和bootstrap 对比 这两个都属于UI渲染框架. layui是国人开发的一套框架,2016年出来的,现在已更新到2.X版本了.比较新,轻量级,样式简单好看. bootstrap 相对来 ...

  3. jmeter-负载

    主: remote_hosts=10.0.70.35:1099,10.0.70.47:1099 server.rmi.localport=1099 从:  remote_hosts=10.0.70.3 ...

  4. linux du命令的疑惑

    起因是测试rsync传输数据.传输完成后,想看一下传输的文件是不是完整,所以检测了下源目录和目标目录的大小,竟然出现了巨大的差距: [root@w anaconda3]$ du -sh ./ .9G ...

  5. JQuery获取指定元素中的checkbox选中状态的一些属性

    项目中用户上传病例数据,每一次上传自动生成一个病例文件夹,数据保存到后台,前端显示文件夹,现在的需求是勾选想要删除的文件夹的chenckbox,点击删除后,数据库和前端都相应的更新. 如果是静态页面, ...

  6. angularjs定时任务的设置与清除

    人们似乎常常将AngularJS中 的$timeOut()  $interval()函数看做是一个内置的.无须在意的函数.但是,如果你忘记了$timeOut()$interval()的回调函数将会造成 ...

  7. nRF5芯片外设GPIO和GPIOTE介绍

    nRF51/nRF52同时包含GPIO和GPIOTE两种外设,经常有人将两者搞混,今天我们就来介绍一下这2种外设有什么不同,及使用注意事项. GPIO和GPIOTE都属于芯片外设,但两者功能完全不一样 ...

  8. Microsoft's OWIN implementation, the Katana project

    参考: https://github.com/aspnet/AspNetKatana/ https://github.com/aspnet/AspNetKatana/wiki/Roadmap

  9. Decrypting OWIN Authentication Ticket

    参考:https://long2know.com/2015/05/decrypting-owin-authentication-ticket/ AuthServer产生的Token因为没有制定自定义的 ...

  10. webBrowser.DocumentText重新赋值无效解决方法

    因为webBrowser这个控件的webBrowser.DocumentText是异步的,所以要自己调用刷新: webBrowser.Navigate("about:blank") ...