UI的测量、布局和绘制源码详解
因为之前撸了AMS、PMS。前几天也跟进WMS看了WMS-01和WMS-02,知道了ViewRootImpl会管理所有View的绘制策略,都是由他控制。还有Choreographer编舞者类会去底层申请Vsync垂直同步信号,获取回来后会去调用ViewRootImpl的performTraversals方法,这个方法中会有performMeasure测量方法、performLayout布局方法和performDraw绘制方法。
一、确定下我们要搞清的东西
- 1、View的MeasureSpec创建规则?
- 2、MeasureSpec的含义以及它的结构是怎样的?
- 3、onMeasure测量方法相关知识?
- 4、onLayout布局方法的操作流程和应该注意的事项?
二、ViewRootImpl为啥管理所有View的绘制流程。
2.1 首先ViewRootImpl是由WindowManagerGlobal实例化的,而WindowManagerGlobal是单例,
2.2 ViewRootImpl的performMeasure、performLayout和performDraw最终都是调用View的measure、layout和draw方法。
2.3 我们再看看View中的requestLayout、invalidate和postInvalidate最终都是调用了谁?
- View.requestLayout ---> mParent.requestLayout(mParent是ViewParent,它是一个接口而ViewRootImpl实现了它。并在ViewRootImpl.setView中通过view.assignParent(this)给其赋值) ---> ViewRootImpl.requestLayout ---> ViewRootImpl.scheduleTraversals(最终回到ViewRootImpl通过Choreographer编舞者类去底层获取Vsync垂直同步信号的地方了。)
- View.invalidate ---> View.invalidateInternal ---> p.invalidateChild(p是ViewParent,它是一个接口而ViewRootImpl实现了它。并在ViewRootImpl.setView中通过view.assignParent(this)给其赋值) ---> ViewRootImpl.invalidateChild ---> ViewRootImpl.invalidateChildInParent ---> ViewRootImpl.invalidateRectOnScreen ---> ViewRootImpl.scheduleTraversals(最终回到ViewRootImpl通过Choreographer编舞者类去底层获取Vsync垂直同步信号的地方了。)
- View.postInvalidate ---> View.postInvalidateDelayed ---> ViewRootImpl.dispatchInvalidateDelayed ---> ViewRootImpl.mHandler(发送的Handler消息为MSG_INVALIDATE) ---> ViewRootImpl.handleMessage(((View) msg.obj).invalidate();) ---> 其实又回到了第2点从View.invalidate, postInvalidate只是通过Handler机制加了一个延时消息并切换为主线程
三、View的MeasureSpec创建规则?
看图说话3.1 当父容器为EXACTLY模式时:
- 如果子View指定了具体宽高值时,那么这个子View的resultSize(参考值)就是你给予的具体值,模式为EXACTLY。
- 如果子View指定是match_parent时,那么子View的resultSize(参考值)为父容器给予的最大值,模式为EXACTLY。
- 如果子View指定的是wrap_content时,那么子View的resultSize(参考值)还是为父容器给予的最大自,模式变为AT_MOST。
3.2 当父容器为AT_MOST模式时:
- 如果子View指定了具体宽高值时,那么这个子View的resultSize(参考值)就是你给予的具体值,模式为EXACTLY。
- 如果子View指定的是match_parent时,那么子View的resultSize(参考值)为父容器给予的最大值,模式为AT_MOST。
- 如果子View指定的是wrap_content时,那么子View的resultSize(参考值)也为父容器给予的最大值,模式也为AT_MOST。
3.3 当父容器为UNSPECIFIED模式时:
- 如果子View制定了具体的宽高值时,那么这个子View的resultSize(参考值)就是你给予的具体值,模式为EXACTLY。
- 如果子View为match_parent或wrap_content时,那么resultSize(参考值)都为0, 模式都为UNSPECIFIED。
四、MeasureSpec的含义以及它的结构是怎样的?
4.1 为什么MeasureSpec的mode和size要合到一起?
我们知道MeasureSpec有三种模式:EXACTLY、AT_MOST和UNSPECIFIED。那么他们其实用两个二进制位就可以表示了:如EXACTLY用01、AT_MOST用10、UNSPECIFIED用00表示。
我们知道一个int有4个字节,32比特位。Google工程师为了节省内存,将前两位用于Mode的表示、后30位用于Size表示。因为Mode总共就三种模式,所以两位足以。而size范围为0~1073741823(1 << MeasureSpec.MODE_SHIFT) - 1)也足够了。
将两者合二为一就可以用一个int值表示两种数据了,不然的话还得用两个int值表示,一个表示mode,一个表示Size。这样的设计不可谓不妙!值得学习~
4.2 接下来看看MeasureSpec是怎么合成和拆解的。
- 首先,MeasureSpec是个内部类,其实很少代码,主要是用位运算来处理。因为位运算的效率是很高的。(为什么会很高,这就和JVM指令集相关了,直接有位运算的指令,你说快不快)。直接看代码,UNSPECIFIED就是00左移30位,EXACTLY就是01左移30位,而AT_MOST就是10左移30位。
private static final int MODE_SHIFT = 30;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
这里的图就用10位意思意思了,哈哈
- 而mode和size的合成其实也很简单,也是位运算、与运算和或运算。size先与非MODE_MASK与运算,再与mode和MODE_MASK与运算的结果进行或运算。
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << View.MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
画了一副运算的示意图,假用10位代替32位表示
- 接下来通过MeasureSpec来分别获得mode和size值就更简单了。
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
计算获取Size值
计算获取Mode值
五、onMeasure测量方法相关知识?和onMeasure方法中如何进行测量?
5.1 onMeasure传递的参数是自身view的模式和父控件给他的参考高宽值,那么是在哪里修改为子View自身的?
实际上在ViewGroup中有一个方法measureChildWithMargins,会通过父容器传过来的parentMeasureSpec还有Padding、Margin值来计算子控件的childMeasureSpec。
measureChildWithMargins则是由继承至ViewGroup的容器像FrameLayou、LinearLayout等容器在onMeasure方法中调用。(这里修类举FrameLayout)。
还是需要回过头看下《三、View的MeasureSpec创建规则?》,根据父容器的mode模式,在根据子控件设置的具体值、match_parent或者wrap_content来决定子控件的MeasureSpec。
FrameLayout:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
}
ViewGroup:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final ViewGroup.MarginLayoutParams lp = (ViewGroup.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);
}
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
...
//根据父容器的模式来计算子View的MeasureSpec
switch (specMode) {
// EXACTLY模式
case View.MeasureSpec.EXACTLY:
if (childDimension >= 0) {
// 子控件设置具体宽高值,那么参考值为自己设置的具体值,EXACTLY
resultSize = childDimension;
resultMode = View.MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
// 子控件设置match_parent,那么参考值为父容器最大值,EXACTLY
resultSize = size;
resultMode = View.MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
// 子控件设置wrap_content,那么参考值为父容器最大值,模式为AT_MOST
resultSize = size;
resultMode = View.MeasureSpec.AT_MOST;
}
break;
case View.MeasureSpec.AT_MOST:
...
break;
}
//noinspection ResourceType
return View.MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
5.2 onMeasure方法中如何进行测量?
这里实际上是要分为两种,一种是View;一种是ViewGroup。
如果是继承至View的话,那么onMeasure的目的就是测量它自身的宽高;而如果是继承至ViewGroup的话,那么它就是一个自定义容器,它的onMeasure的目的就是通过测量它子View的宽高进而测量自身容器所需的宽高,当然这里还得结合MeasureSpec的模式来区分对待。
5.2.1 自定义View测量步骤:
- 在onMeasure方法中通过传入的参数来分别获取mode和size。
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
- 然后通过自身mode模式不同来设置宽高值
switch (specMode) {
case View.MeasureSpec.EXACTLY:
//模式为EXACTLY的时,宽高为父容器给定的参考值(具体值或者父容器最大值)
resultWidth = specWidthSize;
resultHeight = specHeightSize;
break;
case View.MeasureSpec.AT_MOST:
//模式为AT_MOST的时,宽高为子View根据自身实际大小计算而来的值
resultWidth = 为子View根据自身实际大小计算而来的值;
resultHeight = 为子View根据自身实际大小计算而来的值;
break;
}
5.2.2 自定义ViewGroup测量步骤:
- 在onMeasure方法中通过传入的参数来分别获取mode和size。
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
- 测量子View的宽高
for (int i = 0; i < getChildCount; i++) {
View child = getChildAt(i);
measureChild(child, parentWidthMeasureSpec, parentHeightMeasureSpec);
}
//它也会去根据父容器的MeasureSpec来确定子View的MeasureSpec
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final ViewGroup.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);
}
- 在自定义ViewGroup时,可能要获取Margin值之类,这时候需要重写generateLayoutParams方法
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
- 然后通过自身mode模式不同来设置宽高值
switch (specMode) {
case View.MeasureSpec.EXACTLY:
//模式为EXACTLY的时,宽高为父容器给定的参考值(具体值或者父容器最大值)
resultWidth = specWidthSize;
resultHeight = specHeightSize;
break;
case View.MeasureSpec.AT_MOST:
//模式为AT_MOST的时,容器的宽高需要根据子View宽高计算
resultWidth = 容器的宽高需要根据子View宽高计算;
resultHeight = 容器的宽高需要根据子View宽高计算;
break;
}
- 最后通过setMeasuredDimension将宽高设置进去
setMeasuredDimension(resultWidth, resultHeight);
5.3 getMeasureWidth和getWidth有什么区别?
getMeasureWidth和getWidth最终的到的值一样的,它们只是时机不同而已,getMeasureWidth是在onMeasure方法调用测量完毕后取到值,而getWidth则是在onLayout布局完后取到值。
六、onLayout布局方法的操作流程和应该注意的事项?
布局的话在自定义View中实际上是不需要处理的,只有在自定义ViewGroup的时候才会用到。
其实布局也是很简单的,在上一步onMeasure测量完毕之后,你只需要根据你的设计稿或者你想要的布局方式,给子View设置left、top、right和bottom值就可以了。当然在计算这些值得时候,需要考虑到Margin、Padding值。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
...
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
七、总结
UI的测量、布局和绘制流程就差不多到这了,其实没有具体细致讲到如何真实测量、布局。只是把View的MeasureSpec创建规则以及MeasureSpec的结构等深入源码看了下,我们只需要把原理搞懂了。其实真实用到还是不难的。
自定义View主要是用到onMeasure和onDraw方法。
自定义viewGroup主要是用到了onMeasure和onLayout方法。