内容简介:众所周知,android为我们提供大量的基础控件,这些控件完成基本功能是没有问题的,也比较全面,但是对于一些比较精致的产品,不仅仅是基础功能实现就OK,它们往往要很炫的效果,这就需要自定义view了,好了不多说了,直接开始主题,View的绘制分为measure、layout、draw,其中测量是最复杂的,我们单独来讲,布局和绘制将在下一篇文章去讲解。在正式讲解View的工作原理之前,我们先了解一下ViewRoot,ViewRoot的实现类是ViewRootImpl,它是连接WindowManager和Dec
众所周知,android为我们提供大量的基础控件,这些控件完成基本功能是没有问题的,也比较全面,但是对于一些比较精致的产品,不仅仅是基础功能实现就OK,它们往往要很炫的效果,这就需要自定义view了,好了不多说了,直接开始主题,View的绘制分为measure、layout、draw,其中测量是最复杂的,我们单独来讲,布局和绘制将在下一篇文章去讲解。
二、理解ViewRoot和DecorView
在正式讲解View的工作原理之前,我们先了解一下ViewRoot,ViewRoot的实现类是ViewRootImpl,它是连接WindowManager和DecorView的纽带,View的三大流程都是通过ViewRoot来完成的,它是在ActivityThread中被初始化的。View的绘制流程是从ViewRoot的performTraversals开始的,经历三个步骤后最终呈现在界面的view,大致如下:
performTraversals会依次调用perfornMeasure、performLayout、performDraw,这三个分别完成顶级View的measure、layout、draw,performMeasure再去调用measure,最后去调用onMeasure完成子view的测量,子view会再去调用measure,依次递归下去,直到所以的子view measure完毕。
下面来简单讲一下DecorView,如下图:
DecorView是所有View的顶级View,它里面有个LinearLayout,分为title bar和content,我们经常在onCreat里面用到的setContentView方法就是为这个content设置布局的,也就是说,我们写的布局都塞进了这个content,哦。。。。,明白了,这就是为啥要叫setContentView而不叫setView了吧。
三、理解一下MeasureSpec
3.1 MeasureSpec的概念
可以说,这个概念是贯穿了整个View绘制的所有流程,是的,表面上看它就是一个尺寸规格,也就是决定View的大小,它绝大部分都可以决定View的大小,当然也不是它一个人说了算,毕竟有些ViewGroup的LayoutParams也对子view的大小有影响。
MeasureSpec代表一个32位的int值,其中高两位是mode,低30位是size,其中mode的三种值,分别是:
- UNSPECIFIED:父容器不对View做任何限制,要多大就给多大,这种主要用于系统内部,应用层开发一般用不到。
- AT_MOST:就是子View的值根据自己定义的大小来给定,但是不可以超过父类的大小,相当于LayoutParams的wrap_content。
- EXACTLY:父类已经检测到了子View的精确大小了,这时候View的大小就是SpecSize,它对应LayoutParams的match_parent和具体值这两种情况。
3.2 MeasureSpec和LayoutParams的对应关系
上面提到过了,View的大小是由MeasureSpec来决定的,我们一般会给view设置LayoutParams参数,这个params参数会在父容器的MeasureSpec约束的情况下转换为对应的MeasureSpec,这个Spec会最终确定view的测量大小,也就是说view的大小是由父容器的MeasureSpec和view的LayoutParams共同决定的,MeasureSpec一旦确定后,onMeasure就可以确定view的测量宽高了。
View的measure过程是由ViewGroup的measure传递来的,这里看一下ViewGroup的measureChildWithMargins方法,
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); } 复制代码
可以看到,在测量子view的时候会去获取子view的MeasureSpec,这里详细看一下getChildMeasureSpec方法
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } 复制代码
这个方法有点长,但是很简单,它就是根据父容器的MeasureSpec和子view的LayoutParams来确定子view的MeasureSpec。我们把上述规则总结到一张表里面,方便记忆:
这里不是我自己创造的一张表,而仅仅是对上述过程的一种解释而已。但是有一种特殊的情况我们要注意一下, 就是当子view是warp_content的时候,不管父类是啥,结果都是一样的,这就会有问题,怎么办呢,这就交给子view的onMeasure去处理吧,所以在自定义view的时候如果view 的params设置为wrap_content的时候,我们就要去实现onMeasure方法。具体的后面会讲。
四、View的measure过程
view的三大过程中,measure是最复杂的,因为往往要确定一个view的大小,要经历好多次测量才能ok。measure过程要分情况来看,View和ViewGroup,因为ViewGroup不仅仅要测量自己还要测量子元素,一层一层传递下去。
4.1、单个View的measure
View里面的measure是final方法,这就意味着该类不允许被继承,measure里面调用了onMeasure方法,也就是说measure的工作就是在onMeasure里面完成的,看看o n M e asure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } 复制代码
代码很简单,setMeasuredDimension设置测量的值,主要的是getDefaultSize方法
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; } 复制代码
getDefaultsize就是返回测量后的大小,这里注意是测量后的大小,因为view的大小最终确定是在layout后,有时候layout也会对view的大小造成影响,不过绝大部分getDefaultsize就是最终view的大小。
注意的点:从getDefaultsize方法可以看到,view的大小由specSize来决定,所以,直接继承View的自定义控件需要重写onMeasure方法并且设置wrap_content时的自身大小,否则在布局中的wrap_content和match_parent就没有什么区别了,从上面的表格也可以清晰的看到,这种情况是我们不希望看法的,怎么解决呢?很简单,我们设置一个默认值就可以了
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //如果view在布局中使用wrap_content ,这时候就是AT_MOST,我们需要在onmeasure里面做特殊处理,否则和match_parent就没有区别了 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(500, 300); }else if (heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpecSize, 300); }else if (widthSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(500, heightSpecSize); } } 复制代码
具体的默认值是多少,我们根据自己的情况来定。
4.2 ViewGroup的measure过程
对于ViewGroup的measure过程,它会更加复杂一点,因为它不仅要measure自己,还要measure子view,ViewGroup没有重写onMeasure方法,它提供了另一种方法measureChildren
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]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } } 复制代码
上面的方法很明了,就是对每一个子元素进行measure,我们看看measureChild:
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); } 复制代码
这也太简单了,就是获取子元素的measure spec值,然后调用view的measure操作,这个和单独的view就没啥区别了,就这样一直迭代下去,直到单个的view测量结束。这是测量子元素的过程,那么ViewGroup怎么测量自己的呢。
其实ViewGroup并没有定义其测量的具体过程,因为它是一个抽象类,其测量过程onMeasure交给了其子类去实现了,比如LinearLayout类就有自己专门的onMeasure方法,这也是符合逻辑的,因为没个Layout都有自己的特性,我们不可能在ViewGroup统一去处理。 我们以LinearLayout为例,看如下代码:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } } 复制代码
看垂直方向的,水平方向的类似:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { mTotalLength = 0; ... final int count = getVirtualChildCount(); final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); boolean matchWidth = false; boolean skippedMeasure = false; final int baselineChildIndex = mBaselineAlignedChildIndex; final boolean useLargestChild = mUseLargestChild; int largestChildHeight = Integer.MIN_VALUE; int consumedExcessSpace = 0; int nonSkippedChildCount = 0; // See how tall everyone is. Also remember max width. for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); if (child == null) { mTotalLength += measureNullChild(i); continue; } if (child.getVisibility() == View.GONE) { i += getChildrenSkipCount(child, i); continue; } nonSkippedChildCount++; if (hasDividerBeforeChildAt(i)) { mTotalLength += mDividerHeight; } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); totalWeight += lp.weight; final boolean useExcessSpace = lp.height == 0 && lp.weight > 0; if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) { // Optimization: don't bother measuring children who are only // laid out using excess space. These views will get measured // later if we have space to distribute. final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); skippedMeasure = true; } else { if (useExcessSpace) { final int usedHeight = totalWeight == 0 ? mTotalLength : 0; measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight); final int childHeight = child.getMeasuredHeight(); if (useExcessSpace) { lp.height = 0; consumedExcessSpace += childHeight; } final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); if (useLargestChild) { largestChildHeight = Math.max(childHeight, largestChildHeight); } } final int margin = lp.leftMargin + lp.rightMargin; final int measuredWidth = child.getMeasuredWidth() + margin; maxWidth = Math.max(maxWidth, measuredWidth); childState = combineMeasuredStates(childState, child.getMeasuredState()); allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT; if (lp.weight > 0) { /* * Widths of weighted Views are bogus if we end up * remeasuring, so keep them separate. */ weightedMaxWidth = Math.max(weightedMaxWidth, matchWidthLocally ? margin : measuredWidth); } else { alternativeMaxWidth = Math.max(alternativeMaxWidth, matchWidthLocally ? margin : measuredWidth); } i += getChildrenSkipCount(child, i); } if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) { mTotalLength += mDividerHeight; } if (!allFillParent && widthMode != MeasureSpec.EXACTLY) { maxWidth = alternativeMaxWidth; } ... maxWidth += mPaddingLeft + mPaddingRight; // Check against our minimum width maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState); if (matchWidth) { forceUniformWidth(count, heightMeasureSpec); } } 复制代码
这个方法非常长,系统会遍历每个子元素,并且调用子元素的measureChildBeforeLayout方法:
void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight) { measureChildWithMargins(child, widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight); } 复制代码
这个方法内部又在执行measure子元素的操作,当子元素全部测量完毕后,Linearlayout才会去测量自己的大小。
注意的点: 测量的过程是从父类开始分发,递归的测量子元素,最后再测量父类。layout的过程是恰好相反的,我们后面再讲。
4.3、关于measure可能会遇到的坑
View的measure一般是很复杂的,某些情况下得多次测量,所以为了保险起见,我们应该在layout结束后再去获取View的宽和高。在实际需求中,比如,在activity中,你怎么获取某个view的width和height呢?有的人肯定会说,很简答啊,直接在oncreat中去调用getWidth和getHeight,这肯定是不行的,你们可以去试试,这里获取到的极有可能是空值,这是因为View的绘制和Activity生命周期不存在同步的关系,无法保证在哪一个周期View的测量工作已经完成了,所以不靠谱。这里简单提一下几种常见的解决方案,但是不展开讲解了:
- Activity的onWindowsFocusChanged,在这个里面去获取宽和高。
- view.post(runnable),等到消息队列开始执行的时候,view肯定是ready状态了。
- ViewTreeObserve 重写addOnGlobalLayoutListener方法。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。