Android|第4章 View的工作原理

一、ViewRoot 和 DecorView

  • ViewRoot对应于ViewRootImpl 类,它是连接WIndowManager 和 DecorView的纽带,View的三大流程均是通过VIewRoot来完成的,在ActivityThread中,当Activity对象被创建后,会将DecorView 添加到Window中,同时会创建VIewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联
  • 【Android|第4章 View的工作原理】View 的绘制流程是从ViewRoot的 performTraversals 方法开始的,performTraversals 依次调用 performMeasure、performLayout 和 performDraw,这三个方法分别完成顶级View的measure、layout 和 draw
  • performMeasure -> measure -> onMeasure,对子元素进行measure(layout 和 draw类似)
  • measure过程决定了View的宽/高,getMeasuredwidth/getMeasuredHeight,除特殊情况外它都等于View的最终宽/高;Layout过程决定View的四个顶点坐标和实际宽/高,getTop/getBottom/getLeft/getRight/getWidth/getHeight;Draw决定View的显示
  • DecorView作为顶级View,包含上下两个部分,上面是标题栏,下面是内容栏,Activity中 setContentView 所设置的布局是被加到内容栏;View层的事件都先经过DecorView 然后传递给我们的View
二、MeasureSpec
MeasureSpec代表一个32位的int值,高 2 位代表SpecMode 测量模式,低30位代表 SpecSize 规格大小
1. SpecMode有三类
  • UNSPECIFIED:父容器不对View有任何限制,要多大有多大,一般用于系统内部
  • EXACTLY:父容器已经检测说View所需要的的精确大小,View的最终大小就是SpecSize指定的值,对应于 match_parent 和 具体数值两种模式
  • AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,对应于 wrap_content
2. MeasureSpec 和 LayoutParams 对于顶级View(DecorView),其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定;
对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同确定
3. View 的measure View的measure过程由ViewGrou传递而来,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); }

4. 注意
  • 当View采用固定宽/高的时候,不管父容器的 MeasureSpec是什么,View的MeasureSpec 都是精确模式且遵循Layoutparams中的大小
  • 当View的宽/高是 match_parent 时,如果父容器是最大模式,那么子View也是最大模式,且不超过父容器的剩余空间
  • 当View的宽/高是 wrap_content 时,不管父容器的模式是精准还是最大化,View的模式总是最大化,且不超过父容器的剩余空间
三、View的工作流程
1. measure 过程 1)View 的 measure过程 View 的measure过程由其measure方法来完成,measure方法是一个final类型的方法,子类不能重写,measure方法中会去调用 onMeasure->setMeasuredDimension方法会设置View 宽/高的测量值
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }

2)ViewGroup 的 measure过程 ViewGroup 除了完成自己的measure,还会遍历去调用所有子元素的measure,各个子元素再递归去执行这个过程。ViewGroup 是一个抽象类,没有重写 onMeasure,其测量过程的onMeasure 方法需要各个子类去具体实现,但提供了 measureChildren方法,ViewGroup 在measure时,会对每一个子元素进行measure
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); } } }

3)异步获取View宽高 如果想在onCreate等方法里获取View的宽高,有4种方式:
  • Activity/View#onWindowFocusChanged
onWindowFocusChanged 表示View已经初始化完毕,宽高已经准备好,在Activity的窗口得到和失去焦点时均会被调用
  • view.post(runnable)
  • ViewTreeObserver#onGlobalLayoutListener
  • view.measure 手动measure
2. Layout 过程 ViewGroup的位置被确定后,onLayout中会遍历所有的子元素并调用其 layout方法,在layout方法中 onLayout方法又会被调用;layout方法确定View 本身的位置,onLayout方法确定所有子元素的位置
View 和 ViewGroup 均没有真正实现 onLayout
3. Draw 过程
  • 绘制背景 background.draw(canvas)
  • 绘制自己 (onDraw)
  • 绘制 children (dispatchDraw)
  • 绘制装饰(onDrawScrollBars)
四、自定义View
1. 分类 1)继承View重写onDraw方法 2)继承ViewGroup 派生特殊的 Layout 3)继承特定的View 4)继承特定的ViewGroup 2. 一些注意 1)让View支持 wrap_content 2)如果有必要,让View支持 padding 3)尽量不要再View中使用 Handler 4)View中如果有线程或者动画,需要及时停止 View#onDetachedFromWindow

    推荐阅读