浅答 View 绘制部分面试知识点
View 的绘制流程
答案参考自:
MeasureSpec
是什么
spec
英[spek]
n.
规格vt.
按特定标准设计并制造
MeasureSpec
参与了View
的measure
过程。在测量过程中,系统将View
的LayoutParams
根据父容器所施加的规则转换成对应的MeasureSpec
,然后再根据这个MeasureSpec
来测量出View
的测量宽/高。
MeasureSpec
代表一个32位的int
值,高两位代表 测量模式SpecMode
,低30位代表 该测量模式下的规格大小SpecSize
。
MeasureSpec
与SpecMode
、SpecSize
之间可以通过位运算互相求出。
每个View
的measure
过程之前都会先计算其MeasureSpec
的值,然后再对其进行measure
。
注意:View
的宽和高各有一个MeasureSpec
值,分别为widthMeasureSpec
和heightMeasureSpec
。
SpecMode
的三种状态
UNSPECIFIED
父容器不对
View
做任何的限制,要多大给多大。这种情况一般用于系统的内部,表示一种测量方式的状态。EXACTLY
父容器已经测出了
View
所需要的精确大小,此时的大小就是View
的最终大小。它对应于
LayoutParams
的match_parent
和具体的数值这两种模式。AT_MOST
父容器指定了一个可用的大小,
View
的大小不能大于这个值。它对应于
LayoutParams
中的wrap_content
。
子View创建MeasureSpec
创建规则是什么
MeasureSpec
和LayoutParams
的对应关系
对于DecorView
和普通View
来说,MeasureSpec
的转化过程略有不同。
对于DecorView
:
DecorView
的MeasureSpec
由窗口的大小和DecorView
自身的LayoutParams
共同决定的。
通过在ViewRootImpl
中的measureHierarchy
方法中,调用getRootMeasureSpec
方法,获得DecorView
的MeasureSpec
。
其中getRootMeasureSpec
方法中,根据自身LayoutParams
的不同大小,共有三种情况:
LayoutParams.MATCH_PARENT
:精确模式,大小就是窗口的大小。LayoutParams.WRAP_CONTENT
:最大模式,大小不定,但是不能超过窗口的大小。- 固定大小(比如100dp):精确模式,大小为
LayoutParams
中指定的大小。
对于普通View
:
普通View
的MeasureSpec
由父容器的MeasureSpec
和自身的LayoutParams
共同决定的。
通过在父容器的measureChildWithMargins
中的getChildMeasureSpec
方法获得View
的MeasureSpec
。
其中getChildMeasureSpec
方法的参数中使用到了父容器的MeasureSpec
。在这个方法中通过switch
和if
语句,对View
的MeasureSpec
进行了决定。结果如下:
↓childLayoutParams \ parentMeasureSpec → |
EXACTLY |
AT_MOST |
UNSPECIFIED |
---|---|---|---|
dp /px |
EXACTLY childSize |
EXACTLY childSize |
EXACTLY childSize |
match_parent |
EXACTLY parentSize |
AT_MOST parentSize |
UNSPECIFIED 0 |
wrap_content |
AT_MOST parentSize |
AT_MOST parentSize |
UNSPECIFIED 0 |
上图可以简单记为:除了dp
/px
的精确模式情况外,三种MeasureSpec
的级别为:EXACTLY
< AT_MOST
< UNSPECIFIED
且childMeasureSpec
= max(childLayoutParams
, parentMeasureSpec
)。(实际上并无级别之分)
通过上面的表格,可以更加直观的看出:普通View
的MeasureSpec
由父容器的MeasureSpec
和自身的LayoutParams
共同决定的。
自定义View wrap_content
不起作用的原因
对于直接继承View
的自定义View
来说,需要重写onMeasure
方法并设置在wrap_content
情况时的自身大小,否则在布局中使用wrap_content
就相当于使用match_parent
。
因为当自定义View
处于wrap_content
时,它的SpecMode
为AT_MOST
模式,由前文可知,当父容器无论为AT_MOST
还是ECACTLY
模式时,自定义View
都为AT_MOST
模式且尺寸大小为parentSize
,也就是父容器的剩余空间,即自定义View
的大小变得与父容器的剩余空间大小一致,显然不是我们需要的。
解决方法:
解决方法也是非常简单的。我们只需要在onMeasure
方法中对
- 仅
width
方向为AT_MOST
模式 - 仅
height
方向为AT_MOST
模式 width
方向以及height
方向都为AT_MOST
模式- 其他的情况
这四个状态单独判断,分别使用setMeasuredDimension
方法直接为自定义View
设置我们需要的值即可。
源码中对于TextView
、ImageView
等控件的wrap_content
也做了特殊的处理。
在Activity中获取某个View的宽高有几种方法
由于Activity
的启动和View
的测量过程并不是同步进行的,因此无法保证在Activity
启动时某个View
已经测量完毕了。所以我们在Activity
启动的时候,如果直接通过getMeasuredWidth
/getMeasuredHeight
方法获得的值可能为0。
以下有四种方法可以解决这个问题(详细代码见《Android开发艺术探索》190页)。
Activity/View#onWindowFocusChanged
在
Activity
的onResume
和onPause
方法启动时,onWindowFocusChanged
方法也会同时被调用,可以重写里面的内容,使之计算View
的宽高。注意:
onWindowFocusChanged
方法会在View
已经初始化完毕后才开始调用。view.post(runnable)
通过
post
可以将一个runnable
投递都消息队列的尾部,然后等待Looper
调用此runnable
的时候,View
也已经初始化好了。ViewTreeObserver
ViewTreeObserver
中的众多回调可以完成这个功能。比如
OnGlobalLayoutListener
接口,当View
树的状态发生改变或者View
树内部的View
的可见性发生改变的时候,onGlobalLayout
方法将被回调,此时就可以获取View
的宽高了。view.measure(int widthMeasureSpec, int heightMeasureSpec)
可以主动调用该方法开始计算
view
的宽高。match_parent
由于正常的
measure
过程中,我们需要父容器的MeasureSpec
的值,而此时父容器并没开始计算,我们无从得知这两个值的大小。故我们无法对match_parent
的view
计算其宽高。dp
/px
因为有精确的数值了,所以我们可以直接使用这个精确的数值通过
MeasureSpec.makeMeasureSpec
方法构建view
的MeasureSpec
。wrap_content
直接将
MeasureSpec.makeMeasureSpec
方法中参数的值设置为(1<<30)-1
即可,这是View
理论上可以支持的最大值,所以这样构建MeasureSpec
是合理的。
onCreate
、onResume
中可以获取View的宽高吗?怎么做?
View#post
为什么可以获取?
答案参考自:
onCreate
、onResume
中可以获取 View 的宽高吗?怎么做?
View 的测绘绘制流程就是从 ViewRootImpl#performTraversals 开始的,而这个方法的调用是在 onResume 方法之后,所以在 onCreate 和 onResume 方法中拿不到 View 的测量值。
View 的宽高是在 onLayout 阶段才能最终确定的,而在 Activity#onCreate 中并不能保证 View 已经执行到了 onLayout 方法,也就是说 Activity 的声明周期与View的绘制流程并不是一一绑定。所以 onCreate 和 onResume 中获取不到 View 的宽高值。以 Handler 为基础,View#post 将传入任务的执行时机调整到 View 绘制完成之后。
View#post
为什么可以获取?
1 |
|
这样写一般是在 Activity 的 onResume 方法中,因为 onResume 执行在 View 初始化之前,如果在 onResume 中直接获取 View 宽高是获取不到的。
使用 view#post 就能获取到,因为 view#post 是向主 Handler 的 MessageQueue 中插入一条待执行消息,但是因为系统在 ViewRoot 中初始化 View 时也是利用 Handler 机制,平且为了优先执行 View 的初始化设置了同步屏障,导致 view#post 插入的消息会在 View 初始化之后执行,那么肯定就能获取到 View 的宽高啦!
Runnable
的执行时机具体是什么
在 Android 7.0 之后,view.post()中的runnbale 能确定被执执行。具体来说:
Android 7.0之后,除了performTraversal
中会调用外,在View的dispatchAttachedToWindow
中也会调用,但Android 7.0之后不管在主线程还是在子线程都可以成功执行view.post
内部逻辑,并不是因为增加了调用时机,而是取消了ThreadLocal
机制,使得 不管在主线程还是子线程调用view.post方法,都会将runnable对象丢到主线程的任务队列中,更新UI或者获取view的信息。
View#post
与Handler#post
的区别
答案参考自:
Handler.post
,它的执行时间基本是等同于onCreate
里那行代码触达的时间;View.post,则不同,它说白了执行时间一定是在
Activity#onResume
发生后才开始算的;或者换句话说它的效果相当于你上面的View.post
方法是写在Activity#onResume
里面的(但只执行一次,因为onCreate
不像onResume会被多次触发);当然,虽然这里说的是
post
方法,但对应的postDelayed
方法区别也是类似的。
getWidth()
方法和getMeasureWidth()
方法的区别
答案参考自:
getMeasureWidth()
方法在measure()
过程结束后就可以获取到了,而getWidth()
方法要在layout()
过程结束后才能获取到。getMeasuredWidth()
获取的是View
原始的大小,也就是这个View
在XML
文件中配置或者是代码中设置的大小。getWidth()
获取的是这个View
最终显示的大小,这个大小有可能等于原始的大小,也有可能不相等。只要在代码里重新修改了子控件的摆放位置,getWidth()
和getMeasureWidth()
的值就会不同。getMeasureWidth()
方法中的值是通过setMeasuredDimension()
方法来进行设置的,而getWidth()
方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。
View加载流程(setContentView)
答案参考自:
通过Activity的setContentView方法间接调用PhoneWindow的setContentView(),在PhoneWindow中通过getLayoutInflate()得到LayoutInflate对象。
通过LayoutInflate对象去加载View,主要步骤是:
- 通过xml的Pull方式去解析xml布局文件,获取xml信息,并保存缓存信息,因为这些数据是静态不变的。
- 根据xml的tag标签通过反射创建View逐层构建View。
- 递归构建其中的子View,并将子View添加到父ViewGroup中。
其中,有四种加载XML文件的常用方法:
使用view的静态方法
1
View view = View.inflate(context, R.layout.child, null);
通过系统获取
1
2LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.child, null);通过LayoutInflater
1
2LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.child, null);通过getLayoutInflater
1
View view = getLayoutInflater().inflate(R.layout.child, null);
View 加载过程结束后,便会开始 View 的绘制流程了。
invalidate()
和 postInvalidate()
的区别
这两个方法都是在重绘当前控件的时候调用的。
invalidate在UI线程中调用,postInvalidate在非UI线程中调用。 因为android的UI线程是非线程安全的,所以在非UI线程中,需要使用postInvalidate来使View重绘。
view调用invalidate将导致当前view的重绘(draw调用),view的父类将不会执行draw方法;viewGroup调用invalidate会使viewGroup的子view调用draw,也就是viewGroup内部的子view进行重绘。
requestLayout()
和 onLayout()
的区别
requestLayout()
requestLayout
方法只会**导致当前view
的measure
和layout
**,而draw
不一定被执行,只有当view
的位置发生改变才会执行draw
方法,因此如果要使当前view
重绘需要调用invalidate
。
onLayout()
在很多情况下requestLayout是不需要被调用的。
例如,我们把一个AbsoluteLayout里面的childView挪动一下位置。我们仅仅需要调用的可能就是重新布局当前AbsoluteLayout,然后调用invalidate方法进行重绘。而不是从当前View向上的整个View树形结构都要重新layout,onLayout,measure,onMeasure一次。在这种情况下可以直接调用onLayout。然后调用invalidate进行重绘。很明显可以提升绘制效率。
由于父View的layout实现了会通知布局的listener。但是由于无法得到listener,因此调用onlayout的时候无法对其进行通知,这也是这种实现的缺陷。
自定义 View 的流程和注意事项
答案参考自:
TODO
- invalidate怎么局部刷新
- Android绘制和屏幕刷新机制原理`