内容简介:MeasureSpec是一个大小跟模式的组合值,MeasureSpec中的值是一个整型(32位)将size和mode打包成一个Int型,其中高两位是mode,后面30位存的是size,为了减少对象的分配开支所以使用了int类型去进行存储。要注意的是一般的int值是十进制的数,而MeasureSpec 是二进制存储的。一定要注意的是SpecMode有三类,每一类都表示特殊的含义,如下所示:其实其中保存的就是我们XML文件对View的赋值。
MeasureSpec是一个大小跟模式的组合值,MeasureSpec中的值是一个整型(32位)将size和mode打包成一个Int型,其中高两位是mode,后面30位存的是size,为了减少对象的分配开支所以使用了int类型去进行存储。要注意的是一般的int值是十进制的数,而MeasureSpec 是二进制存储的。一定要注意的是 MeasureSpec是父View对子View的期望宽高要求
,可以认为是父View传递给子View的。
SpecMode有三类,每一类都表示特殊的含义,如下所示:
-
UNSPECIFIED
: 父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。 (如ListView或ScrollView) -
EXACTLY
:一个明确的大小值,如多少多少dp或matchparent -
AT_MOST
:对应于LayoutParams中的wrap_content。
1.2 LayoutParams是什么?
其实其中保存的就是我们XML文件对View的赋值。
<View android:layout_width="100dp" android:layout_height="100dp" /> 复制代码
比如上面这种情况layoutParams.width和layoutParams.height就是100dp
具体分为三种: 1. LayoutParams.MATCH_PARENT
:精确模式,大小就是窗口的大小; 2. LayoutParams.WRAP_CONTENT
:最大模式,大小不定,但是不能超过窗口的大小; 3. 具体的大小值(比如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。
可以看到当我们定义子View为match_parent或wrap_content的时候,最终生成的MeasureSpec的Size为父View的大小,而在View的默认实现中当调用measure开始测量后走到onMearsure设置最终期望宽高的时候默认实现为直接使用MeasureSpec中的Size值。
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)。
以上所述就是小编给大家介绍的《面试系列之View相关知识点》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。