View的工作原理

目录

  • 初识ViewRoot和DecorView
  • 理解MeasureSpec
  • View的工作流程
  • 自定义View

初识ViewRoot和DecorView

View的绘制流程从ViewRoot的perfromTraversals方法开始,他经过measure,layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高,layout用来确定view在父容器中的放置位置,draw负责将View绘制在屏幕上,针对perfromTraversals的大致流程,可以看图:

perfromTraversals的工作流程图

图中的perfromTraversals会依次调用perfromMeasure,perfromLayout,perfromDraw,他们分别完成顶级View的measure,layout和draw这三大流程,其中在perfromMeasure中会调用measure方法,在measure方法中又调用onMeasure,这个时候measure流程就从父容器传递到子元素了,这样就完成了一次measure过程,接着子元素会重复父容器的measure过程,如此反复的完成了整个View树的遍历,同理,其他两个也是如此,唯一有点区别的是perfromDraw的传递过程是在draw反复中通过dispatchDraw来实现的,不过这并没有什么本质的区别。

measure过程决定了View的宽高,Measure完成之后可以通过getMeasureWidth和getMeasureHeight来获取View测量后的高宽,在所有的情况下塔几乎都是等于最终的宽高,但是特殊情况除外。

layout过程决定了view的四个顶点的坐标和实际View的宽高,完成之后,通过getTop,getLeft,getRight,getBottom来拿到View的四个顶点的位置,并可以通过getWight和getHeight方法来拿到View的最终宽高。

Draw决定了View的显示,只有draw方法完成了之后,view才会显示在屏幕上。

理解MeasureSpec

MeasureSpec代表一个32位int值,高两位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某个测量模式下的规格大小。

SpecMode有三类,每一类都有特殊的含义

  • UNSPECIFIED

父容器不对View有任何的限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。

  • EXACTLY

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

  • AT_MOST

父容器指定了一个可用大小,即SpecSize,view的大小不能大于这个值,具体是什么值要看不同view的具体实现,它对应于LayoutParams中wrap_content。

MeasureSpec 和 LayoutParams 的对应关系

MeasureSpec不是唯一由layoutparams决定的,layoutparams需要和父容器一起决定view的MeasureSpec从而进一步决定view的宽高,对于顶级view(DecorView)和普通的view来说,MeasureSpec的转换过程有些不同,对于decorview,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams来共同决定,MeasureSpec一旦确定后,onMeasure就可以确定View的测量宽高。

  • LayouParams.MATCH_PARENT:精确模式,大小就是窗口的大小
  • LayouParams.WRAP_CONTENT:最大模式,大小不定,但是不能超出屏幕的大小
  • 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小

对于普通View,其MeasureSpec 由父容器的MeasureSpec和自身的LayoutParams来共同决定,那么针对不同的父容器和Viev本身不同的LayoutParams,View就可以有多种MeasureSpec。

当View采用固定宽/高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpee都是精确模式,那么View也是精准模式并且其大小是父容器的剩余空间;

如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。

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

View的工作流程

measure过程

View的measure过程

  • measure
/** 开始测量一个View有多大,parent会在参数中提供约束信息,实际的测量工作是在onMeasure()中进行的,该方法会调用onMeasure()方法,所以只有onMeasure能被也必须要被override */
public final void measure(int widthMeasureSpec, int heightMeasureSpec);

父布局会在自己的onMeasure方法中,调用child.measure ,这就把measure过程转移到了子View中。

  • onMeasure
/** 具体测量过程,测量view和它的内容,来决定测量的宽高(mMeasuredWidth  mMeasuredHeight )。该方法中必须要调用setMeasuredDimension(int, int)来保存该view测量的宽高。 */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);

子View会在该方法中,根据父布局给出的限制信息,和自己的content大小,来合理的测量自己的尺寸。

  • setMeasuredDimension
/** 保存测量结果 */
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight);

当View测量结束后,把测量结果保存起来,具体保存在mMeasuredWidth和mMeasuredHeight中。

ViewGroup的measure过程

  • measureChildren
/** 让所有子view测量自己的尺寸,需要考虑当前ViewGroup的MeasureSpec和Padding。跳过状态为gone的子view */
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec);-->getChildMeasureSpec()-->child.measure();

测量所有的子View尺寸,把measure过程交到子View内部。

  • measureChild
/** 测量单个View,需要考虑当前ViewGroup的MeasureSpec和Padding。 */
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec);-->getChildMeasureSpec()-->child.measure();

对每一个具体的子View进行测量。

  • measureChildWithMargins
/** 测量单个View,需要考虑当前ViewGroup的MeasureSpec和Padding、margins。 */
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed);-->getChildMeasureSpec()-->child.measure();

对每一个具体的子View进行测量。但是需要考虑到margin等信息。

  • getChildMeasureSpec
/** measureChildren过程中最困难的一部分,为child计算MeasureSpec。该方法为每个child的每个维度(宽、高)计算正确的MeasureSpec。目标就是把当前viewgroup的MeasureSpec和child的LayoutParams结合起来,生成最合理的结果。
比如,当前ViewGroup知道自己的准确大小,因为MeasureSpec的mode为EXACTLY,而child希望能够match_parent,这时就会为child生成一个mode为EXACTLY,大小为ViewGroup大小的MeasureSpec。
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension);

根据当前自身的状况,以及特定子View的尺寸参数,为特定子View计算一个合理的限制信息。

layout过程

Layout的作用是ViewGroup用来确定子元素的作用的,当ViewGroup的位置被确认之后,他的layout就会去遍历所有子元素并且调用onLayout方法,在layout方法中onLayou又被调用。

layout方法确定了View本身的位置,而onLayout方法则会确定所有子元素的位置。

layout的方法的大致流程如下,首先会通过一个setFrame方法来设定View的四个顶点的位置,即初始化mLeft,mTop,mRight,mBottom这四个值,View的四个顶点一旦确定,那么View在父容器的位置也就确定了。接下来会调用onLayout方法,这个方法的用途是调用父容器确定子元素的位置。

draw过程

draw过程的作用是将View绘制到屏幕上面,View的绘制过程:

  1. 绘制背景background.draw(canvas)。
  2. 绘制自己(onDraw)。
  3. 绘制children(dispatchDraw)。
  4. 绘制装饰(onDrawScrollBars)。

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

自定义View

自定义View的分类

  1. 继承View重写onDraw方法
  2. 继承ViewGroup派生出来的Layout
  3. 继承特定的View
  4. 继承特定的ViewGroup

自定义View的须知

  1. 让View支持warp_content
  2. 如果有有必要,让你的View支持padding
  3. 尽量不要在View中使用Handler
  4. View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
  5. View带有滑动嵌套时,需要处理好滑动冲突

参考资料:

《Android开发艺术探索》

Android开发艺术探索笔记

Android View框架的measure机制

你可能感兴趣的:(View的工作原理)