目录
- 1 初识 ViewRoot 和 DecorView
- 2 理解 MeasureSpec
- 3 View的工作流程
- 3.1 View 类和 ViewGroup 类的区别和联系是什么?
- 3.2 简要说一下 View 的工作流程
- 3.3 简要说一下 measure 过程
- 3.4 View 的子类可以重写 View 类的 measure 方法吗?
- 3.5 为什么说在 onLayout 方法中去获取 View 的测量宽/高是一个好的习惯?
- 3.6 在 Activity 启动时,如何获取某个 View 的宽/高?
- 3.7 简要说一下 layout 过程
- 3.8 layout 方法和 onLayout 方法的区别是什么?
- 3.9 View 的 getMeasuredWidth 和 getWidth 这两个方法有什么区别?
- 3.10 View 的绘制过程的步骤是什么?
- 3.11 ViewGroup 的 draw 过程是如何传递的?
- 3.12 View 类中的 setWillNotDraw 方法的含义及其开发意义是什么?
- 4 自定义 View
- 参考
1 初识 ViewRoot 和 DecorView
1.1 ViewRootImpl 类的作用是什么?
ViewRoot对应于ViewRootImpl类,是连接WindowManager和DecorView的纽带,更广一点可以说是Window和View之间的纽带。换句话说,Window需要通过ViewRootImpl才能与View建立联系。ViewRoot这个类目前已经没有了,是老版本中的一个类,在 Android2.2 以后用ViewRootImpl代替ViewRoot。ViewRootImpl完成View的三大流程,包括measure、layout和draw过程。ViewRootImpl向DecorView分发收到的由用户发起的输入事件,包括按键事件(KeyEvent)和点击事件(MotionEvent)。
1.2 ViewRootImpl 对象是如何和 DecorView 关联起来的?
在调用 startActivity 方法启动Activity后,最终会调用到 ActivityThread 的 handleLaunchActivity() 方法中:
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
// 初始化 WindowManagerService
WindowManagerGlobal.initialize();
// 完成 Activity对象 的创建,调用 Activity 对象的 attach 方法和onCreate 方法
Activity a = performLaunchActivity(r, customIntent);
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
Bundle oldState = r.state;
// 调用 Activity 对象的 onResume 方法,把 DecorView 对象添加到 Window 中。
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed);
}
}
需要说明的是,handleLaunchActivity 方法中的注释中是一个总的说明,下面进行详细地说明:
在调用 Activity 的 attach 方法中,创建了 mWindow 对象:
mWindow = new PhoneWindow(this);
在 Activity 的 onCreate 方法中,通过 setContentView 方法,会创建一个 DecorView 对象:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
在 PhoneWindow 中会创建 DecorView 对象:
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
在 handleResumeActivity 中,在调用完 Activity 对象的 onResume 方法之后,会把 DecorView 对象添加到 Window 里面:
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
// 把 DecorView 对象添加到 Window 中,真正调用的是 WindowManagerImpl 的 addView 方法。
wm.addView(decor, l);
在 WindowManagerImpl 中调用 addView 方法,内部 WindowManagerGlobal 的 addView 方法:
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
在 WindowManagerGlobal 的 addView 方法中,会创建一个 ViewRootImpl 对象,并调用ViewRootImpl 对象的 setView 方法,就把 DecorView 对象(view 对象就是 DecorView 对象)和 ViewRootImpl 对象关联起来了:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
}
1.3 顶级 View 的整体绘制流程是什么?
顶级 View 的绘制流程是从 ViewRootImpl 的 performTraversals() 方法开始的,performTraversals() 方法经过 measure、layout 和 draw 三个过程才能最终把一个 View 绘制出来。
- measure 过程用来测量
View的宽和高; - layout 过程用来确定
View在父容器中的放置位置,即确定View的四个顶点的坐标和实际的View的宽/高; - draw 过程负责将
View绘制在屏幕上。
performTraversals() 的工作流程如下:
2 理解 MeasureSpec
2.1 说一说对于 MeasureSpec 的理解?
- 从类上看,
MeasureSpec类是View类的内部类,它封装了由父容器传递给子元素的布局要求,这个值在很大程度上决定了一个View的尺寸规格,之所以说很大程度上是因为这个过程还受父容器的影响,因为父容器影响View的MeasureSpec的创建过程。具体来说,在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个MeasureSpec来测量出View的宽和高; - 从值上看,
MeasureSpec代表一个 int 值(32位),高 2 位代表 SpecMode(指测量模式),低 30 位代表 SpecSize(指在某种测量模式下的规格大小,表示尺寸大小的参考值或者建议值)。public static class MeasureSpec { private static final int MODE_SHIFT = 30; // 对应的二进制是 11000000 00000000 00000000 00000000,作用是打包和解包 MeasureSpec 值。 private static final int MODE_MASK = 0x3 << MODE_SHIFT; // 对应的二进制是 00000000 00000000 00000000 00000000, // 未指定模式:父容器不对 View 有任何限制,要多大给多大,父容器对 View 说,我不管你,你使劲造吧,比如 ScrollView 作为父容器,在高度方向会给子 View 这个模式。 public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 对应的二进制是 01000000 00000000 00000000 00000000, // 精确模式:父容器已经检测出 View 所需要的精确大小 public static final int EXACTLY = 1 << MODE_SHIFT; // 对应的二进制是 10000000 00000000 00000000 00000000, // 最大模式:父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。 public static final int AT_MOST = 2 << MODE_SHIFT; public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { // (size & ~MODE_MASK) 表示取出 size 这个 int 值的低 30 位 // (mode & MODE_MASK) 表示取出 mode 这个 int 值的高 2 位 // (size & ~MODE_MASK) | (mode & MODE_MASK) 表示打包一个 MeasureSpec 值,也是 int 型。 return (size & ~MODE_MASK) | (mode & MODE_MASK); } } public static int getMode(int measureSpec) { // 从 MeasureSpec 值中解包出 SpecMode 这个 int 值 return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { // 从 MeasureSpec 值中解包出 SpecSize 这个 int 值 return (measureSpec & ~MODE_MASK); } static int adjust(int measureSpec, int delta) { final int mode = getMode(measureSpec); if (mode == UNSPECIFIED) { // No need to adjust size for UNSPECIFIED mode. return makeMeasureSpec(0, UNSPECIFIED); } int size = getSize(measureSpec) + delta; if (size < 0) { Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size + ") spec: " + toString(measureSpec) + " delta: " + delta); size = 0; } return makeMeasureSpec(size, mode); } // 打印一个 MeasureSpec 里的 specMode 和 specSize 信息。这个方法好用。 public static String toString(int measureSpec) { int mode = getMode(measureSpec); int size = getSize(measureSpec); StringBuilder sb = new StringBuilder("MeasureSpec: "); if (mode == UNSPECIFIED) sb.append("UNSPECIFIED "); else if (mode == EXACTLY) sb.append("EXACTLY "); else if (mode == AT_MOST) sb.append("AT_MOST "); else sb.append(mode).append(" "); sb.append(size); return sb.toString(); } }
2.2 对于顶级 View(如 DecorView)和普通 View 来说,获取传入它们的 measure 方法的 MeasureSpec 的方式有什么不同?
对于顶级 View(如 DecorView),其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 来共同确定,可以查看
ViewRootImpl中的measureHierarchy方法(这是一个测量协商方法),有如下代码:// getRootMeasureSpec 传入期望的窗口宽度和窗口自身的LayoutParams的宽度,获取宽度的MeasureSpec childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); // getRootMeasureSpec 传入期望的窗口高度和窗口自身的LayoutParams的高度,获取高度的MeasureSpec childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); // 执行测量,内部就是调用了 View 的 measure 方法。 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);我们主要去看看
getRootMeasureSpec方法的内部实现:private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; }绘制出表格如下:

对于普通
View,View的 measure 过程由其父容器ViewGroup传递而来,可以查看ViewGroup的measureChild方法:protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); // getChildMeasureSpec 传入父容器的宽度MeasureSpec,父容器已用的空间(这里是父容器的左右padding,子 View 的左右 margin,父容器宽度方向已用尺寸),子元素的LayoutParams宽度 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); // getChildMeasureSpec 传入父容器的高度MeasureSpec,父容器已用的空间(这里是父容器的上下padding,子 View 的上下 margin,父容器高度方向已用尺寸),子元素的LayoutParams高度 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }可以看到,子
View需要的MeasureSpec由父容器的MeasureSpec和子View自身的LayoutParams来共同确定,还和父容器的 padding,子View的 margin,父容器已用空间 有关。getChildMeasureSpec方法的过程如下面表格所示:
其中,parentSize 是指父容器中目前可以使用的大小。需要说明的是,当子View的布局参数为 match_parent 或者 wrap_content,并且父容器的测量模式是UNSPECIFIED,这时给子 View 的测量尺寸是 0 或者 parentSize,这是由View.sUseZeroUnspecifiedMeasureSpec来决定的。
怎么看上面的表格呢?
艺术探索里面是一行一行地看。按照代码流程来看的话,是一列一列地看。但是,千万不要斜着看啊。
这里也是采用一行一行地看。
当子 View 采用具体的宽/高时,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精确模式+子 View 的 LayoutParams 中指定的具体的大小。
当子 View 的宽/高采用 match_parent 时,这时候父容器的 MeasureSpec 会发挥作用,子 View 的模式总是跟父容器的模式一样:
- 如果父容器的模式是精确模式(
EXACTLY),那么子View的MeasureSpec就是精确模式+父容器的剩余空间(或者说父容器的可用空间); - 如果父容器的模式是最大模式(
AT_MOST),那么子View的MeasureSpec就是最大模式+父容器的剩余空间(或者说父容器的可用空间); - 如果父容器的模式是未指定模式(
UNSPECIFIED),那么子View的MeasureSpec就是未指定模式+父容器的剩余空间或者0。
当子 View 的宽高采用 wrap_content 时,这时候父容器的 MeasureSpec 同样会发挥作用:
- 如果父容器的模式是精确模式(
EXACTLY),那么子View的MeasureSpec就是最大模式+父容器的剩余空间(或者说父容器的可用空间); - 如果父容器的模式是最大模式(
AT_MOST),那么子View的MeasureSpec就是最大模式+父容器的剩余空间(或者说父容器的可用空间); - 如果父容器的模式是未指定模式(
UNSPECIFIED),那么子View的MeasureSpec就是未指定模式+父容器的剩余空间或者0。
需要特别总结说明的是,当子 View 的宽高采用 wrap_content 时,不管父容器的模式是精确模式还是最大模式,子 View 的模式总是最大模式+父容器的剩余空间。
2.3 当 View 声明了 match_parent,那么 mode 一定是 EXACTLY,当 View 声明了 wrap_content,那么 mode 一定是 AT_MOST,这种说法正确吗?
对于顶层 View 来说,是正确的;对于普通 View 来说,是错误的。
参考:每日一问 | 自定义控件测量模式真的和 match_parent,wrap_content 一一对应吗
3 View的工作流程
3.1 View 类和 ViewGroup 类的区别和联系是什么?
View 类是一个具体类,也就是说它没有任何抽象方法;
ViewGroup 类继承于 View 类,是一个抽象类,包含一个抽象方法:
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
3.2 简要说一下 View 的工作流程
View 的工作流程主要是指 measure、layout、draw 这三大流程,即测量、布局和绘制。measure是确定 View 的测量宽/高,layout 是确定 View 的最终宽/高和四个顶点的位置,draw 是将 View 绘制到屏幕上。
3.3 简要说一下 measure 过程
measure 过程分为 View 的 measure 过程和 ViewGroup 的 measure 过程。
View 的 measure 过程
如果是一个原始的 View,那么通过 measure 方法就完成了测量过程,在 measure 方法中会去调用 View 的 onMeasure 方法,View 类里面定义了 onMeasure 方法的默认实现(我们需要理解默认实现,以便自定义 View 的时候清楚默认实现的作用或者在默认实现的基础上作进一步的修改):
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
源码中的写法很紧凑,我们把这个方法稍微改动一下,便于分步骤查看:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 先获取建议的最小宽度和高度
int suggestedMinimumWidth = getSuggestedMinimumWidth();
int suggestedMinimumHeight = getSuggestedMinimumHeight();
// 再通过 getDefaultSize 方法获取宽度和高度的测量值
int measuredWidth = getDefaultSize(suggestedMinimumWidth, widthMeasureSpec);
int measuredHeight = getDefaultSize(suggestedMinimumHeight, heightMeasureSpec);
// 最后调用 setMeasuredDimension 方法设置 View 宽度和高度的测量值。
setMeasuredDimension(measuredWidth, measuredHeight);
}
先看一下 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 方法的源码:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
我们只看 getSuggestedMinimumWidth 方法:如果 View 的背景 mBackground == null 成立,即 View 没有设置背景,那么 View 的建议最小宽度就是 mMinWidth(mMinWidth 对应于 android:minWidth 这个属性所指定的值);如果 View 设置了背景,那么 View 的建议最小宽度为 mWidth 和 mBackground.getMinimumWidth() 中较大的那个。mBackground 是一个 Drawable 对象,所以去看Drawable 类下的 getMinimumWidth 方法:
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
可以看到,getMinimumWidth 方法获取的是 Drawable 的原始宽度。如果存在原始宽度(即满足 intrinsicWidth > 0),那么直接返回原始宽度即可;如果不存在原始宽度(即不满足 intrinsicWidth > 0),那么就返回 0。
ShapeDrawable没有原始宽度和高度,而BitmapDrawable有原始宽度和高度。
接着看最重要的 getDefaultSize 方法:
// 这是一个 static 修饰的方法,所以是一个工具方法
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
如果 specMode 为 MeasureSpec.UNSPECIFIED 即未指定模式,那么返回由方法参数传递过来的尺寸(即默认尺寸,或者说指定的尺寸,内部提供的尺寸)作为 View 的测量宽度和高度;
如果 specMode 不是 MeasureSpec.UNSPECIFIED 即是最大模式或者精确模式,那么返回从 measureSpec 中取出的 specSize 作为 View 测量后的宽度和高度。
注意到注释里的一句话:
Utility to return a default size. Uses the supplied size if the
MeasureSpec imposed no constraints. Will get larger if allowed
by the MeasureSpec.
返回一个默认尺寸的工具方法。如果 MeasureSpec 没有施加任何限制,就使用提供的尺寸,否则将获取 MeasureSpec 所允许的更大的尺寸。
为什么注释里会说 specMode 不是 MeasureSpec.UNSPECIFIED 即是最大模式或者精确模式时,获取到的测量宽高会较大呢?
还是看一下这个表格:
当 specMode 为 EXACTLY 或者 AT_MOST 时,View 的布局参数为 wrap_content 或者 match_parent 时,给 View 的 specSize 都是 parentSize。这会比建议的最小宽高要大。
但是,注意到当 specMode 为 EXACTLY 或者 AT_MOST, 而View 的布局参数为 wrap_content 和 View 的布局参数为 match_parent 的效果是一样的,最后都会给 View 的 specSize 为 parentSize。这是不符合我们的预期的。
直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小(给 View 指定一个默认的内部宽高)。具体处理方式没有固定的依据,根据需要灵活指定。可以查看 TextView 、ImageView、ProgressBar、Space 等源码针对 wrap_content 情形的处理方式。
ViewGroup 的 measure 过程
如果是一个 ViewGroup,除了完成自己的 measure 过程以外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行 measure 过程。
自定义ViewGroup 完成自己的 measure 过程是指在 onMeasure 方法里调用了 setMeasuredDimension(int measuredWidth, int measuredHeight) 方法,参数分别是 ViewGroup 的测量宽度和测量高度。
有的自定义 ViewGroup 是先完成自己的 measure 过程,再去完成子元素的 measure 过程,如 ViewPager,伪代码如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(自定义ViewGroup的测量宽度,自定义ViewGroup的测量高度);
// 测量每一个子View
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
有的自定义 ViewGroup 是先完成子元素的 measure 过程,再完成自己的 measure 过程,大多数的自定义 ViewGroup 都是这样的,伪代码如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 测量每一个子View
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
setMeasuredDimension(自定义ViewGroup的测量宽度,自定义ViewGroup的测量高度);
}
ViewGroup 并没有重写 View 的 onMeasure 方法,但是它提供了 measureChildren、measureChild、measureChildWithMargins 这几个方法专门用于测量子元素。
// 遍历所有的子元素,并使用 measureChild 方法来对每一个子元素进行 measure。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
// 只有当子元素的可见性不是 GONE 时,才对它进行测量。这一点是个细节。
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
// 测量每一个子元素:最重要的逻辑是通过 getChildMeasureSpec 方法来获取测量子元素所需要的宽度 MeasureSpec 和 高度 MeasureSpec。
// getChildMeasureSpec(int spec, int padding, int childDimension) 方法需要的参数必须搞清楚:
// spec 是 ViewGroup 从 onMeasure 方法接收到的 MeasureSpec,
// padding 是 ViewGroup 已使用的空间,childDimension 是子元素的布局参数的宽和高。
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
为什么ViewGroup 类没有像 View 一样对其 onMeasure 方法做统一的实现,实际上 ViewGroup 继承了 View 类对 onMeasure 方法的实现?这是因为不同的 ViewGroup 子类有不同的布局特性,这导致它们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout 这两者的布局特性显然不同,因此 ViewGroup 无法统一实现。
3.4 View 的子类可以重写 View 类的 measure 方法吗?
不可以,这是因为 View 类中的 measure 方法是一个 final 类型的方法。
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
3.5 为什么说在 onLayout 方法中去获取 View 的测量宽/高是一个好的习惯?
正常情况下,View 的 measure 过程完成以后,通过 getMeasuredWidth/getMeasuredHeight 方法就可以正确地获取到 View 的测量宽/高;但是,在某些极端情况下,系统可能需要多次 measure 才能确定最终的测量宽/高,在这种情形下,在 onMeasure 方法中拿到的测量宽/高很可能是不准确的。
3.6 在 Activity 启动时,如何获取某个 View 的宽/高?
| 方式 | 解释 |
|---|---|
Activity/View 的 onWindowFocusChanged | 在 onWindowFocusChanged 回调时,表示 View 已经初始化完毕了,宽/高已经准备好了,所以这时去获取宽/高是没有问题的。但是,onWindowFocusChanged 会被多次调用,当 Activity 的窗口得到焦点和失去焦点时均会被调用一次。 |
View.post(Runnable runnable) | 通过 post 可以将一个 Runnable 对象放到消息队列的尾部,然后等到 Looper 调用此 Runnable 的时候,View 也已经初始化好了。注意:是 View.post 方法而不是 Handler.post 方法。参考:每日一问 在Activity 的 onResume 方法中 view.postRunnable 能获取到 View 宽高吗?,每日一问 在Activity 的 onResume 方法中 handler.postRunnable 能获取到 View 宽高吗? |
ViewTreeObserver 的多个回调,如 OnGlobalLayoutListener 接口。 | OnGlobalLayoutListener 接口:当 View 树的状态发生改变或者 View 树内部的 View 的可见性发生改变时,它的 onGlobalLayout 方法将被回调。需要注意的是,伴随着 View 树的状态改变等,onGlobalLayout 会被调用多次。使用完接口后,记得移除。 |
使用 View.measure(int widthMeasureSpec, int heightMeasureSpec) 方法进行手动测量 | 这种方法比较复杂。当 View 的 LayoutParams 为 match_parent 时,这种方式不能测量出具体的宽/高;当 View 的 LayoutParams 为具体的数值(如 100 dp)时,这种方式可以测量出具体的宽/高;当 View 的 LayoutParams 为 wrap_content 时, 这种方式不能测量出具体的宽/高。 |
3.7 简要说一下 layout 过程
如果是 View 的话,那么在它的 layout 方法中就确定了自身的位置(具体来说是通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft,mRight,mTop,mBottom 这四个值),layout 过程就结束了。
如果是 ViewGroup 的话,那么在它的 layout 方法中只是确定了 ViewGroup 自身的位置,要确定子元素的位置,就需要重写 onLayout 方法;在 onLayout 方法中,会调用子元素的 layout 方法,子元素在它的 layout 方法中确定自己的位置,这样一层一层地传递下去完成整个 View 树的 layout 过程。
3.8 layout 方法和 onLayout 方法的区别是什么?
layout 方法的作用是确定 View 本身的位置,即设定 View 的四个顶点的位置,这样就确定了 View 在父容器中的位置;
onLayout 方法的作用是父容器确定子元素的位置,这个方法在 View 中是空实现,因为 View 没有子元素了,在 ViewGroup 中则进行抽象化,它的子类必须实现这个方法。
3.9 View 的 getMeasuredWidth 和 getWidth 这两个方法有什么区别?
getMeasuredWidth获取的是测量宽度,定义了一个View想要在父View里的尺寸,getWidth获取的是宽度,有时也被称为绘制宽度,定义了绘制时或者布局后屏幕上的View的实际尺寸。两者的赋值时机不同,测量宽/高的赋值时机要早于最终宽/高。具体来说,
View的测量宽/高形成于View的measure过程,可以看measure方法中的setMeasuredDimensionRaw方法:private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }而
getMeasuredWidth获取的就是mMeasuredWidth值:public final int getMeasuredWidth() { return mMeasuredWidth & MEASURED_SIZE_MASK; }再看一下
getWidth方法:public final int getWidth() { return mRight - mLeft; }而
mRight,mLeft的赋值是在layout过程的setFrame方法中,所以View的最终宽/高形成于View的layout过程。两者的值多数情况下是相等的,但在某些特殊情况下会不一致。例如有两种特殊情况:
- 重写
View的layout方法:手动修改传入 layout 方法的参数值; View需要多次measure才能确定自己的测量宽/高,在前几次的测量过程中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来看,测量宽/高还是会和最终宽/高相同。
- 重写
参考:每日一问 | getWidth, getMeasuredWidth 有什么区别?
3.10 View 的绘制过程的步骤是什么?
- 绘制背景(background.draw(canvas););
- 绘制自己(onDraw);
- 绘制 children(dispatchDraw(canvas));
- 绘制装饰(onDrawScrollBars)。
dispatchDraw 方法的调用是在 onDraw 方法之后,也就是说,总是先绘制自己再绘制子 View
对于 View 类来说,dispatchDraw 方法是空实现的,对于 ViewGroup 类来说,dispatchDraw 方法是有具体实现的。
3.11 ViewGroup 的 draw 过程是如何传递的?
通过 dispatchDraw 来传递的。dispatchDraw 会遍历调用子元素的 draw 方法,如此 draw 事件就一层一层传递了下去。dispatchDraw 在 View 类中是空实现的,在 ViewGroup 类中是真正实现的。
3.12 View 类中的 setWillNotDraw 方法的含义及其开发意义是什么?
含义:如果一个 View 不需要绘制任何内容,那么就设置这个标记为 true,系统会进行进一步的优化。默认地,View 没有启用这个标记,但是View 的子类例如 ViewGroup 可以启用这个标记。特别地,如果覆写了 onDraw 方法,就要清除这个标记。
开发意义:当创建的自定义控件继承于 ViewGroup 并且不具备绘制功能时,就可以开启这个标记,便于系统进行后续的优化;当明确知道一个 ViewGroup 需要通过 onDraw 绘制内容时,需要关闭这个标记。
查看 LinearLayout 对这个方法的调用:setWillNotDraw(divider == null);,在有 divider 时才会关闭这个标记,否则是打开的;ScrollView 直接使用 setWillNotDraw(false);;ViewStub 类则使用 setWillNotDraw(true);。
4 自定义 View
4.1 自定义 View 的分类
| 分类 | 用途 | 特点 |
|---|---|---|
| 1.继承 View 重写 onDraw 方法 | 用于实现一些不规则的效果,不方便通过布局的组合方式来达到 | 需要通过绘制的方式来实现,即重写 onDraw 方法;需要自己支持 wrap_content,处理 padding |
| 2.继承 ViewGroup 派生特殊的 Layout | 用于实现自定义的布局 | 稍微复杂一些,需要合适地处理 ViewGroup 的测量、布局这两个过程,并同时处理好子元素的测量和布局过程 |
| 3.继承特定的 View | 用于扩展已有的 View 的功能 | 不需要自己支持 wrap_content 和 padding 等 |
| 4.继承特定的 ViewGroup | 用于实现几种 View 组合在一起的效果 | 不要自己处理 ViewGroup 的测量和布局这两个过程,方法2能实现的效果一般方法4也可以实现,但方法2更接近 View 底层 |
4.2 自定义 View 的注意事项
- 对于直接继承
View或者ViewGroup的控件,要在onMeasure方法中对wrap_content做特殊处理; - 必要时,让自定义
View支持padding; - 尽量不要在自定义
View中使用Handler,用View自己的post方法就行; - 及时关闭自定义
View中的线程或者动画,比如在onDetachedFromWindow方法中; - 自定义
View带有嵌套滑动时,要处理好滑动冲突; - 有自定义布局属性的,在构造方法中取得属性后应及时调用recycle方法回收资源;
- 在
onDraw和onTouchEvent方法中都应避免创建对象,过多操作会造成卡顿; - 自定义
ViewGroup要重载关于 LayoutParams 的几个方法; - 必要时添加对
View的状态存储与恢复的支持; - 对于自定义
ViewGroup,需要绘制内容但是又没有在布局中设置background的话,会画不出来,这时候需要调用setWillNotDraw方法,并设置为false。
参考
Android中的ViewRootImpl类源码解析
详细介绍了 ViewRootImpl 这个类的作用,非常全面深入,非常耐看。Android中MotionEvent的来源和ViewRootImpl-任玉刚
任玉刚大佬亲自操刀分析点击事件的来源,补充开发艺术探索里没有的部分,你懂的。Android Framework 输入子系统 (09)InputStage解读
本文详细说明 InputStage 的含义以及使用的责任链模式。Android开发之漫漫长途 Ⅴ——Activity的显示之ViewRootImpl的PreMeasure、WindowLayout、EndMeasure、Layout、Draw
这是我读到的详细说明 ViewRootImpl 的绘制流程的文章。ViewRootImpl的performDraw过程
开发艺术探索里面没有说明 performDraw 的具体过程,只是一笔带过,详细了解可以看这篇文章。Android View的绘制流程 - 屈超
同样是阅读Android开发艺术探索,看大神是如何总结的。带你一步步了解 onDraw() 和 dispatchDraw() 的区别
作者通过一个例子直观地展示了 onDraw 和 dispatchDraw 的区别,学习多动手。小知识又来了!ViewGroup onDraw为什么不调用?
作者详细从小例子入手,深入源代码分析了 ViewGrou onDraw 为什么不调用,怎样才能调用。View 测量算法我知道
哎呀,我不知道啊,赶紧学习一波儿吧。【Android源码解析】View.post()到底干了啥
涉及到同步屏障的知识。什么?View.post()没执行?!
一个细节的知识点。每日一问 | 谈一下自定义 View 的流程-玩Android
洋神,陈小缘,JsonChao 大佬们在讨论自定义 View 的流程了,快去看看吧。【带着问题学】关于自定义View你应该知道的知识点
这里面有个面试题值得一看。