文档章节

Android View绘制流程

SuShine
 SuShine
发布于 2015/06/25 15:27
字数 2840
阅读 10
收藏 0
点赞 0
评论 0

框架分析

在之前的下拉刷新中,小结过触屏消息先到WindowManagerServiceWms)然后顺次传递给ViewRoot(派生自Handler),经decor viewActivity再传递给指定的View,这次整理View的绘制流程,通过源码可知,这个过程应该没有涉及到IPC(或者我没有发现),需要绘制时在UI线程中通过ViewRoot发送一个异步请求消息,然后ViewRoot自己接收并不处理这个消息。

在正式进入View绘制之前,首先需要明确一下Android UI的架构组成,偷图如下:

 

上述架构很清晰的呈现了ActivityWindowDecorView(及其组成)、ViewRootWMS之间的关系,我通过源码简单理了下从启动Activity到创建View的过程,大致如下

 

在上图中,performLaunchActivity函数是关键函数,除了新建被调用的Activity实例外,还负责确保Activity所在的应用程序启动、读取manifest中关于此activity设置的主题信息以及上图中对“6.onCreate”调用也是通过对mInstrumentation.callActivityOnCreate来实现的。图中的“8. mContentParent.addView”其实就是架构图中phoneWindowDecorView里面的ContentViews,该对象是一个ViewGroup类实例。在调用AddView之后,最终就会触发ViewRoot中的scheduleTraversals这个异步函数,从而进入ViewRootperformTraversals函数,在performTraversals函数中就启动了View的绘制流程。

performTraversals函数在2.3.5版本源码中就有近六百行的代码,跟我们绘制view相关的可以抽象成如下的简单流程图

 

流程图中的host其实就是mView,而ViewRoot中的这个mView其实就是DecorView,之所以这么说,又得具体看源码中ActivityThreadhandleResumeActivity函数,在这里我就不展开了。上述流程主要调用了Viewmeasurelayoutdraw三个函数。

measure过程分析

因为DecorView实际上是派生自FrameLayout的类,也即一个ViewGroup实例,该ViewGroup内部的ContentViews又是一个ViewGroup实例,依次内嵌ViewViewGroup形成一个View树。所以measure函数的作用是为整个View树计算实际的大小,设置每个View对象的布局大小(“窗口”大小)。实际对应属性就是View中的mMeasuredHeight(高)和mMeasureWidth(宽)。

View类中measure过程主要涉及三个函数,函数原型分别为

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

前面两个函数都是final类型的,不能重载,为此在ViewGroup派生的非抽象类中我们必须重载onMeasure函数,实现measure的原理是:假如View还有子View,则measureView,直到所有的子View完成measure操作之后,再measure自己。ViewGroup中提供的measureChildmeasureChildWithMargins就是实现这个功能的。

在具体介绍测量原理之前还是先了解些基础知识,即measure函数的参数由类measureSpecmakeMeasureSpec函数方法生成的一个32位整数,该整数的高两位表示模式(Mode),低30位则是具体的尺寸大小(specSize)。

MeasureSpec有三种模式分别是UNSPECIFIED, EXACTLYAT_MOST,各表示的意义如下

如果是AT_MOSTspecSize代表的是最大可获得的尺寸;

如果是EXACTLYspecSize代表的是精确的尺寸;

如果是UNSPECIFIED,对于控件尺寸来说,没有任何参考意义。

那么对于一个View的上述ModespecSize值默认是怎么获取的呢,他们是根据ViewLayoutParams参数来获取的:

参数为fill_parent/match_parent时,ModeEXACTLYspecSize为剩余的所有空间;

参数为具体的数值,比如像素值(pxdp),ModeEXACTLYspecSize为传入的值;

参数为wrap_contentModeAT_MOSTspecSize运行时决定。

具体测量原理

上面提供的ModespecSize只是程序员对View的一个期望尺寸,最终一个View对象能从父视图得到多大的允许尺寸则由子视图期望尺寸和父视图能力尺寸(可提供的尺寸)两方面决定。关于期望尺寸的设定,可以通过在布局资源文件中定义的android:layout_widthandroid:layout_height来设定,也可以通过代码在addView函数调用时传入的LayoutParams参数来设定。父View的能力尺寸归根到最后就是DecorView尺寸,这个尺寸是全屏,由手机的分辨率决定。期望尺寸、能力尺寸和最终允许尺寸的关系,我们可以通过阅读measureChildmeasureChildWithMargins都会调用的getChildMeasureSpec函数的源码来获得,下面简单列表说明下三者的关系

父视图能力尺寸

子视图期望尺寸

子视图最终允许尺寸

EXACTLY + Size1

EXACTLY + Size2

EXACTLY + Size2

EXACTLY + Size1

fill_parent/match_parent

EXACTLY+Size1

EXACTLY + Size1

wrap_content

AT_MOST+Size1

AT_MOST+Size1

EXACTLY + Size2

EXACTLY+Size2

AT_MOST+Size1

fill_parent/match_parent

AT_MOST+Size1

AT_MOST+Size1

wrap_content

AT_MOST+Size1

UNSPECIFIED+Size1

EXACTLY + Size2

EXACTLY + Size2

UNSPECIFIED+Size1

fill_parent/match_parent

UNSPECIFIED+0

UNSPECIFIED+Size1

wrap_content

UNSPECIFIED+0

上述表格展现的是子视图最终允许得到的尺寸,显然147三项没有对Size1Size2进行比较,所以允许尺寸是可以大于父视图的能力尺寸的,这个时候最终的视图尺寸该是多少呢?AT_MOSTUNSPECIFIEDView又该如何决策最终的尺寸呢? 

通过Demo演示的得到的结果,假如Size2Size1的尺寸大,假如不使用滚动效果的话,子视图超出部分将被裁剪掉,该父视图中如果在该子视图后面还有其他视图,那么也将被裁剪掉,但是通过调用其getVisibility还是显示该控件是可见的,所以裁剪后控件依然是有的,只是用户没办法观察到;在使用滚动效果的情况下,就能将原本被裁剪掉的控件通过滚动显示出来。

对于第二个问题,根据源码ViewOnMeasure函数调用的getDefaultSize函数获知,默认情况下,控件都有一个最小尺寸,该值可以通过设置android:minHeightandroid:minWidth来设置(无设置时缺省为0);在设置了背景的情况下,背景drawable的最小尺寸与前面设置的最小尺寸比较,两者取大者,作为控件的最小尺寸。在UNSPECIFIED情况下就选用这个最小尺寸,其它情况则根据允许尺寸来。不过这个是默认规则,通过demo发现,TextViewAT_MOST+Size情况下,并不是以Size作为控件的最终尺寸,结果发现在TextView的源码中,重载了onMeasure函数,有价值的代码如下:

……

int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int heightSize = MeasureSpec.getSize(heightMeasureSpec);

……

if (widthMode == MeasureSpec.AT_MOST) {

    width = Math.min(widthSize, width);

}

……

if (heightMode == MeasureSpec.AT_MOST) {

    height = Math.min(desired, heightSize);

}

……

至于其中的widthdesired值,感兴趣的同学可以具体关注下。虽然FrameWork提供了视图默认的尺寸计算规则,但是最终的视图布局大小可以重载onMeasure函数来修改计算规则,当然也可以不计算直接通过setMeasuredDimension来设置(需要注意的是,如果通过setMeasuredDimension的同时还要调用父类的onMeasure函数,那么在调用父类函数之前调用的setMeasuredDimension会无效果)。

layout过程分析

上述measure过程达到的结果是设定了视图的高和宽,layout过程的作用就是设定视图在父视图中的四个点(分别对应View四个成员变量mLeftmTopmLeftmBottom)。同样layout也是被fianl修饰符限定为不能重载,不过在ViewGrouponLayout函数被abstract修饰,即所有派生自ViewGroup的类必须实现onLayout函数,从而实现对其包含的所有子视图的布局设定。

那么上述的measure结果与layout有什么关系,截取ViewRootFrameLayout两个类中onLayout函数的部分代码如下:

//ViewRoot的performTraversals函数measure之后对layout的调用代码

host.layout(0, 0, host.mMeasuredWidthhost.mMeasuredHeight);

//FrameLayou的onLayout函数部分源码

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

        final int count = getChildCount();

        ……

        for (int i = 0; i < count; i++) {

            final View child = getChildAt(i);

            if (child.getVisibility() != GONE) {

                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();

                final int height = child.getMeasuredHeight();

                int childLeft = parentLeft;

                int childTop = parentTop;

                final int gravity = lp.gravity;

 

                if (gravity != -1) {

                    final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;

                    final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

 

                    switch (horizontalGravity) {

                        case Gravity.LEFT:

                            childLeft = parentLeft + lp.leftMargin;

                            break;

                        case Gravity.CENTER_HORIZONTAL:

                            childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin;

                            break;

                        case Gravity.RIGHT:

                            childLeft = parentRight - width - lp.rightMargin;

                            break;

                        default:

                            childLeft = parentLeft + lp.leftMargin;

                    }

 

                    switch (verticalGravity) {

                        case Gravity.TOP:

                            childTop = parentTop + lp.topMargin;

                            break;

                        case Gravity.CENTER_VERTICAL:

                            childTop = parentTop + (parentBottom - parentTop - height) / 2 + lp.topMargin - lp.bottomMargin;

                            break;

                        case Gravity.BOTTOM:

                            childTop = parentBottom - height - lp.bottomMargin;

                            break;

                        default:

                            childTop = parentTop + lp.topMargin;

                    }

                }

 

                child.layout(childLeft, childTop, childLeft + width, childTop + height);

            }

        }

    }

从代码显然可知具体layout布局时,就是根据measure过程设置的高和宽,结合视图在父视图中的起始位置,再外加视图的layoutgravity属性来设置四个点的具体位置(在LinearLayout中还会增加对layoutweight属性的考虑)。这个过程相对没有measure那么复杂。

需要注意的是在自定义组合控件的时候,我们可以根据需要不用或只用部分measure过程计算得到的尺寸,具体可以看下之前做的下拉刷新控件直接重载的onLayout函数:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

    if (getChildCount() > 2) {

        throw new IllegalStateException("NPullToFreshContainer can host only two direct child");

    }

        

    View headView = getChildAt(0);

    View contentView = getChildAt(1);

    if(headView != null){

     headView.layout(0, -HEAD_VIEW_HEIGHT + mTatolScroll, getMeasuredWidth(), mTatolScroll);// mTatolScroll是下拉的位移值

    }

   

    if(contentView != null){

    contentView.layout(0, mTatolScroll, getMeasuredWidth(), getMeasuredHeight());

    }

        

    if (mFirstLayout) {        

     HEAD_VIEW_HEIGHT = getChildAt(0).getMeasuredHeight();

       mFirstLayout = false;

    }

}

draw过程分析

View的Draw过程,其实相对来说应该比measure过程更为复杂,正因为其很复杂,所以android框架层已经将draw过程考虑得相当周全,虽然view类的Draw函数没用final修饰,但是我们自定义的View,一般也不需要去重载实现它,自己目前也没有自己去draw过界面,对整个过程,只能偷别人整理的逻辑,结合源码浏览了一下,在这里做个标注。

draw()方法实现的功能流程如下:

1、调用background.draw(canvas)绘制该View的背景

2、调用onDraw(canvas)方法绘制视图本身(每个View都需要重载该方法,ViewGroup不需要实现该方法)

3、调用dispatchDraw(canvas)方法绘制子视图(ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,其内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法)

4、调用onDrawScrollBars(canvas)绘制滚动条

为了说明measurelayoutdraw过程的连续性,摘得draw中的源码如下

……

if (mBackgroundSizeChanged) {

    background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);

    mBackgroundSizeChanged = false;

}

……

上述的mLeftmTopmLeftmBottom就是我们在layout是设定的结果值,这里之所以要用减法获取高宽尺寸而不用measure过程设定的mMeasuredHeightmMeasureWidth,个人感觉就是因为我们可以在代码中通过直接调用Viewlayout函数避开measure测算结果而导致真实高宽不等于mMeasuredHeightmMeasureWidth这种情况。

上述代码中的mBackgroundSizeChanged是个私有成员变量,源码中只能在ViewonScrollChanged(int l, int t, int oldl, int oldt) layout过程调用的setFrame(int left, int top, int right, int bottom) setBackgroundDrawable(Drawable d)这三个函数中对其修改为true

到这里,除了具体的绘制外,我们对从ActivityView的绘制流程应该比较清楚了。

http://blog.163.com/eric_blog_/blog/static/197817384201210141029421/

本文转载自:http://blog.csdn.net/sfshine/article/details/8810312

共有 人打赏支持
SuShine
粉丝 119
博文 452
码字总数 126400
作品 0
青岛
后端工程师
自定义View心法——View工作流程

前言 本文的目的有两个: 给对自定义View感兴趣的人一些入门的指引 给正在使用自定义View的人一些更深入的解析 自定义View一直都被认为是Android开发高手的必备技能,而稳中带皮的学习View的...

Alex_Payne
05/24
0
0
Android View绘制过程以及事件传递原理

一. 对于控件,Android中的测量方式 在Android中,控件绘制的步骤是 measure,layout,draw 一般来说,如果父布局的宽度和高度,内外边距位指定,那么,上面的方法可以简化如下 在android开发中...

IamOkay
2015/03/23
0
0
Android中View(视图)绘制不同状态背景图片原理深入分析以及StateListDrawable使用详解

今天继续给大家分享下View的相关知识,重点有一下两点: 1、View的几种不同状态属性 2、如何根据不同状态去切换我们的背景图片。 开篇介绍:android背景选择器selector用法汇总 对Android开发...

狱天穹
2014/09/24
0
0
Android HWUI硬件加速模块浅析

原址 什么是硬件加速(What) 传统软件的UI绘制是依靠CPU来完成的,硬件加速就是将绘制任务交由GPU来执行。Android系统负责硬件加速的模块主要是HWUI,如下图所示: 为什么要硬件加速(Why)...

u010164190
04/27
0
0
Tips_Android中View绘制流程以及invalidate()等相关方法分析

Android中View绘制流程以及invalidate()等相关方法分析 http://blog.csdn.net/qinjuning/article/details/7110211 measure,layout,draw的递归向下过程=== 《Android中将布局文件/View添加至窗...

mstian06
2014/06/14
0
0
Android中View的绘制流程

安卓view的绘制流程主要分为三个阶段: 一、测量过程(mesure) 二、确定子元素的过程(layout) 三、绘制过程(draw) 我们在activity的oncread方法中会使用setContentView来填充一个布局 ...

小怪兽是程序员
06/07
0
0
Android视图绘制流程完全解析,带你一步步深入了解View(二)

原文地址:http://blog.csdn.net/guolin_blog/article/details/16330267 在此转载,以备不时之需,建议看原文,转载的排版有点问题 在上一篇文章中,我带着大家一起剖析了一下LayoutInflate...

工作日
2014/02/26
0
0
Android自定义View全解

目录 1. 自定义View基础 1.1 分类 自定义View的实现方式有以下几种 1.2 View绘制流程 View的绘制基本由measure()、layout()、draw()这个三个函数完成 1.3 坐标系 在Android坐标系中,以屏幕左...

銀灬楓
05/10
0
0
Android学习——控制硬加速 hardwareAccelerated

Android学习——控制硬加速 hardwareAccelerated 在3.0才有的。 分类: android学习 2012-04-27 13:59 8742人阅读 评论(4) 收藏 举报 androidapplicationapinull测试 从Android3.0 (API lev...

Jonson
2014/05/15
0
0
Android SurfaceView简例

Android SurfaceView简例 Android中各的SurfaceView和View有很大的不同,两者应用场景不同。大多数View能做的事情SurfaceView也可以,但是SurfaceView效率更高。Android的View绘制过程由And...

开开心心过
2017/09/13
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

Git 2.18版本发布:支持Git协议v2,提升性能

Git 2.18版本发布:支持Git协议v2,提升性能Git 2.18版本发布:支持Git协议v2,提升性能 新版本协议的主要驱动力是使 Git 服务端能够对各种 ref(分支与 tag)进行过滤操作。 这就意味着,G...

linux-tao
30分钟前
0
0
python浏览器自动化测试库【2018/7/22-更新】

64位py2.7版本 更新 document_GetResources 枚举页面资源 document_GetresourceText 获取指定url的内容 包括页面图片 下载地址下载地址 密码:upr47x...

开飞色
46分钟前
28
0
关于DCL双重锁失效及解决方案

关于DCL双重锁失效及解决方案 Double Check Lock (DCL)实现单例 DCL 方式实现单例的优点是既能够在需要时才初始化单例,又能够保证线程安全,且单例对象初始化后调用getInstance方法不进行...

DannyCoder
52分钟前
0
0
PowerDesigner 16.5 安装配置

PowerDesigner16.5破解版是一款业内领先且开发人员常用的数据库建模工具,PowerDesigner可以从物理和概念两个层面设计数据库,方便用户制作处清晰直观的数据流程图和结构模型,欢迎有需要的朋...

Gibbons
今天
0
0
mac Homebrew 指令积累

1通用命令 brew install [包名] //安装包 brew list //列举安装的包 brew info [包名] // 显示安装包的详细信息 mysql 相关 #启动mysql 服务 brew service start mysql my...

Kenny100120
今天
0
0
前端Tips: 创建, 发布自己的 Vue UI 组件库

创建, 发布自己的 Vue UI 组件库 前言 在使用 Vue 进行日常开发时, 我们经常会用到一些开源的 UI 库, 如: Element-UI, Vuetify 等. 只需一行命令, 即可方便的将这些库引入我们当前的项目: n...

ssthouse_hust
今天
1
0
大数据教程(2.13):keepalived+nginx(多主多活)高可用集群搭建教程【自动化脚本】

上一章节博主为大家介绍了目前大型互联网项目的keepalived+nginx(主备)高可用系统架构体系,相信大家应该看了博主的文章对keepalived/nginx技术已经有一定的了解,在本节博主将为大家分享k...

em_aaron
今天
5
0
Git 2.18版本发布:支持Git协议v2,提升性能

在最新的官方 Git 客户端正式版2.18中添加了对 Git wire 协议 v2 的支持,并引入了一些性能与 UI 改进的新特性。在 Git 的核心团队成员 Brandon Williams 公开宣布这一消息前几周,Git 协议 ...

六库科技
今天
0
0
Java8新特性之接口

在JDK8以前,我们定义接口类中,方法都是抽象的,并且不能存在静态方法。所有的方法命名规则基本上都是 public [返回类型] [方法名](参数params) throws [异常类型] {}。 JDK8为接口的定义带...

developlee的潇洒人生
今天
0
0
aop + annotation 实现统一日志记录

aop + annotation 实现统一日志记录 在开发中,我们可能需要记录异常日志。由于异常比较分散,每个 service 方法都可能发生异常,如果我们都去做处理,会出现很多重复编码,也不好维护。这种...

长安一梦
今天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部