这篇主要是我认为《Android开发艺术探索》第四章的重点,所以建议结合任老师的书来看,否则可能会觉得不知所云,没写的并不是说明不重要,而是我没有意识到重要性或者是我已经掌握的知识。
View的流程主要包括测量流程(measure)、布局流程(layout)、绘制流程(draw)。
View 的绘制流程是从 ViewRoot 的 performTraversals 方法开始的,它经过measure、layout、draw 三个过程才能最终将一个 View 绘制出来,其中 measure 用来测量 View的宽和高,layout 用来确定 View 在父容器中的放置位置,draw 负责将 View 绘制在屏幕上。
理解MeasureSpec
performTraversals 会依次调用 performMeasure、performLayout 和 performDraw 三个方法,这三个方法分别完成顶级 View 的 measure、layout 和 draw 这三大流程,其中在 performMeasure 中会调用 measure 方法,在 measure 方法中会调用 onMeasure 方法,在 onMeasure 方法中则会对所有的子元素进行 measure 过程,这时 measure 流程就从父容器传递到子元素中了,这样就完成了一次 measure 过程。接着子元素会重复父容器的 measure 过程,如此反复就完成了整个 View 树的遍历。同理, perforLayout 和 performDraw 的传递流程和 perforMeasure 是类似的,唯一不同的是,perforDraw 的传递过程是在 draw 方法中通过 dispatchDraw 来实现的。
SpecMode 有三类
-
UNSPECIFIED
父容器不对 View 有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
-
EXACTLY
父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的match_parent 和具体的数值这两种模式。
-
AT_MOST
父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。
对于 DecorView ,其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 共同确定;对于普通 View ,其 MesaureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定,MeasureSpec 一旦确定后,onMeasure 中就可以确定 View 的测量宽/高。
对于普通 View 的测量过程,可以查看 ViewGroup 的 getChildMeasureSpec 方法,如果不知道怎么找的话,可以在AndroidStudio写一下,然后跳转过去就可以了。如果不方便看(就是懒得看源码),可以参考下面的表格,是根据 getChildMeasureSpec 整理的。
View 的工作流程
measure过程
问:直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent。
答:如果 View 在布局中使用 wrap_content ,那么它的 specMode 是 AT_MOST 模式,在这种模式下,它的宽/高等于 specSize,而此时 view 的 specSize 是 parentSize,而 parentSize 是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小,即 View 的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用 match_parent 完全一致。
解决方法: 给 View 指定一个默认的内部宽/高( mWidth,mHeight ),并在 wrap_content 时设置此宽/高即可,对于非 wrap_content 情形,沿用系统的测量值。
@Override
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) {
setMeasuredDimension(mWidth, mHeight);
}
else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
}
else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
建议在 onLayout 方法中去获取 View 的测量宽/高或者最终宽/高。
问:在 Activity 已启动的时候获取某个 View 的宽/高。
答:
Activity/View # onWindowFocusChanged
onWindowFocusChanged 会被调用多次,当 Activity 的窗口得到焦点和失去焦点时均会被调用一次。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = mContainer.getMeasuredWidth();
int height = mContainer.getMeasuredHeight();
}
}
mContainer 为需要测量的 View。
view.post(runnable)
通过 post 可以将一个 runnable 投递到消息队列的尾部,然后等待 Looper 调用此 runnable 的时候, View 也已经初始化好了。
@Override
protected void onStart() {
super.onStart();
mContainer.post(new Runnable() {
@Override
public void run() {
int width = mContainer.getMeasuredWidth();
int height = mContainer.getMeasuredHeight();
}
});
}
ViewTreeObserver
使用 ViewTreeObserver 的众多回调都可以完成这个功能,例如使用 OnGlobalLayoutListener 这个接口,当 View 树的状态发生改变或者 View 树内部的 View 的可见效发生改变时, onGlobalLayout 方法将被回调,因此这是获取 View 的宽/高一个很好的时机,伴随着 View 树的状态改变等, onGlobalLayout 会被调用多次。
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver treeObserver = mContainer.getViewTreeObserver();
treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = mContainer.getMeasuredWidth();
int height = mContainer.getMeasuredHeight();
}
});
}
view.measure(int widthMeasureSpec, int heightMeasureSpec)
通过手动对 View 进行 measure 来得到 View 的宽/高,需要根据 View 的 LayoutParams 来分:
match_parent
无法 measure 出具体的宽/高。根据 view 的 measure 过程,构造此种 MeasureSpec 需要知道 parentSize,即父容器的剩余空间,而此时我们无法知道 parentSize 的大小,所以理论上不可能测量出 View 的大小。具体数值(dp/px)
比如宽高都是100px,如下 measure:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
mContainer.measure(widthMeasureSpec,heightMeasureSpec);
- wrap_content
如下measure:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
mContainer.measure(widthMeasureSpec,heightMeasureSpec);
注意到 (1 << 30) - 1
,通过分析 MeasureSpec 的实现可以知道,View 的尺寸使用 30 位二进制表示,也就是说最大时30个1,即2^30 -1
,也就是(1 << 30) - 1
,在最大化模式下,我们用 View 理论上能支持的最大值去构造 MeasureSpec 是合理的。
layout 过程
Layout 的作用是 ViewGroup 用来确定子元素的位置,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在 layout 方法中 onLayout 方法又会被调用,layout 方法确定 View 本身的位置,而 onLayout 方法则会确定所有子元素的位置。
问:View 的 getMeasuredWidth 和 getWidth的区别
答:在 View 的默认实现中, View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于 View 的 measure 过程,而最终宽/高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。
draw 过程
draw 的作用是将 View 绘制到屏幕上面,View 的绘制过程遵循如下步骤:
- 绘制背景 background.draw(canvas)
- 绘制自己 (onDraw)
- 绘制 children (dispatchDraw)
- 绘制装饰 (onDrawScrollBars)
View 中有一个特殊的方法 setWillNotDraw,如果一个 View 不需要绘制任何内容,那么设置这个标记为为 true 以后,系统会进行相应的优化。默认情况下, View 没有启动这个优化标记位,但是 ViewGroup 会默认启用这个优化标记位。这个标记位对实际开发的意义是:当自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个 ViewGroup 需要通过 onDraw 来绘制内容时,我们需要显式地关闭 WILL_NOT_DRAW 这个标记位。