学习内容

View的底层工作原理,比如View的测量流程、布局流程以及绘制流程;以及常见的View回调方法;熟悉掌握前面的知识后,自定义View的时候也会更加的得心应手。

4.1 初识ViewRoot和DecorView

  • ViewRoot对应于ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。
  • 在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。
  • View的绘制流程从ViewRoot的performTraversals开始,经过measure、layout和draw三个过程才可以把一个View绘制出来,其中measure用来测量View的宽高,layout用来确定View在父容器中的放置位置,而draw则负责将View绘制到屏幕上。
  • performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程。   其中performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,   在onMeasure方法中则会对所有子元素进行measure过程,这样就完成了一次measure过程;子元素会重复父容器的measure过程,如此反复完成了整个View数的遍历。另外两个过程类似,大致调用流程如下图:

  •  
    performTraversals工作流程图.png
  • measure过程决定了View的宽/高,完成后可通过getMeasuredWidth/getMeasureHeight方法来获取View测量后的宽/高。Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成后可通过getTop、getBotton、getLeft和getRight拿到View的四个定点坐标。Draw过程决定了View的显示,完成后View的内容才能呈现到屏幕上。
  • 如下图,DecorView作为顶级View,一般情况下它内部包含了一个竖直方向的LinearLayout,里面分为两个部分(具体情况和Android版本和主题有关),上面是标题栏,下面是内容栏。在Activity通过setContextView所设置的布局文件其实就是被加载到内容栏之中的。
//获取内容栏
ViewGroup content = findViewById(R.android.id.content);
//获取我们设置的Viewcontext.getChildAt(0);

DecorView其实是一个FrameLayout,View层的事件都先经过DecorView,然后才传给我们的View。

 
DecorView的结构.png

4.2 理解MeasureSpec

  • MeasureSpec很大程度上决定一个View的尺寸规格,测量过程中,系统会将View的layoutParams根据父容器所施加的规则转换成对应的MeasureSpec,再根据这个measureSpec来测量出View的宽/高。
  • MeasureSpec代表一个32位的int值,高2位为SpecMode,低30位为SpecSize,SpecMode是指测量模式,SpecSize是指在某种测量模式下的规格大小。
    MpecMode有三类;
    1.UNSPECIFIED 父容器不对View进行任何限制,要多大给多大,一般用于系统内部
    2.EXACTLY 父容器检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,对应LayoutParams中的match_parent和具体数值这两种模式。
    3.AT_MOST 父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,不同View实现不同,对应LayoutParams中的wrap_content。
  • 当View采用固定宽/高的时候,不管父容器的MeasureSpec的是什么,View的MeasureSpec都是精确模式兵其大小遵循Layoutparams的大小。 当View的宽/高是match_parent时,如果他的父容器的模式是精确模式,那View也是精确模式并且大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且起大小不会超过父容器的剩余空间。 当View的宽/高是wrap_content时,不管父容器的模式是精确还是最大化,View的模式总是最大化并且不能超过父容器的剩余空间。

4.3 View的工作流程

1. View的measure过程
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
 
View的measure过程.png
  • setMeasuredDimension方法会设置View的宽/高的测量值
  • getDefaultSize方法返回的大小就是measureSpec中的specSize,也就是View测量后的大小,绝大部分情况和View的最终大小(layout阶段确定)相同。
  • getSuggestedMinimumWidth方法,作为getDefaultSize的第一个参数(建议宽度)
  • 直接继承View的自定义控件,需要重写onMeasure方法并且设置
    wrap_content时的自身大小,否则在布局中使用了wrap_content相当于使用了match_parent。解决方法:在onMeasure时,给View指定一个内部宽/高,并在wrap_content时设置即可,其他情况沿用系统的测量值即可。
2. ViewGroup的measure过程
 
ViewGroup的measure过程.png
  • 对于ViewGroup来说,除了完成自己的measure过程之外,还会遍历去调用所有子元素的measure方法,个个子元素再递归去执行这个过程,和View不同的是,ViewGroup是一个抽象类,没有重写View的onMeasure方法,提供了measureChildren方法。
  • measureChildren方法,遍历获取子元素,子元素调用measureChild方法
  • measureChild方法,取出子元素的LayoutParams,再通过getChildMeasureSpec方法来创建子元素的MeasureSpec,接着将MeasureSpec传递给View的measure方法进行测量。
  • ViewGroup没有定义其测量的具体过程,因为不同的ViewGroup子类有不同的布局特征,所以其测量过程的onMeasure方法需要各个子类去具体实现。
  • measure完成之后,通过getMeasureWidth/Height方法就可以获取View的测量宽/高,需要注意的是,在某些极端情况下,系统可能要多次measure才能确定最终的测量宽/高,比较好的习惯是在onLayout方法中去获取测量宽/高或者最终宽/高。

如何在Activity中获取View的宽/高信息?
       因为View的measure过程和Activity的生命周期不是同步进行,如果View还没有测量完毕,那么获取到的宽/高就是0;

所以在Activity的onCreate、onStart、onResume中均无法正确的获取到View的宽/高信息。下面给出4种解决方法。

  1. Activity/View#onWindowFocusChanged。
    onWindowFocusChanged这个方法的含义是:VieW已经初始化完毕了,宽高已经准备好了,需要注意:它会被调用多次,当Activity的窗口得到焦点和失去焦点均会被调用。
  2. view.post(runnable)。
     通过post将一个runnable投递到消息队列的尾部,当Looper调用此runnable的时候,View也初始化好了。
  3. ViewTreeObserver。
    使用ViewTreeObserver的众多回调可以完成这个功能,比如OnGlobalLayoutListener这个接口,当View树的状态发送改变或View树内部的View的可见性发生改变时,onGlobalLayout方法会被回调。需要注意的是,伴随着View树状态的改变,onGlobalLayout会被回调多次。
  4. view.measure(int widthMeasureSpec,int heightMeasureSpec)。
    (1). match_parent:
       无法measure出具体的宽高,因为不知道父容器的剩余空间,无法测量出View的大小
    (2). 具体的数值(dp/px):
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);

(3). wrap_content:

int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);
2. View的layout过程
  • View的默认实现中,View的测量宽/高最终宽/高是相等的,测量宽/高形成于View的measure过程,而最终宽/高形成于View的layout过程。
3. View的draw过程
  • 将View绘制到屏幕上,大概的几个步骤
    1.绘制背景background.draw(canvas)
    2.绘制自己(onDraw)
    3.绘制children(dispatchDraw)
    4.绘制装饰(onDrawScrollBars)
  • View的绘制过程是通过dispatchDraw来实现的,它会遍历所有子元素的draw方法。
  • 如果一个View不需要绘制任何内容,那么设置setWillNotDraw为true后,系统会进行相应的优化;ViewGroup默认为true,如果我们的自定义ViewGroup需要通过onDraw来绘制内容的时候,需要显示的关闭它。

4.4 自定义View

  • 直接继承View或ViewGroup的控件, 需要在onmeasure中对wrap_content做特殊处理。
  • 直接继承View的控件,如果不在draw方法中处理padding,那么padding属性就无法起作用。直接继承ViewGroup的控件也需要在onMeasure和onLayout中考虑padding和子元素margin的影响,不然padding和子元素的margin无效。
  • View内部提供了post系列的方法,完全可以替代Handler的作用。
  • View中有线程和动画,需要在View的onDetachedFromWindow中停止。
  • 自定义View示例请看原著和随书源码

/////////////////////////////+++++++++++++++++++++++++++

 

本篇文章主要介绍以下几个知识点:

  • 初识 ViewRoot 和 DecorView;
  • 理解 MeasureSpec;
  • View 的工作流程:measure、layout、draw。

4.1 初识 ViewRoot 和 DecorView

为更好的理解 View 的三大流程(measurelayoutdraw),先了解一些基本的概念。

ViewRoot 对应于 ViewRootImpl 类,是连接 WindowManagerDecorView 的纽带,View 的三大流程都是通过 ViewRoot 来完成的。

View 的绘制流程从 ViewRootperformTraversals 方法开始,它经过 measure(测量 View 的宽高),layout(确定 View 在父容器的位置) 和 draw(负责将 View 绘制在屏幕上) 三个过程才能将一个 View 绘制出来,如下:

 
performTraversals 的工作流程

DecorView 是一个 FrameLayout,View 层的事件都先经过 DecorView,再传递给 View。

DecorView 作为顶级 View,一般它内部会包含一个竖直方向的 LinearLayout,上面是标题栏,下面是内容栏。在 Activity 中通过 setContentView 设置的布局文件就是被加到内容栏中,而内容栏的 id 为 content,可通过 ViewGroup content = findviewbyid(android.R.id.content) 得到 content,通过 content.getChildAt(0) 得到设置的 View。其结构如下:

顶级 View:DecorView 的结构

4.2 理解 MeasureSpec

MeasureSpec 很大程度上决定了一个 View 的尺寸规格。在 View 的测量过程中,系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,再根据这个 measureSpec 来测量出 View 的宽高(测量宽高不一定等于 View 的最终宽高)。

4.2.1 MeasureSpec

MeasureSpec 代表一个32位 int 值,高两位代表 SpecMode(测量模式),低30位代表 SpecSize(某个测量模式下的规格大小),MeasureSpec 内部的一些常量定义如下:

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
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; // MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
} // 解包:获取其原始的 SpecMode
@MeasureSpecMode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
} // 解包:获取其原始的 SpecSize
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}

SpecMode 有三类,其含义分别如下:

  • UNSPECIFIED
    父容器不对 View 有任何的限制(一般用于系统内部),表示一种测量的状态

  • EXACTLY
    父容器检测出 View 的精度大小,此时 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式

  • AT_MOST
    父容器指定一个可用大小即SpecSize,View 的大小不能大于这个值。它对应于 LayoutParams 中的 wrap_content

4.2.2 MeasureSpec 和 LayoutParams 的对应关系

Layoutparams 需要和父容器一起才能决定 View 的 MeasureSpec,一旦确定 MeasureSpec 后,onMeasure 中就可以确定 View 的测量宽高。

顶级 View(DecorView),其 MeasureSpec 由窗口的尺寸和自身的 Layoutparams 来共同决定;普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 Layoutparams 来决定。

对于 DecorView,在 ViewRootImpl 中的 measureHierarchy 方法中的一段代码展示了其 MeasureSpec 的创建过程:

// 其中 desiredWindowWidth 和 desiredWindowHeight 是屏幕的尺寸
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth , lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

接下来看下 getRootMeasureSpec 方法的实现:

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}

上述代码明确了 DecorView 的 MesourSpec 的产生过程,根据其 Layoutparams 的宽高的参数来划分,遵守如下规则:

  • LayoutParams.MATCH_PARENT
    精确模式,大小就是窗口的大小

  • LayoutParams.WRAP_CONTENT
    最大模式,大小不定,但是不能超出屏幕的大小

  • 固定大小(比如100dp)
    精确模式,大小为 LayoutParams 中指定的大小

对于 普通的 View,指布局中的 View,其 measure 过程由 ViewGroup 传递而来,先看下 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);
// 调用子元素的 measure 方法前会通过上面的 getChildMeasureSpec 方法得到子元素的 MesureSpec
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

上述对子元素进行 measure,显然,子元素的 MesureSpec 的创建和父容器的 MesureSpec 、子元素的 LayoutParams 有关和 View 的 margin 有关,其中 getChildMeasureSpec 方法如下:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 参数中的 pading 是指父容器中已占有的控件大小
// 因此子元素可以用的大小为父容器的尺寸减去 pading
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 来确定子元素的 MesureSpec。

上面getChildMeasureSpec 展示了普通 View 的 MeasureSpec 创建规则,也可参考下表(表中的 parentSize 指父容器中目前可使用的大小):

 
普通 View 的 MeasureSpec 的创建规则

当 View 采用固定宽/高时,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精确模式并且其大小遵循 LayoutParams 中的大小。

当 View 的宽/高是 match_parent 时,若父容器是精准模式,那么 View 也是精准模式并且其大小是父容器的剩余空间;若父容器是最大模式,那么 View 也是最大模式并且其大小不会超过父容器的剩余空间。

当 View 的宽/高是 wrap_content 时,不管父容器的模式是精准还是最大化,View 的模式总是最大化,并且大小不能超过父容器的剩余空间。

注:UNSPECIFIED 模式主要用于系统内部多次 Measure 的情形,一般不需关注此模式。

综上,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams,就可以快速地确定出子元素的 MeasureSpec 了,有了 MeasureSpec 就可以进一步确定出子元素测量后的大小了。

4.3 View 的工作流程

View 的工作流程主要是指 measure(测量,确定 View 的测量宽/高)、layout(布局,确定 View 的最终宽/高和四个顶点的位置)、draw(绘制,将 View 绘制到屏幕上)这三大流程。

4.3.1 measure 过程

若只是一个原始的 View,那么通过 measure 方法就完成了其测量过程,若是一个 ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个流程。

4.3.1.1 View 的 measure 过程

View 的 measure 过程由其 measure 方法来完成,measure 方法中会去调用 View 的 onMesure 方法如下:

    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 设置 View 宽/高的测量值
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

其中 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;
}

上面的 AT_MOSTEXACTLY 这两种情况,可理解为 getDefaultSize 返回的大小就是 mesourSpec 中的 specSize,而这个 specSize 就是 View 测量后的大小(测量大小不一定等于 View 的最终大小)。

至于 UNSPECIFIED 这种情况,一般用于系统内部的测量过程,View 的大小为 getDefaultSize的第一个参数是 size,其宽/高获取方法如下:

protected int getSuggestedMinimumWidth() {
// 1. 若 View 没有设置背景,View 的宽度为 mMinwidth,
// 而 mMinwidth 对应于 android:minwidth 这个属性所指定的值,
// 因此 View 的宽度即为 android:minwidth 属性所指定的值,
// 若这个属性不指定,那么 mMinWidth 则默认为0;
// 2. 若 View 指定了背景,则View的宽度为max(mMinwidth,mbackground().getMininumwidth)
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
} protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

上面注释分析了 getSuggestedMinimumWidth 方法的实现,getSuggestedMinimumHeight和它的原理一样。注释中未说明的 mBackground.getMinimumWidth() 方法(即 Drawable 的 getMinimumWidth方法)如下:

public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
// 返回 Drawable的原始宽度(有原始宽度的话),否则就返回0
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

总结 getSuggestedMinimumWidth 的逻辑:
若 View 没设背景,那么返回 android:minwidth所指定的值(可为0);
若 View 设了背景,则返回 android:minwidth和背景的最小宽度这两者中的最大值。
View 在 UNSPECIFIED 情况下的测量宽/高即为 getSuggestedMinimumWidthgetSuggestedMinimumHeight的返回值 。

结论:直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent

从上述代码中知道,若 View 在布局中使用 wrap_content,那么它的 specMode 是 AT_MOST 模式,它的宽/高等于 specSize;此情况下 View 的 specSize 是 parentSize,而 parentSize 是父容器中目前可以使用的大小,即父容器当前剩余的空间大小。显然,View 的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用 match_parent 完全一致。

解决上述问题代码如下:

    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
// 给 View 指定一个默认的内部宽/高(mWidth, mHeight),并在 wrap_content 时设置此宽/高即可
// 对于非 wrap_content 情形,沿用系统的测量值即可
//(注:TextView、ImageView 等针对 wrap_content 情形,它们的 onMeasure 方法做了特殊处理)
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}

4.3.1.2 ViewGroup 的 measure 过程

和 View 不同的是,ViewGroup 是一个抽象类,它没有重写 View 的 onMeasure 方法,但它提供了一个 measureChildren 方法:

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
// ViewGroup 在 measure 时,会对每一个子元素进行 measure
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

上述代码中的 measureChild 方法如下:

    protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 1. 取出子元素的 LayoutParams
final LayoutParams lp = child.getLayoutParams();
// 2. 通过 getChidMeasureSpec 来创建子元素的 MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 3. 将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

上面代码注释说明了 measurechild 的思想。

由于 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要各个子类去具体实现;

不同的 ViewGroup 子类有不同的布局特性,它们的测量细节各不相同,如 LinearLayout 和 RelativeLayout 这两者的布局特性不同,因此 ViewGroup 无法对其 onMeasure 方法做统一实现。


下面通过 LinearLayout 的 onMeasure 方法来分析 ViewGroup 的 measure 过程,先来看一下 LinearLayout 的 onMeasure 方法:

    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}

这里选择查看竖直方向的 LinearLayout 测量过程,即 measureVertical 方法(其源码比较长就不贴了),

这里只描述其大概逻辑:系统会遍历子元素并对每个子元素执行 measureChildBeforeLayout 方法,

此方法内部会调用子元素的 measure 方法,当子元素测量完毕之后,LinearLayout 会根据子元素的情况来测量自己的大小。


View 的 measure 过程完成后,通过 getMeasureWidth/Height 可以正确地获取到 View 的测量宽/高。但在系统要多次 measure 才能确定最终的测量宽/高的情况下,在 onMeasure 方法中拿到的测量宽/高可能是不准确的。因此建议在 onLayout 方法中去获取 View 的测量宽/高或者最终宽/高。

问题:如何在 Activity 已启动的时候获取某个 View 的宽/高?

注:由于 View 的 measure 过程和 Activity 的生命周期方法不是同步执行的,无法保证 Activiy 执行了 onCreate、onStart、onResume 时某个 View 已经测量完毕了,从而在 onCreate、onStart、onResume 中均无法正确得View的宽/高信息(若 View 还没测量完毕,那么获得的宽/高就是0)。

这里给出四种方法:

(1)Activity/View#onWindowFocusChanged

onWindowFocusChanged方法是指:View 已初始化完毕,宽/高已准备好,此时去获取宽/高是没问题的(注:当 Activity 继续执行和暂停执行时,onWindowFocusChanged 均会被调用,若频繁地进行 onResumeonPause,那么 onWindowFocusChanged 也会被频繁地调用)。典型代码如下:

    public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (!hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}

(2)view.post(runnable)

通过 post 可将一个 runnable 投递到消息队列的尾部,然后等待 Lopper 调用此 runnable 时,View 就初始化好了。典型代码如下:

    protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = mTextView.getMeasuredWidth();
int height = mTextView.getMeasuredHeight();
}
});
}

(3)ViewTreeObserver

使用 ViewTreeObserver 的众多回调可完成这个功能,典型代码如下:

    protected void onStart() {
super.onStart(); ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = mTextView.getMeasuredWidth();
int height = mTextView.getMeasuredHeight();
}
});
}

(4)view.measure(int widthMeasureSpec , int heightMeasureSpec)

通过手动测量 View 的宽高,此方法较复杂,根据 View 的LayoutParams 来分情况来处理:

  • match_parent:无法测量出具体的宽高

  • 具体的数值(dp/px):如宽高都是100dp,如下 measure:

 int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
  • wrap_content:如下measure:
 // View 的尺寸使用30位的二进制表示,即最大是30个1(即 2^30-1),也就是 (1<<30)-1
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);

关于 View 的 measure,网络上有两个错误的用法。为什么说是错误的,首先其违背了系统的内部实现规范(因为无法通过错误的 MeasureSpec 去得出合理的 SpecMode,从而导致 measure 过程出错),其次不能保证 measure 出正确的结果。

  • 第一种错误的方法:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1, View.MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1, View.MeasureSpec.UNSPECIFIED);
view.measure(widthMeasureSpec, heightMeasureSpec);
  • 第二种错误的方法:
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

4.3.2 layout 过程

Layout 是 ViewGroup 用来确定子元素的位置的,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调其 layout 方法,在 layout 方法中 onLayout 又被调用。layout 方法确定 View 本身的位置,而 onLayout 方法则会确定所有子元素的位置,View 的 layout 方法如下:

     public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight; // 1. 通过 setFrame 方法来设定 View 的四个顶点的位置,
// 即初始化 mLeft,mTop,mRight,mBottom 这四个值
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 2. View 的四个顶点一旦确定,那么 View 在父容器的位置也就确定了,
// 接下来会调用onLayout方法(用途:父容器确定子元素的位置)
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
} mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

和 onMeasure 类似,onLayout 的具体位置实现同样和具体布局有关,所有 View 和 ViewGroup 均没有真正的实现 onLayout 方法。 LinearLayout 的 onLayout 如下:

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}

LinearLayout 的 onLayout 和 onMeasure 的实现逻辑类似,就 layoutVertical 来说,其主要代码如下:

     void layoutVertical(int left, int top, int right, int bottom) {
. . . final int count = getVirtualChildCount();
// 遍历所有子元素
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight(); final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); . . . if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
} childTop += lp.topMargin;
// 调用 setChildFrame 为子元素指定对应的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i);
}
}
}

上述方法中的 setChildFrame 方法,仅仅是调用子元素的 layout 方法而已,如下:

 private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}

这样父元素在 layout 方法中完成自己的定位后,就通过 onLayout 方法去调用子元素的 layout 方法,子元素又会通过自己的 layout 方法来确定自己的位置,这样一层一层传递下去完成整个 View 树的 layout 过程。

问题:View 的测量宽/高和最终宽/高有什么区别?(即:View 的 getMeasureWidthgetWidth 这两个方法有什么区别?)

为了回答这个问题,先看下 getWidthgetHeight 方法的实现:

    public final int getWidth() {
return mRight - mLeft;
} public final int getHeight() {
return mBottom - mTop;
}

可以看出,getWidthgetHeight 返回的刚好是 View 的测量宽度、高度。

对于上面的问题:在 View 的默认实现中,View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于 View 的 measure 过程,一个是 layout 过程,而最终宽/高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。

日常开发中可用认为 View 的测量宽/高 = 最终宽/高,但某些特殊情况下,如重写 View 的 layout 方法如下:

 public void layout(int l,int t,int r, int b){
super.layout(l, t, r + 100, b + 100);
}

上述代码会导致在任何情况下 View 的最终宽/高总是比测量宽/高大 100px。

4.3.3 draw 过程

Draw 过程其作用是将 View 绘制到屏幕上面。View 的绘制过程遵循如下几步:

(1)绘制背景 background.draw(canvas)

(2)绘制自己 (onDraw)

(3)绘制 children (dispatchDraw)

(4)绘制装饰 (onDrawSrcollBars)

这一点通过 draw 方法的源码可看出来:

     public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; /*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/ // Step 1, draw the background, if needed
int saveCount; if (!dirtyOpaque) {
drawBackground(canvas);
} // skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children
dispatchDraw(canvas); // Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
} // Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas); // we're done...
return;
} . . .
}

View 绘制过程的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历所有子元素的 draw 方法,如此 draw 事件就一层层地传递下去。View 有一个特殊的方法 setwilINotDraw

public void setwilINotDraw(boolean willNotDraw){
// 若一个 View 不需要绘制任何内容,那么设置这个标记位为 true 以后,系统会进行相应的优化。
// 默认情况下,View 没有启用这个校化标记位,但 ViewGroup 会默认启用这个优化标记位。
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

实际开发中,自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。若明确知道一个 ViewGroup 需要通过 onDraw 来绘制内容时,需要显式地关闭 WILL_NOT_DRAW 这个标记位。

Android之view的工作原理2的更多相关文章

  1. Android 中View的工作原理

    Android中的View在Android的知识体系中扮演着重要的角色.简单来说,View就是Android在视觉的体现.我们所展现的页面就是Android提供的GUI库中控件的组合.但是当要求不能满 ...

  2. Android艺术开发探索第四章——View的工作原理(上)

    这章就比较好玩了,主要介绍一下View的工作原理,还有自定义View的实现方法,在Android中,View是一个很重要的角色,简单来说,View是Android中视觉的呈现,在界面上Android提 ...

  3. 《Android开发艺术探索》读书笔记 (4) 第4章 View的工作原理

    本节和<Android群英传>中的第3章Android控件架构与自定义控件详解有关系,建议先阅读该章的总结 第4章 View的工作原理 4.1 初始ViewRoot和DecorView ( ...

  4. Android学习笔记View的工作原理

    自定义View,也可以称为自定义控件,通过自定义View可以使得控件实现各种定制的效果. 实现自定义View,需要掌握View的底层工作原理,比如View的测量过程.布局流程以及绘制流程,除此之外,还 ...

  5. Android艺术开发探索第四章——View的工作原理(下)

    Android艺术开发探索第四章--View的工作原理(下) 我们上篇BB了这么多,这篇就多多少少要来点实战了,上篇主席叫我多点自己的理解,那我就多点真诚,少点套路了,老司机,开车吧! 我们这一篇就扯 ...

  6. 梳理源码中 View 的工作原理

    欢迎Follow我的GitHub, 关注我的掘金. 在View的工作过程中, 执行三大流程完成显示, 测量(measure)流程, 布局(layout)流程, 绘制(draw)流程. 从perform ...

  7. View的工作原理(一) 总览View的工作流程

    View的工作原理(一) 总览View的工作流程 学习自 <Android开发艺术探索> 简书博主-丶蓝天白云梦 Overview 从本章开始,开始学习View的工作原理,包括View的 ...

  8. android多线程-AsyncTask之工作原理深入解析(上)

    关联文章: Android 多线程之HandlerThread 完全详解 Android 多线程之IntentService 完全详解 android多线程-AsyncTask之工作原理深入解析(上) ...

  9. android多线程-AsyncTask之工作原理深入解析(下)

    关联文章: Android 多线程之HandlerThread 完全详解 Android 多线程之IntentService 完全详解 android多线程-AsyncTask之工作原理深入解析(上) ...

随机推荐

  1. nginx中ngx_http_access_module模块

    实现基于IP的访问控制功能指令:4.1 allow允许访问指定的⽹网络或地址Syntax: allow address | CIDR | unix:| all;Default: —Context: h ...

  2. Redis键值设计(转载)

    参考资料:https://blog.csdn.net/iloveyin/article/details/7105181 丰富的数据结构使得redis的设计非常的有趣.不像关系型数据库那样,DEV和DB ...

  3. MySQL分组排序(取第一或最后)

    MySQL分组排序(取第一或最后) 方法一:速度非常慢,跑了30分钟 SELECT custid, apply_date, rejectrule FROM ( SELECT *, IF ( , ) A ...

  4. [Javascript] Create an Image with JavaScript Using Fetch and URL.createObjectURL

    Most developers are familiar with using img tags and assigning the src inside of HTML. It is also po ...

  5. 2019-2020 ICPC, NERC, Southern and Volga Russian Regional Contest (Online Mirror, ICPC Rules, Teams Preferred)【A题 类型好题】

    A. Berstagram Polycarp recently signed up to a new social network Berstagram. He immediately publish ...

  6. 设置tomcat实现跨域

    1.tomcat下自带的cors过滤器 修改tomcat路径下conf/web.xml文件 <filter> <filter-name>CorsFilter</filte ...

  7. ICEM—倾斜孔

    原视频下载:https://yunpan.cn/cS3UGMEscrYpL  访问密码 839b

  8. centos7中oracle数据库安装和卸载

    参考: 完全命令行安装(验证可行):https://jingyan.baidu.com/article/90895e0f29c92164ec6b0bd1.html 存在疑问:是否需要jdk的配置(因为 ...

  9. 使用PyMySQL连接MySQL错误

    使用PyMySQL连接MySQL错误 之前写了一个小项目,今天突然想起来,准备优化一下,但是原本好好的项目竟然跑不起来了 emmm....我真的啥都没干呀 具体错误是这样的: Traceback (m ...

  10. Flutter移动电商实战 --(22)JSON解析和复杂数据模型转换技巧

    json转Model类 创建model文件夹,在里面新建category.dart类 主要根据这个json来分析我们要做成类的样子 { "code": "0", ...