前言
Android的视图是如何绘制的?深入了解一下UI的绘制原理无论对我们APP的性能优化还是对我们的自定义view都有很大的帮助。下文将和大家一道探究一下Android的viewTree的绘制原理,希望对大家的开发和学习有所帮助。
本篇是图解Android系列第二篇,更多文章敬请关注后续文章。如果这篇文章对大家学习Android有帮助,还望大家多多转载。学习小组QQ群: 193765960。
版权归作者所有,如有转发,请注明文章出处:https://xiaodanchen.github.io/archives/
Activity的视图结构
先看一下activity的视图结构图
- 每个activity都有一个Window(实际是phonewindow)
- Phonewindow含有一个DecorView,这是我们window的topview
- DecorView是继承自Framelayout,换言之其为整个ViewTree的根节点viewGroup
再看一下Phonewindow的类图
接下来我们来看一下单个Activity的viewTree的结构,我选择了两版sdk来查看
1)Android4.4系统的activity:
2)Android6.0系统的activity:
ViewTree的绘制
id为“content”的ContentFrameLayout是我们的布局文件加载显示的区域,更确切地说是我们activity的setcontentView()方法设置的视图显示的区域。下面我么就看看ContentFrameLayout中整个viewTree是如何绘制出来的。
在《图解Android:事件传递机制》中我们说过Android中的任何一个布局、任何一个控件包括我们自定义的控件其实都是直接或间接继承自View实现的,所以说这些View应该都具有相同的绘制流程与机制才能显示到屏幕上(可能每个控件的具体绘制逻辑有差异, 但是主流程都是一样的)。每一个View的绘制过程都必须经历三个最主要的过程,也就是measure()、layout()和draw()。
先看一下类图:
那么,整个Android的UI绘制机制是从哪里开始的即入口在哪里呢?答案就是ViewRootImpl类的performTraversals()方法。ViewRootImpl这个类是一个隐藏类,所以如果你是使用Eclipse开发的话可能看不到这个文件(AndroidStudio可以),没关系,根据路径(androidSDK\android-sdk-windows\sources\android-23\android\view\)去找到ViewRootImpl.Java文件,然后用文本阅读工具直接打开就好。
看一下官方对ViewRootImpl的介绍:
上面这段注释啥意思呢?说白了就是ViewRootImpl是一个window中的viewTree的入口,实现了window对viewTree管理的必需逻辑。
ViewRootImpl类performTraversals()代码,源代码长的恐怖,这里给大家过滤一下
|
|
measure相关
View类的UI绘制相关函数
|
|
- 注意onMeasure(int widthMeasureSpec, int heightMeasureSpec)入参的含义:
1) widthMeasureSpec和heightMeasureSpec是parent暴露给child的尺寸
2) widthMeasureSpec和heightMeasureSpec是32位的数值,其中高16位为MeasureSpecMode,低16位为MeasureSpecSize
3) MeasureSpecMode有三种取值:* MeasureSpec.EXACTLY:child为精准尺寸(layout_with=mach_parent、24dp的情况) * MeasureSpec.AT_MOST:child为最大尺寸(layout_with=wrap_content的情况) * MeasureSpec.UNSPECIFIED:child未指定尺寸
- child的尺寸有parent穿过来的widthMeasureSpec、heightMeasureSpec和子的MeasureSpecMode共同决定
layout相关
View的layout相关代码
|
|
ViewGroup的layout相关代码
|
|
|
|
注意:
- View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。
- measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作 完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。
- 凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的。
- 使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。
draw相关
View的draw相关代码
|
|
注意:
- View的onDraw()方法为空,需要用户自己实现
- 关于draw,官方的注释已经很清楚,我们需要注意的是第四步:递归调用完成viewTree的绘制
- dispatchdraw()为空,需要在子类去实现
ViewGroup的draw相关代码
|
|
关于invalidate方法
invalidate系列方法请求重绘View树(也就是draw方法),如果View大小没有发生变化就不会调用layout过程,并且只绘制那 些“需要重绘的”View,也就是哪个View(View只绘制该View,ViewGroup绘制整个ViewGroup)请求invalidate系 列方法,就绘制该View。
有以下几种触发invalidate方法的情况:
- 直接调用invalidate方法:会绘制调用者本身。
- 触发setSelection方法:会绘制调用者本身。
- 触发setVisibility方法:当View可视状态在INVISIBLE转换VISIBLE时会间接调用invalidate方法,继而绘制该View。当View的可视状态在 INVISIBLE\VISIBLE 转换为GONE状态时会间接调用requestLayout和invalidate方法,同时由于View树大小发生了变化,所以会请求measure过 程以及draw过程,同样只绘制需要“重新绘制”的视图。
- 触发setEnabled方法:不会重新绘制任何View包括该调用者本身。
- 触发requestFocus方法:只绘制“需要重绘”的View。