ViewRoot 和 DecorView
1,ViewRoot 是什么?
ViewRoot 对应于 ViewRootImpl 类
是连接 WindowManager 和 DecorView 的纽带
发起并完成 View 的三大流程(测量, 布局, 绘制)
ViewRoot 需要和 DecorView 建立联系.
2,ViewRoot 如何完成 View 的三大流程?
WechatIMG125.jpeg
ViewRoot 的 performTraversals()开始 View 的绘制流程, 依次调用 performMeasure(),performLayout()和 performDraw()
performMeasure()最终执行父容器的 measure()方法, 并依此执行所有子 View 的 measure 方法.
performLayout()和 performDraw()同理
3,View 三大流程的作用
measure 决定了 View 的宽 / 高, 测量后可以通过 getMeasuredWidth/Height 来获得 View 测量后的宽 / 高, 除特殊情况外该值等于 View 最终的宽 / 高
layout 决定了 View 的顶点坐标以及实际 View 的宽 / 高: 完成后可以通过 getTop/Bottom/Left/Right 获取顶点坐标, 并通过 getWidth/Height()获得 View 的最终宽 / 高
draw 决定了 View 的显示, 最终将 View 显示出来
MeasuredWidth/height != getWidth/Height()的场景: 更改 View 的布局参数并进行重新布局后, 就会导致测量 != 实际值
4,DecorView 的作用
DecorView 是顶级 View, 本质就是一个 FrameLayout
包含了两个部分, 标题栏和内容栏
内容栏 id 是 content, 也就是 activity 中 setContentView 所设置的部分, 最终将布局添加到 id 为 content 的 FrameLayout 中
获取 content:ViewGroup content = findViewById(R.Android.id.content)
获取设置的 View:content.getChidlAt(0)
5,ViewRootIml 如何和 DecorView 建立联系
Activity 对象在 ActivityThread 中创建完毕后, 会将 DecorView 添加到 Windows 中
同时会创建 ViewRootImpl, 调用 ViewRoot 的 setView 方法将 ViewRootImpl 和 DevorView 建立关联
- root = new ViewRootImpl(view.getContext(), display)
- root.setView(view, wparams, panelParentView)
6,ViewRoot 为什么要和 DecorView 建立关联
DecorView 等 View 的三大流程需要通过 ViewRoot 完成
MeasureSpec
1,MeasureSpec 是什么?
MeasureSpec 是一种 "测量规则" 或者 "测量说明书", 决定了 View 的测量过程
View 的 MeasureSpec 会根据自身的 LayoutParamse 和父容器的 MeasureSpec 生成.
最终根据 View 的 MeasureSpec 测量出 View 的宽 / 高(测量时数据并非最终宽高)
2,MeasureSpec 要点解析
MeasureSpec 代表一个 32 位 int 值, 高 2 位是 SpecMode, 低 30 位是 SpecSize
SpecMode 是指测量模式
SpecSize 是指在某种测量模式下的大小
类 MesaureSpec 提供了用于 SpecMode 和 SpecSize 打包和解包的方法
3, 测量模式 SpecMode 的类型
UNSPECIFIED: 父容器不对 View 有任何限制, 一般用于系统内部
EXACTLY: 精准模式, View 的最终大小就是 SpecSize 指定的值(对应于 LayoutParams 的 match_parent 和具体的数值)
AT_MOST: 最大值模式, 大小不能大于父容器指定的值 SpecSize(对应于 wrap_content)
4,MeasureSpec 和 LayoutParams 的对应关系
View 的 MeasureSpec 是需要通过自身的 LayoutParams 和父容器的 MeasureSpec 一起才能决定
DecorView(顶级 View)是例外, 其本身 MeasureSpec 由窗口尺寸和自身 LayoutParams 共同决定
MeasureSpec 一旦确定, onMeasure 中就可以确定 View 的测量宽 / 高
5, 普通 View 的 Measure 的创建规则
View 本身布局参数为具体 dp/px 数值, 模式: EXACTLY, 尺寸: 自身尺寸(不管父容器的 MeasureSpec)
View 为 match_parent, 模式: EXACTLY/AT_MOST 由父容器 MeasureSpec 决定, 尺寸: 父容器目前可用大小
View 为 wrap_content, 模式: AT_MOST, 尺寸: 父容器可用尺寸(不能超过该尺寸)
当父容器为 UNSPECIFIED 时, View 为具体数值时规则不变; 其余 match_parent/wrap_content, 模式均为: UNSPECIFIED, 尺寸: 0
UNSPECIFIED 一般用于系统内部多次 measure 的情况, 不需要关注该模式.
WechatIMG126.jpeg
View 的工作流程
measure: 测量 -- 确定 View 的测量宽 / 高
layout: 布局 -- 确定 View 的最终宽 / 高和四个顶点的位置
draw: 绘制 -- 将 View 绘制到屏幕上
1,Measure 过程
1,View 的 measure 过程及要点
View 的 measure 方法是 final 类型方法 -- 表明该方法无法被重载
View 的 measure 方法会调用 onMeasure 方法, onMeasure 会调用 setMeasuredDimension 方法设置 View 宽 / 高的测量值
2,View 的 onMeasure 源码要点
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- //1. setMeasuredDimension 方法设置 View 宽 / 高的测量值
- setMeasuredDimension(
- //2. 第一个参数是获得的测量宽 / 高(通过 getDefaultSize 获取)
- getDefaultSize(getSuggestedMinimumWidth(), //3. 获取的建议最小的宽 / 高
- widthMeasureSpec),
- getDefaultSize(getSuggestedMinimumHeight(),
- heightMeasureSpec));
- }
setMeasuredDimension 方法设置 View 宽 / 高的测量值(测量值通过 getDefaultSize 获取)
getDefaultSize 用于获取 View 的测量宽 / 高
3,View 的 getDefaultSize 源码要点(决定了 View 宽高的测量值)
- //1. 获取 View 宽和高的测量值
- public static int getDefaultSize(int size, int measureSpec) {
- int result = size;
- int specMode = MeasureSpec.getMode(measureSpec);
- int specSize = MeasureSpec.getSize(measureSpec);
- switch (specMode) {
- //2. UNSPECIFIED 模式时, 宽 / 高为第一个参数也就是 getSuggestedMinimumWidth()获取的建议最小值
- case MeasureSpec.UNSPECIFIED:
- result = size;
- break;
- //3. AT_MOST(wrap_content)和 EXACTLY(match_parent / 具体值 dp 等)这两个模式下, View 宽高的测量值为当前 View 的 MeasureSpec(测量规格)中指定的尺寸 specsize
- case MeasureSpec.AT_MOST:
- case MeasureSpec.EXACTLY:
- result = specSize;
- break;
- }
- return result;
- }
4,View 的 getSuggestedMinimumWidth/Height()源码要点
- // 获取建议的最小宽度
- protected int getSuggestedMinimumWidth() {
- return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
- }
如果 View 没有背景, View 的最小宽度就为 Android:minWidth 这个参数指定的值(mMinWidth), 没有指定则默认为 0
如果 View 有背景, 会从 mMinWidth 和背景的最小宽度中取最大值.
背景的最小宽度 (getMinimumWidth()) 本质就是 Drawable 的原始宽度(ShapeDrawable 无原始宽度, BitmapDrawable 有原始宽度 -- 图片的尺寸)
5,View 的 wrap_content 和 match_parent 效果一致的原因分析
根据 View 的 onMeasure 方法中的 getDefaultSize 方法, 我们可以发现在两种模式下, View 的测量值等于该 View 的测量规格 MeasureSpec 中的尺寸.
View 的 MeasureSpec 本质是由自身的 LayoutParams 和父容器的 MeasureSpec 决定的.
当 View 为 wrap_content 时, 该 View 的模式为 AT_MOST, 且尺寸 specSize 为父容器的剩余空间大小.
当 View 为 match_parent 时, 该 View 的模式跟随父容器的模式(AT_MOST/EXACTLY), 且尺寸 specSize 为父容器的剩余空间大小.
因此 getDefaultSize 中无论 View 是哪种模式, 最终测量宽 / 高均等于尺寸 specSize, 因此两种属性效果是完全一样的(View 的大小充满了父容器的剩余空间)
除非给定 View 固定的宽 / 高, View 的 specSize 才会等于该固定值.
6, 自定义 View 需要重写 onMeasure 方法, 并写明两种模式的处理方法
- //1. 重写 onMeasure, 特殊处理 wrap_content 的情况
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
- int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
- int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
- int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
- if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
- //2. 均为 wrap_content 时, 将值设置为 Android:minWidth/Height 属性指定的值
- setMeasuredDimension(mWidth, mHeight);
- }else if(widthSpecMode == MeasureSpec.AT_MOST){
- //3. 哪个为 wrap_content 哪个就用 Android:minXXX 属性给定的最小值
- setMeasuredDimension(mWidth, heightSpecSize);
- }else if(heightSpecMode == MeasureSpec.AT_MOST){
- setMeasuredDimension(widthSpecSize, mHeight);
- }
- }
7,ViewGroup(抽象类)的 measure 流程
ViewGroup 没有 onMeasure 方法, 只定义了 measureChildren 方法(onMeasure 根据不同布局难以统一)
measureChildren 中遍历所有子元素并调用 measureChild 方法
measureChild 方法中会获取子 View 的 MeasureSpec, 然后调用子元素 View 的 measure 方法进行测量
8,getChildMeasureSpec 获取子元素 MeasureSpec 的要点
子 View 的 MeasureSpec 是根据自身的 LayoutParams 和父容器 SpecMode 生成
当子 View 的布局参数为 wrap_content, 且父容器模式为 AT_MOST 时, 效果与子元素布局为 match_parent 是一样的. 因此当子 View 的布局参数为 wrap_content 时, 需要指定默认的宽 / 高
9,LinearLayout 的 onMeasure()分析
ViewGroup 因为布局的不同, 无法统一 onMeasure 方法, 具体内容根据布局的不同而不同, 这里直接以 LinearLayout 进行分析
onMeasure 会根据 orientation 选择 measureVertical 或者 measureHorizontal 进行测量
measureVertical 本质是遍历子元素, 并执行子元素的 measure 方法, 并获得子元素的总高度以及子元素在竖直方向上的 margin 等.
最终 LinearLayout 会测量自己的大小, 在 orientation 的方向上, 如果布局是 match_parent 或者具体数值, 测量过程与 View 一致(高度为 specSize); 如果布局是 wrap_content, 高度是所有子元素高度总和, 且不会超过父容器的剩余空间, 最终高度需要考虑在竖直方向上的 padding
10, 如何获取 View 的测量宽 / 高
在 measure 完成后, 可以通过 getMeasuredWidth/Height()方法, 就能获得 View 的测量宽高
在一定极端情况下, 系统需要多次 measure, 因此得到的值可能不准确, 最好的办法是在 onLayout 方法中获得测量宽 / 高或者最终宽 / 高
11, 如何在 Activity 启动时获得 View 的宽 / 高
Activity 的生命周期与 View 的 measure 不是同步运行, 因此在 onCreate/onStart/onResume 均无法正确得到
若在 View 没有测量好时, 去获得宽高, 会导致最终结果为 0
有四种办法去正确获得宽高
A. onWindowFocusChanged 获得 View 的宽 / 高
- //1. View 已经初始化完毕, 可以获得宽高
- @Override
- public void onWindowFocusChanged(boolean hasFocus) {
- super.onWindowFocusChanged(hasFocus);
- //2. Activity 得到焦点和失去焦点均会调用一次(频繁 onResume 和 onPause 会导致频繁调用)
- if(hasFocus){
- int width = view.getMeasuredWidth();
- int height = view.getMeasuredHeight();
- }
- }
B. view.post(runnable)获得 View 的宽 / 高
- //1. 通过 post 将一个 runnable 投递到消息队列尾部
- view.post(new Runnable() {
- @Override
- //2. 等到 Looper 调用次 runnable 时, View 已经完成初始化
- public void run() {
- int width = view.getMeasuredWidth();
- int height = view.getMeasuredHeight();
- }
- });
C. ViewTreeObserver 获得 View 的宽 / 高(Kotlin 版)
- val observer = imageView.viewTreeObserver
- //1. 使用 ViewTreeObserver 的接口, 可以再 View 树状态改变或者 View 树内部 View 的可见性改变时, onGlobalLayout 会被毁掉
- observer.addOnGlobalLayoutListener(object :ViewTreeObserver.OnGlobalLayoutListener {
- //2. 能正确获取 View 宽 / 高
- override fun onGlobalLayout() {
- //3. 随着 View 树状态改变, 会多次调用. 因此需要移除监听器
- imageView.viewTreeObserver.removeGlobalOnLayoutListener(this)
- val width = imageView.measuredWidth
- val height = imageView.measuredHeight
- }
- })
D. view.measure()获得 View 的宽 / 高(Kotlin)
mathc_parent 的情况下是不可以的, 因为需要知道 parent 的 size, 这里无法获取.
具体数值
- //1. 具体数值时(dp/px), 让 View 重新测量
- val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
- val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
- imageView.measure(widthMeasureSpec, heightMeasureSpec)
- //2. 完成后就可以获得宽 / 高
- val width = imageView.width
- val height = imageView.height
- wrap_content
- //1. wrap_content, 将 specSize 设置为 30 位二进制的最大值 (1 << 30) - 1, 让 View 重新测量(在 AT_MOST 情况下是合理的)
- val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 shl 30) - 1, View.MeasureSpec.AT_MOST)
- val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 shl 30) - 1, View.MeasureSpec.AT_MOST)
- imageView.measure(widthMeasureSpec, heightMeasureSpec)
- //2. 完成后就可以获得宽 / 高
- val width = imageView.width
- val height = imageView.height
Layout 过程
1,View 的 layout 过程
使用 layout 方法确定 View 本身的位置
layout 中调用 onLayout 方法确定所有子 View 的位置
2,View 的 layout()源码分析
调用 setFrame()设置 View 四个定点位置(即初始化 mLeft,mRight,mTop,mBottom 的值)
之后调用 onLayout 确定子 View 位置, 该方法类似于 onMeasure,View 和 ViewGroup 中均没有实现, 具体实现与具体布局有关.
3,LinearLayout 的 onLayout 方法
根据 orientation 选择调用 layoutVertical 或者 layoutHorizontal
layoutVertical 中会遍历所有子元素并调用 setChildFrame(里面直接调用子元素的 layout 方法)
层层传递下去完成了整个 View 树的 layout 过程
setChildFrame 中的宽 / 高实际就是子元素的测量宽 / 高(getMeasure... 后直接传入)
4,View 的测量宽高和最终宽高有什么区别?
等价于 getMeasuredWidth 和 getWidth 有什么区别
getWidth = mRight - mLeft, 结合源码测量值和最终值是完全相等的.
区别在于: 测量宽高形成于 measure 过程, 最终宽高形成于 layout 过程(赋值时机不同)
也有可能导致两者不一致: 强行重写 View 的 layout 方法, 在传参方面改变最终宽 / 高(虽然这样毫无实际意义)
某些情况下, View 需要多次 measure 才能确定自己的测量宽高, 在前几次测量中等到的值可能有最终宽高不一致. 但是最终结果上, 测量宽高 = 最终宽高
Draw 过程
1,draw 的步骤
绘制背景(drawBackground(canvas))
绘制自己(onDraw)
绘制 children(dispatchDraw)- 遍历调用所有子 View 的 draw 方法
绘制装饰(如 onDrawScollBars)
2,View 特殊方法 setWillNotDraw
若一个 View 不绘制任何内容, 需要将该标志置为 true, 系统会进行相应优化
默认 View 不开启该标志位
默认 ViewGroup 开启该标志位
如果我们自定义控件继承自 ViewGroup 并且本身不进行绘制时, 就可以开启该标志位
当该 ViewGroup 明确通过 onDraw 绘制内容时, 就需要显式关闭 WILL_NOT_DRAW 标志位.
来源: http://blog.51cto.com/14009815/2309073