View绘制流程(一)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了View绘制流程(一)相关的知识,希望对你有一定的参考价值。

参考技术A

最近在学习 View 的绘制流程,看了几篇不错的博客( ViewRootImpl的独白,我不是一个View(布局篇) 、 android应用层View绘制流程与源码分析 )自己对照源码,梳理了一遍。

在Activity的onResume之后,当前Activity的Window对象中的View会被添加在WindowManager中。

整个View树的绘图流程是在 ViewRootImpl 类的 performTraversals() 方法(这个方法巨长)开始的,该方法做的执行过程主要是根据之前设置的状态,判断是否重新计算视图大小 (measure) 、是否重新放置视图的位置 (layout) 、以及是否重绘 (draw) ,其核心也就是通过判断来选择顺序执行这三个方法。

ViewRootImpl 调用 performMeasure 执行Window对应的View的测量。

int widthMeasureSpec :他由两部分组成, 高2位表示MODE ,定义在MeasureSpec类(View的内部类)中,有三种类型, MeasureSpec.EXACTLY 表示确定大小, MeasureSpec.AT_MOST 表示最大大小, MeasureSpec.UNSPECIFIED 不确定。 低30位表示size ,也就是父View的大小。对于系统Window类的DecorVIew对象Mode一般都为MeasureSpec.EXACTLY ,而size分别对应屏幕宽高。对于子View来说大小是由父View和子View共同决定的。

默认的尺寸大小即传入的参数都是通过 getDefaultSize 返回的,我们就看一下该方法的实现。

到此一次最基础的元素View的 measure 过程就完成了。

View实际是嵌套的,而且measure是递归传递的,所以每个View都需要 measure ,能够嵌套的View都是ViewGroup的子类,所以在ViewGroup中定义了 measureChildren , measureChild , measureChildWithMargins 方法来对子视图进行测量, measureChildren 内部实质只是循环调用 measureChild , measureChild 和 measureChildWithMargins 的区别就是是否把 margin padding 也作为子视图的大小。 ViewGroup 本身不调用 measureChildWithMargins 和 measureChildren 方法,由继承类通过for循环调用此方法进行子View的测量。下面看一下ViewGroup中稍微复杂的 measureChildWithMargins 方法。

getChildMeasureSpec 的逻辑是通过其父View提供的 MeasureSpec 参数得到 specMode 和 specSize ,然后根据计算出来的 specMode 以及子View的 childDimension (layout_width或layout_height)来计算自身的 measureSpec ,如果其本身包含子视图,则计算出来的 measureSpec 将作为调用其子视图 measure 函数的参数,同时也作为自身调用 setMeasuredDimension 的参数,如果其不包含子视图则默认情况下最终会调用 onMeasure 的默认实现,并最终调用到 setMeasuredDimension 。

Activity 的 onResume 之后,当前 Activity Window 对象中的View(DecorView)会被添加在 WindowManager 中。也就是在 ActivityThread 的 handleResumeActivity 方法中调用 wm.addView(decor, l); 将DecorView添加到 WindowManager 中;

WindowManager 继承 ViewManager ,它的实现类为 WindowManagerImpl ,该类中的方法的具体实现是由其代理类 WindowManagerGlobal 实现的;

在它的 addView 方法中会创建 ViewRootImpl 的实例,然后将Window对应的View(DecorView),ViewRootImpl,LayoutParams顺序添加在WindowManager中,最后将Window所对应的View设置给创建的ViewRootImpl,通过 ViewRootImpl 来更新界面并完成Window的添加过程;

设置view调用的是 ViewRootImpl 的 setView 方法,在该方法中调用 requestLayout(); 方法来异步执行view的绘制方法;之后将 Window 添加到屏幕,通过 WMS (跨进程通信)

在 requestLayout 方法中最终会调用 ViewRootImpl 的 performTraversals(); 方法,该方法做的执行过程主要是根据之前设置的状态,判断是否重新计算视图大小 (measure) 、是否重新放置视图的位置 (layout) 、以及是否重绘 (draw) ,其核心也就是通过判断来选择顺序执行这三个方法: performMeasure 、 performLayout 、 performDraw ;

在 performMeasure 方法中调用的是 View 的 measure 方法,该方法是 final 修饰,不能被子类重写,在该方法中实际调用的是 View 的 onMeasure 方法,子类可以重写 onMeasure 方法来实现自己的测量规则。

View 默认的 onMeasure 方法很简单只是调用了 setMeasuredDimension 方法,该方法的作用是给 View 的成员变量 mMeasuredWidth mMeasuredHeight 赋值,View的测量主要就是给这两个变量赋值,这两个变量一旦赋值,也就意味着测量过程的结束。

setMeasuredDimension 方法传入的尺寸是通过 getDefaultSize(int size, int measureSpec); 方法返回的,在
getDefaultSize 方法中解析 measureSpec Mode Size ,如果Mode为 MeasureSpec.AT_MOST 或者 MeasureSpec.EXACTLY ,最终的size的值为解析后的size;如果 Mode MeasureSpec.UNSPECIFIED ,最终的size为建议的最小值= getSuggestedMinimumWidth ,该方法的具体实现为 return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); ,建议的最小宽度和高度都是由View的Background尺寸与通过设置View的 miniXXX 属性共同决定的

measureSpec 是由 getRootMeasureSpec 方法决定的: measureSpec = View.MeasureSpec.makeMeasureSpec(windowSize, View.MeasureSpec.EXACTLY); 根布局的大小是 Window 的大小,Window大小是不能改变的,总是全屏的。

View实际是嵌套的,而且measure是递归传递的,所以每个View都需要measure,能够嵌套的View都是ViewGroup的子类,所以在ViewGroup中定义了 measureChildren , measureChild , measureChildWithMargins 方法来对子视图进行测量, measureChildren 内部实质只是循环调用 measureChild , measureChild 和 measureChildWithMargins 的区别就是是否把 margin padding 也作为子视图的大小。

measureChildWithMargins 方法的作用就是对 父View 提供的 measureSpec 参数结合 子View LayoutParams 参数进行了调整,然后再来调用 child.measure() 方法,具体通过方法 getChildMeasureSpec 方法来进行参数调整。计算出来自身的 measureSpec 作为调用其子视图 measure 方法的参数,同时也作为自身调用 setMeasuredDimension 的参数,如果其不包含子视图则默认情况下最终会调用 onMeasure 的默认实现,并最终调用到 setMeasuredDimension 。

最终决定 View measure 大小是 View 的 setMeasuredDimension 方法,该方法就是设置mMeasuredWidth和mMeasuredHeight的大小,ViewGroup在 onMeasure 方法调用 setMeasuredDimension 之前调整了 measureSpec

每日一问:简述 View 的绘制流程

Android 开发中经常需要用一些自定义 View 去满足产品和设计的脑洞,所以 View 的绘制流程至关重要。网上目前有非常多这方面的资料,但最好的方式还是直接跟着源码进行解读,每日一问系列一直追求短平快,所以本文笔者尽量精简。

想必大多数 Android 开发都知道自定义 View 需要关注的几个方法:onMeasure()onLayout()onDraw(),这其实也是每个 View 至关重要的绘制流程。

基本绘制都是会从根视图 ViewRootperformTraversals() 方法开始,从上到下遍历整个视图树,每个View控件负责绘制自己,而 ViewGroup 还需要负责通知自己的子 View 进行绘制操作。performTraversals() 的核心代码如下:

private void performTraversals() 
    ...
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ...
    //执行测量流程
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    //执行布局流程
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    ...
    //执行绘制流程
    performDraw();

measure()

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

每个 View 都有自己的大小,所以基本自定义 View 的时候都需要重写 onMeasure() 这个方法,以定制化我们的 View 的宽高。如果不重写这个方法,我们通常会出现 wrap_contentmatch_parent 是一样的显示效果。至于原因,其实一探源码便知。

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;


protected int getSuggestedMinimumHeight() 
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
       

可以看到,View 默认是会使用 getDefaultSize() 方法进行设置宽高的,在 AT_MOSTEXACTLY 两种情况下都会直接使用测量规格里面的尺寸。在 UNSPECIFIED 模式下会直接取getSuggestedMinimumWidth() 的返回值。

getSuggestedMinimumWidth() 会直接根据是否设置 backgroud 来进行计算,需要注意的是,直接设置 color 作为 backgroud 也会直接采用 minXXX 的值。

ViewGroup 中,并没有去重写 ViewonMeasure() 方法,而这都需要它的子类根据自己的逻辑去实现,比如 LinearLayoutRelativeLayout 明显测量逻辑是不一样的。不过,ViewGroup 倒是提供了一个 measureChildren() 方法来依次遍历每个子 View 对其进行测量。

在经过 onMeasure() 操作后,getMeasureWidth()getMeasureHeight() 方法就可以拿到正确的返回值了。

由于 View 的 measure 过程和 Activity 的生命周期方法不是同步执行的,如果 View 还没有测量完毕,那么获得的宽/高就是 0。所以在 onCreate()onStart()onResume() 中均无法正确得到某个 View 的宽高信息。可以通过在 onWindowFocusChanged() 判断获取到焦点后进行获取,或者使用 view.post() 方式。

layout()

public void layout(int l, int t, int r, int b)

我们可以重写的 onLayout() 方法主要作用是确定子 View 的显示位置,由于 View 已经是最小的层级,所以我们在自定义 View 的时候通常不需要管这个方法,而在自定义 ViewGroup 的时候就不得不注意这个方法了。

经过 onLayout() 流程后,我们的 leftrighttopbottom 得以赋值,所以这时候可以通过 getWidth()getHeight() 方法来获取 View 的实际宽高了。

注意:在 View 的默认实现中,View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于 View 的 measure 过程,而最终宽/高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。在一些特殊的情况下则两者不相等:

draw()

public void draw(Canvas canvas)

绘制的流程也就是通过调用 View 的 draw() 方法实现的。draw() 方法里的逻辑看起来更清晰,我就不贴源码了。一般是遵循下面几个步骤:

  • 绘制背景 – drawBackground()
  • 绘制自己 – onDraw()
  • 绘制孩子 – dispatchDraw()
  • 绘制装饰 – onDrawScrollbars()

由于不同的控件都有自己不同的绘制实现,所以V iew 的 onDraw() 方法肯定是空方法。而 ViewGroup 由于需要照顾子 View 的绘制,所以肯定在 dispatchDraw() 方法里遍历调用了child的 draw() 方法。

参考:
Android View的绘制流程
https://blog.csdn.net/yisizhu/article/details/51527557

以上是关于View绘制流程(一)的主要内容,如果未能解决你的问题,请参考以下文章

Android View 绘制流程

View的绘制流程

Android - View 绘制流程

Android一步步深入了解View:视图绘制流程完全解析

View的绘制流程源码分析

Android View 绘制流程(Draw) 完全解析