首先明确两个概念:html
MeasureSpec是一个大小跟模式的组合值,MeasureSpec中的值是一个整型(32位)将size和mode打包成一个Int型,其中高两位是mode,后面30位存的是size,为了减小对象的分配开支因此使用了int类型去进行存储。要注意的是通常的int值是十进制的数,而MeasureSpec 是二进制存储的。必定要注意的是MeasureSpec是父View对子View的指望宽高要求
,能够认为是父View传递给子View的。java
SpecMode有三类,每一类都表示特殊的含义,以下所示:android
UNSPECIFIED
: 父容器不对View有任何限制,要多大给多大,这种状况通常用于系统内部,表示一种测量的状态。 (如ListView或ScrollView)EXACTLY
:一个明确的大小值,如多少多少dp或matchparentAT_MOST
:对应于LayoutParams中的wrap_content。其实其中保存的就是咱们XML文件对View的赋值。canvas
<View
android:layout_width="100dp"
android:layout_height="100dp" />
复制代码
好比上面这种状况layoutParams.width和layoutParams.height就是100dp数组
具体分为三种:bash
LayoutParams.MATCH_PARENT
:精确模式,大小就是窗口的大小;LayoutParams.WRAP_CONTENT
:最大模式,大小不定,可是不能超过窗口的大小;具体的大小值(好比100dp)
:精确模式,大小为LayoutParams中指定的大小。首先由一段代码来讲明 代码所示:ide
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);
}
复制代码
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。)post
3.子View根据父View对其的指望宽高和自身的规则算出其最终的指望宽高。(child.measure(childWidthMeasureSpec, childHeightMeasureSpec)) (这里的自身规则指的是其在OnMeasure中的逻辑,好比TextView会根据其中字符串的长度高度肯定最终的大小值)。
在最外层的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表明固定大小。
在这个方法中子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的大小相同。
Layout的做用是ViewGroup用来肯定子元素的位置,当ViewGroup的位置被肯定后,它在onLayout中会遍历全部的子元素并调用其layout方法,在layout方法中的onLayout方法又会被调用。 layout方法中会调用setFrame方法保存其在ViewGroup中的位置,自定义ViewGroup的时候必须重写OnLayout方法,在其中进行子View位置的设置。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
复制代码
@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中进行的。
View的绘制主要分为四部分:
onDraw(canvas) 方法是view用来draw 本身的,具体如何绘制,颜色线条什么样式就须要子View本身去实现,View.java 的onDraw(canvas) 是空实现,ViewGroup 也没有实现,每一个View的内容是各不相同的,因此须要由子类去实现具体逻辑。
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); } 复制代码
首先要明确一点,测量获得的宽高并不必定是View的最终宽高,当measure执行完毕后(准确的是咱们在onMeasure中调用setMeasuredDimension(width,height)方法后)咱们就能够获得View的一个指望宽高,一般状况下指望宽高是和最终的宽高相同的,可是也有特殊状况(好比在layout方法最终赋值View宽高的时候手动的修改值而不用测量获得的值)。
public final int getWidth() {
return mRight - mLeft;
}
复制代码
invalidate和postInvalidate都是调用onDraw()方法,而后去达到重绘view的目的。 invalidate()用于主线程,postInvalidate()用于子线程, postInvalidate的原理其实就是经过主线程的handler完成线程的调度最终在主线程中调用invalidate方法。 requestLayout()会调用measure和layout方法,当View的大小位置须要改变的时候调用。若是view的大小发生了变化那么requestlayout也会调用draw()方法。
自定义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进行一个封装复用。
在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();
... ...
}
复制代码
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…
众所周知安卓不容许在非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.");
}
}
复制代码
详细分析可参考https://toutiao.io/posts/08f9tz/preview
垂直布局分析
设置了权重的View会被测量两次,没有只会测量一次。(特殊状况:若是子View的lp.weight>0且lp.height==0且LinearLayout设置了明确宽高的(mode==MeasureSpec.EXACTLY)状况下子View也只会测量一次。)
1.LinearLayout中的第一个循环会遍历全部的子View计算其高度并将高度进行累加。
第一次测量完成后会根据LinearLayout总高度-累加高度算出剩余高度,剩余高度有多是负值,最后根据剩余高度和总权重算出每一份权重的占比。 2.第二个循环会对全部设置了权重weight的子View进行测量,并根据子View设置的权重值分配子View最终的高度。
结论:简而言之就是第一次循环算出全部子View的高度和,而后用Linearlayout自身高度-已用高度算出剩余高度并根据剩余高度/总权重算出每一份权重的大小,第二次循环给设置了权重的View根据权重设置的值分配大小。
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)); } 复制代码
在OnMeasure中会测量两次子View,第一次水平方向根据水平方向规则(toLeft,toBottom等)测量获取子View左右值(mLeft,mRight),高度可认为设置为最大值。第二次测量根据竖直方向的规则(Above,Bottom等)测量获取子View上下值(mTop,mBottom)。
由于RelativeLayout子View以前既能够是水平依赖也能够是竖直依赖,因此水平竖直方向都须要去进行一次测量。 这里须要注意的一点是在规则的处理上alignParentLeft的优先级是高于toLeft的。 详情可见:www.jianshu.com/p/87bc61b8a…