目录介绍
- 1.View测量布局绘制整体流程
- 1.1 MeasureSpec是什么?
- 1.2 LayoutParams是什么?
- 1.3 View的测量流程(Measure)
- 1.4 在getChildMeasureSpec方法中都做了什么?
- 1.5 Layout布局过程
- 1.6 Draw过程
- 2.getWidth,getMeasureWidth的区别
- 3.requestLayout()、invalidate()与postInvalidate()有什么区别?
- 4.自定义View整体思想和类型
- 5.什么时候可以获取到View的宽高,为什么?
- 6.获取控件宽高的几种方法
- 7.子线程中真的不能更新UI吗?
- 8.常用布局测量流程
- 8.1 LinearLayout
- 8.2 FrameLayout
- 8.3 RelativeLayout
此文为我个人总结学习的View相关知识点和常考知识点,文章中说的每一个点都需要你个人去理解而不是去背,如果这些你都搞懂那么恭喜你与View相关的知识点应该是难不住你了。
1. View测量布局绘制整体流程
首先明确两个概念:
1.1 MeasureSpec是什么?
MeasureSpec是一个大小跟模式的组合值,MeasureSpec中的值是一个整型(32位)将size和mode打包成一个Int型,其中高两位是mode,后面30位存的是size,为了减少对象的分配开支所以使用了int类型去进行存储。要注意的是一般的int值是十进制的数,而MeasureSpec 是二进制存储的。一定要注意的是MeasureSpec是父View对子View的期望宽高要求
,可以认为是父View传递给子View的。
SpecMode有三类,每一类都表示特殊的含义,如下所示:
UNSPECIFIED
: 父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。 (如ListView或ScrollView)EXACTLY
:一个明确的大小值,如多少多少dp或matchparentAT_MOST
:对应于LayoutParams中的wrap_content。
1.2 LayoutParams是什么?
其实其中保存的就是我们XML文件对View的赋值。
<View
android:layout_width="100dp"
android:layout_height="100dp" />
复制代码
比如上面这种情况layoutParams.width和layoutParams.height就是100dp
具体分为三种:
LayoutParams.MATCH_PARENT
:精确模式,大小就是窗口的大小;LayoutParams.WRAP_CONTENT
:最大模式,大小不定,但是不能超过窗口的大小;具体的大小值(比如100dp)
:精确模式,大小为LayoutParams中指定的大小。
1.3 View的测量流程(Measure):
首先由一段代码来说明 代码所示:
protected void onMeasure(int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
//获取子View的LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//根据子View自身的LayoutParams和父View的MeasureSpec和可用空间获取子View自身的MeasureSpec
//获取宽度MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
//获取高度MeasureSpec
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//根据父View对子View的期望MeasureSpec结合自身的规则进行最终的测量得出自身的期望宽高
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
widthUsed+=child.getMeasuredWidth();
heightUsed+=child.getMeasuredHeight();
}
//给父View设置上最终的期望宽高
setMeasuredDimension(widthUsed, heightUsed);
}
复制代码
以ViewGroup为例
1.首先会遍历所有子View。(for循环)
2.根据子View自身的LayoutParams和父View自身的MeasureSpec以及父View的可用空间获取子View自身的MeasureSpec,这个MeasureSpec是父View对子View的期望宽高。(对应getChildMeasureSpec方法,最终在getChildMeasureSpec方法中使用MeasureSpec.makeMeasureSpec(size, mode) 来求得结果)
(有这一步的原因是因为我们在XML中定义的View宽高比如说是match_parent或wrap_content这种格式,那么我们其实并不知道他具体应该被赋值多大,google就要帮我们计算你match_parent的时候是多大,wrap_content的是多大,这个计算过程,就是计算出来的父View的MeasureSpec不断往子View传递,结合子View的LayoutParams 一起再算出子View的MeasureSpec,然后继续传给子View,不断计算每个View的MeasureSpec,子View有了MeasureSpec才能测量自己和自己的子View。)
3.子View根据父View对其的期望宽高和自身的规则算出其最终的期望宽高。(child.measure(childWidthMeasureSpec, childHeightMeasureSpec)) (这里的自身规则指的是其在OnMeasure中的逻辑,比如TextView会根据其中字符串的长度高度确定最终的大小值)。
MeasureSpec中的值既然是父View对子View的期望值,那么最外层的View是如何设置的?
在最外层的DecorView中,有这样一段代码:
private void performTraversals() {
......
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.EXACTLY);
break;
......
}
return measureSpec;
}
复制代码
可以看到我们最外层的View也就是DecorView中根据getRootMeasureSpec这个方法获取的MeasureSpec的Mode是EXACTLY,size是屏幕的宽高。 也就是说我们最外层的DecorView中默认的宽高就是屏幕的宽高,EXACTLY代表固定大小。
1.4 在getChildMeasureSpec方法中都做了什么?
在这个方法中子View根据自身的LayoutParams和父View自身的MeasureSpec及可用空间获取子View自身的MeasureSpec。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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;
}
复制代码
也就是说当我们自定义View的时候如果我们需要使自己的View支持wrap_content,那么就必须重写OnMeasure方法并对wrap_content做一个特殊的测量,否则在wrap_content的情况下我们自定义View的大小就会和父View的大小相同。
1.5 Layout布局过程
Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中的onLayout方法又会被调用。 layout方法中会调用setFrame方法保存其在ViewGroup中的位置,自定义ViewGroup的时候必须重写OnLayout方法,在其中进行子View位置的设置。
- 在View中onLayout默认是一个空实现
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
复制代码
- 在ViewGroup中是抽象方法,所以重写ViewGroup的时候必须去实现OnLayout方法。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.layout(l, t, r,b);
}
复制代码
具体的计算过程可以看下最简单FrameLayout 的onLayout 函数的源码,每个不同的ViewGroup 的实现都不一样。 MeasuredWidth和MeasuredHeight这两个参数为layout过程提供了一个很重要的依据(如果不知道View的大小,你怎么固定四个点的位置呢),但是这两个参数也不是必须的,layout过程中的4个参数l, t, r, b完全可以由我们任意指定,而View的最终的布局位置和大小(mRight - mLeft=实际宽或者mBottom-mTop=实际高)完全由这4个参数决定,但通常情况下用的就是第一步在measure过程中计算出来的期望宽高。
从measure和layout方法中可以看出的另一点是measure只是进行一些初始化参数的工作,真正的测量逻辑是在OnMeasure中进行的。而layout方法直接对你的View进行了位置和大小的确定,真正的逻辑不是在OnLayout中进行的。
1.6 Draw过程
View的绘制主要分为四部分:
- 绘制背景background.draw(canvas)。
- 绘制自己(onDraw)。
- 绘制children(dispatchDraw)。
- 绘制装饰(onDrawScrollBars)。
OnDraw
onDraw(canvas) 方法是view用来draw 自己的,具体如何绘制,颜色线条什么样式就需要子View自己去实现,View.java 的onDraw(canvas) 是空实现,ViewGroup 也没有实现,每个View的内容是各不相同的,所以需要由子类去实现具体逻辑。
dispatchDraw
dispatchDraw(canvas) 方法是用来绘制子View的,View.java 的dispatchDraw()方法是一个空方法,因为View没有子View,不需要实现dispatchDraw ()方法,ViewGroup就不一样了,它实现了dispatchDraw ()方法并在其中遍历子View然后调用子View的draw()方法。
当我们自定义ViewGroup的时候默认是不会执行OnDraw方法的(ViewGroup默认调用了setWillNotDraw(true),因为系统默认认为我们不会在ViewGroup中绘制内容),我们如果需要进行绘制可以在dispatchDraw中去进行或者调用setWillNotDraw(false)方法。
从setWillNotDraw这个方法的注释中可以看出,如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化。默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个优化标记位。这个标记位对实际开发的意义是:当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显式地关闭WILL_NOT_DRAW这个标记位。
/**
* If this view doesn't do any drawing on its own,set this flag to
* allow further optimizations. By default,this flag is not set on
* View,but could be set on some View subclasses such as ViewGroup.
*
* Typically,if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0,DRAW_MASK);
}
复制代码
2.getWidth,getMeasureWidth的区别
首先要明确一点,测量得到的宽高并不一定是View的最终宽高,当measure执行完毕后(准确的是我们在onMeasure中调用setMeasuredDimension(width,height)方法后)我们就可以得到View的一个期望宽高,通常情况下期望宽高是和最终的宽高相同的,但是也有特殊情况(比如在layout方法最终赋值View宽高的时候手动的修改值而不用测量得到的值)。
- getMeasureWidth()方法在measure()过程结束后就可以获取到了,另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的。
- getWidth()方法要在layout()过程结束后才能获取到,当在layout方法中调用setFrame()后就可以获取此值了,这个值是View的真实宽高。
- getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。
public final int getWidth() {
return mRight - mLeft;
}
复制代码
3.requestLayout()、invalidate()与postInvalidate()有什么区别?
invalidate和postInvalidate都是调用onDraw()方法,然后去达到重绘view的目的。 invalidate()用于主线程,postInvalidate()用于子线程, postInvalidate的原理其实就是通过主线程的handler完成线程的调度最终在主线程中调用invalidate方法。 requestLayout()会调用measure和layout方法,当View的大小位置需要改变的时候调用。如果view的大小发生了变化那么requestlayout也会调用draw()方法。
4.自定义View整体思想和类型
自定义View
1.继承自系统View(ImageView,TextView等)
一般重写OnMearsure方法,因为系统View再其自身的OnMearsure,OnDraw中都处理好了内容,我们一般不需要进行修改,复写的时候通常直接super父类方法然后实现自己的逻辑即可。 比如实现一个正方形的ImageView
2.继承View
如果你的View是定义了明确宽高的话,那么通常不需要我们重写OnMeasure的,如果宽高定义为了wrap_content的话我们需要早OnMeasure中针对wrap_content这种模式进行一个修改并设置最终宽高,因为默认情况下View的wrap_content和match_parent大小是相同的(在getChildMeasureSpec方法计算得出)。 如果我们的一些用到的属性是跟View的大小变化相关的话,那么我们可以通过OnSizeChanged去进行监听(OnSizeChanged在layout方法中的setFrame执行时会被调用,也就是说当我们调用requestLayout时可以通过OnSizeChanged去获取新的控件宽高等值)。 我们可以在OnDraw中进行内容的绘制,onDraw不要进行过多的耗时操作,如频繁的创建对象。
3.继承自ViewGroup
需要重写OnMeasure并且对子View进行遍历测量,然后自身去调用setMeasureDimens设置自身宽高。 onLayout必须重写并遍历子View调用其layout方法进行布局和大小的确定。(如果不调用会没有子View显示) onDraw默认不执行,如果需要进行绘制可以调用setWillNotDraw(false)取消onDraw的禁用或者在dispatchDraw中进行绘制。 TagLayout(流式布局)布局思路: 需要定义一个已使用宽度(widthUsed)和高度(heightUsed),在OnMeasure执行完对所有子View测量后,OnLayout方法中根据自身定义的规则如果widthUsed+view.getMeasureWidth>viewGroup.getMeasureWidth的话需要进行换行,widthUsed清零且heightUsed+=view.getMeasureHeight,子View调用layout时传入的四个点坐标就是(widthUsed,heightUsed,widthUsed+view.getMeasureWidth,heightUsed+view.getMeasureHeight),以此类推完成所有子View的布局;
4.继承自系统ViewGroup
这种情况不需要我们重写OnMearsure和OnLayout,因为系统已经帮我们写好了,通常这种情况下是我们将自己定义的布局添加到ViewGroup中,对整个的View进行一个封装复用。
5.什么时候可以获取到View的宽高,为什么?
在OnResume执行完后可以获取宽高,因为View的测绘流程是由ViewRootImpl的performTraversals开始的。当Activity创建时执行到handleResumeActivity方法中先会执行OnResume方法然后WindowManager会调用addView将DecorView添加进去,之后ViewRootImpl才会被创建出来从而调用performTraversals开始View的测绘流程。
final void handleResumeActivity( ... ... ) {
// 最终会执行到 onResume(),不是重点
r = performResumeActivity(token, clearHide, reason);
if (r != null) {
final Activity a = r.activity;
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
ViewManager wm = a.getWindowManager();
// 5. 执行到 WindowManagerImpl 的 addView()
// 然后会跳转到 WindowManagerGlobal 的 addView()
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
}
}
}
}
}
public void addView( ... ... ) {
ViewRootImpl root;
synchronized (mLock) {
// 初始化一个 ViewRootImpl 的实例
root = new ViewRootImpl(view.getContext(), display);
try {
// 调用 setView,为 root 布局 setView
// 其中 view 为传下来的 DecorView 对象
// 也就是说,实际上根布局并不是我们认为的 DecorView,而是 ViewRootImpl
root.setView(view, wparams, panelParentView);
}
}
}
// 6. 将 DecorView 加载到 WindowManager, View 的绘制流程从此刻才开始public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
// 请求对 View 进行测量和绘制
// 与 setContentView() 不同,此处的方法是 ViewRootImpl 的方法
requestLayout();
}
@Overridepublic void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
// 7. 此方法内部有一个 post 了一个 Runnable 对象
// 在其中又调用一个 doTraversal() 方法;
// 再之后又会调用到 performTraversals() 方法,然后 View 的测绘流程就从此处开始了
scheduleTraversals();
}
}
private void performTraversals() {
... ...
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
... ...
performLayout(lp, mWidth, mHeight);
... ...
performDraw();
... ...
}
复制代码
6.获取控件宽高的几种方法
1.onWindowFocusChanged 这个方法会被调用多次,在View初始化完毕后会调用,当Activity的窗口得到焦点和失去焦点都会被调用一次(Activity继续执行和暂停执行时)。
2.ViewTreeObserver 当View树的状态发生改变或者View树内部的View可见性发现改变时,onGlobalLayout方法将被回调。
3.View.post(new Runnble) 内部分两种情况:
第一种View已经完成测绘(这种直接调用主线程handler.post(new Runnable)发送一个Message并回调给Runnble处理) 第二种View没有完成测绘,这种会先将Runnble任务通过数组保存下来,当View开始测绘时(ViewRootImpl.performTraversals())会将包存下来的Runnble任务通过主线程handler进行发送消息,由于消息在messagequeue中是串行处理的,所以view.post的Runnble任务会在view的测绘完成后在开始执行其自身的消息,这时View已经完成测绘,自然就可以获取到宽高了。 更详细的可参考: www.cnblogs.com/dasusu/p/80…
7.子线程中真的不能更新UI吗?
众所周知安卓不允许在非UI线程中去更新UI,每当我们对View状态做出改变的时候(如调用requestLayout()或invalidate()等方式时)都会去检查当前线程是否是主线程,而**检查线程的判断是在ViewRootImpl的checkThread()方法中去执行的。
**也就是说在ViewRootImpl没有创建出来的时候(OnResume执行完后ViewRootImpl才创建出来的)checkThread()这一步检测是不会执行的,在这种情况下我们在子线程中是可以更新UI的。
ViewRootImpl.java
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
复制代码
8.常用布局测量流程
8.1 LinearLayout设置权重测量流程
详细分析可参考https://toutiao.io/posts/08f9tz/preview
垂直布局分析
设置了权重的View会被测量两次,没有只会测量一次。(特殊情况:如果子View的lp.weight>0且lp.height==0且LinearLayout设置了明确宽高的(mode==MeasureSpec.EXACTLY)情况下子View也只会测量一次。)
1.LinearLayout中的第一个循环会遍历所有的子View计算其高度并将高度进行累加。
- 如果子View的lp.weight>0且lp.height==0且LinearLayout设置了明确宽高的(mode==MeasureSpec.EXACTLY)情况下子View只会测量一次。
第一次测量完成后会根据LinearLayout总高度-累加高度算出剩余高度,剩余高度有可能是负值,最后根据剩余高度和总权重算出每一份权重的占比。 2.第二个循环会对所有设置了权重weight的子View进行测量,并根据子View设置的权重值分配子View最终的高度。
结论:简而言之就是第一次循环算出所有子View的高度和,然后用Linearlayout自身高度-已用高度算出剩余高度并根据剩余高度/总权重算出每一份权重的大小,第二次循环给设置了权重的View根据权重设置的值分配大小。
8.2 FrameLayout测量过程
FrameLayout只会测量一次,计算出所有子View的宽高之后,如果FrameLayout自身MeasureSpec.MODE=EXACTLY,那么它最终宽高就是设置的值,如果是MeasureSpec.MODE=AT_MOST(wrap_content)的话那么最终宽高会选取所有子View中的最大宽和最大高作为最终宽高。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//子View测量自身宽高,因为Framelayout内部View可重叠放置所以当前可用宽高都传的0
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//记录最大宽高
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
//修正最大宽高
// Account for padding too
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
//设置最终FrameLayou宽高
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}
复制代码
8.3 RelativeLayout测量过程
在OnMeasure中会测量两次子View,第一次水平方向根据水平方向规则(toLeft,toBottom等)测量获取子View左右值(mLeft,mRight),高度可认为设置为最大值。第二次测量根据竖直方向的规则(Above,Bottom等)测量获取子View上下值(mTop,mBottom)。
为什么需要测量两次?
因为RelativeLayout子View之前既可以是水平依赖也可以是竖直依赖,所以水平竖直方向都需要去进行一次测量。 这里需要注意的一点是在规则的处理上alignParentLeft的优先级是高于toLeft的。 详情可见:www.jianshu.com/p/87bc61b8a…