View的绘制流程

View的绘制流程

Activity、Window、DecorView、ViewRoot之间关系

知识储备

Activity : Activity只负责控制生命周期和处理事件,视图控制由Window负责。

Window:每个Activity都会创建一个Window用于承载View视图的显示,Window是一个抽象类存在了一个唯一实现类PhoneWindow

PhoneWindow:PhoneWindow是Android系统中的一个关键类,它是Android应用程序窗口的实现类之一,用于管理应用程序窗口的显示和交互。

DecorViewDecorView继承自FrameLayout,可以认为DecorViewView的根布局。DecorView内部是一个LinearLayout。1

包含三个子ViewViewStub(用于运行时动态加载布局资源)、TitleViewContentView

  • ViewStub:ViewStub(用于运行时动态加载布局资源)

  • TitleBar:屏幕顶部的状态栏

  • ContentView:Activity对应的XML布局,通过setContentView设置到DecorView中。

Activity.onCreate()方法中调用setContentView()方法就是将Activity资源文件添加到ContentView中。

ViewRoot:ViewRoot的实现类为ViewRootImpl,它是连接WindowManagerServiceDecorView的纽带所有View的绘制和事件分发都是通过ViewRoot完成。

View的绘制

前期工作

  1. 通过ClassLoader创建一个Activity实例后,调用activity#attach()方法对Activity进行初始化。

  2. Activity#attach()方法中初始化一个PhoneWindow对象,与该Activity进行绑定

    final void attach() {
                ......
            
                mWindow = new PhoneWindow(this, window, activityConfigCallback);
                ......
            }

  3. Activity#onCreate()生命周期初始化DecorView对象;

  4. Activity#onResume()生命周期之后,在WindowManagerGlobal#addView()方法中初始化ViewRoot对象,然后将DecorViewViewRoot关联,调用ViewRoot#setView()方法开始View绘制流程。

  5. ViewRoot中将View的绘制流程封装为一个Runnable,在Choreographer类中将其封装为一个异步消息,然后通过主线程Handler发送该消息,使View的绘制优先被处理。

  6. 这个消息被处理后,最终会来到View的绘制的入口方法performTraversals(),分别调用performMeasure()performLayout()performDraw()进行View的测量、布局与绘制。

private void performTraversals() {
     ...............
    //measur过程
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
     ...............
    //layout过程
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
     ...............
    //draw过程
    performDraw();
}

开始绘制

  • measure:为测量宽高过程,如果是ViewGroup还要在onMeasure中对所有子View进行measure操作。

  • layout:用于摆放View在ViewGroup中的位置,如果是ViewGroup要在onLayout方法中对所有子View进行layout操作。

  • draw:往View上绘制图像。

Measure

ViewRootImpl#performMeasure()方法中会调用DecorView#measure()(也就是View#measure()方法),从根布局DecorView开始View的绘制。下面代码的mView就是我们的DecorView

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
      if (mView == null) {
          return;
      }
      try {
          mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      } finally {
          Trace.traceEnd(Trace.TRACE_TAG_VIEW);
      }
}

.在ViewGroup#onMeasure()方法中,会遍历所有的子View,通过ViewGroup#measureChildWithMargins(),结合父ViewMeasureSpecmarginpadding属性计算子ViewMeasureSpec,然后调用子Viewmeasure()方法。

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        //计算子`View`的`MeasureSpec`
        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);
        //调用子`View`的`measure()`方法
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

然后ViewGroup结合MeasureSpec和子View的最大宽高计算出自己的宽高。

View#onMeasure()方法中,只需要设置自己的宽高即可,若SpecModeAT_MOSTEXACTLY,则直接使用SpecSize作为控件的宽高。

View#setMeasruedDimensionRaw()方法中会设置mMeasureWidthmMeasureHeight的值,这样在Measure阶段完成之后就可以通过View#getMeasureWidth()View#getMeasureHeight()方法获取控件的测量宽高了。

补充知识点:MeasureSpec

Measure过程中使用到一个比较重要的参数MeasureSpec,这是一个int型变量,2位代表测量模式SpecMode,后30位代表测量尺寸SpecSize,它是父控件提供给子控件的参数,用于作为一个子控件自身大小的参考。

SpecMode共有以下三种模式:

SpecMode说明
UNSPECIFIED父容器不对View有任何限制,一般用于系统内部。例如我们常用的ScrollView就是这种测量模式。
EXACTLY父容器已经检测出View所需大小,View的最终大小为SpecSize。对应LayoutParams中的match_parent或具体数值。
AT_MOST父容器指定了一个可用大小SpecSizeView的大小不能超过这个阈值。对应LayoutParams中的wrap_content
Layout

Layout阶段根据前面测量出View的尺寸以及View的一些属性(paddingmargin)来确定View的位置,同样也是从根布局DecorView开始,调用DecorView#layout()方法,最终来到View#layout()

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        .........
        final View host = mView;
        if (host == null) {
            return;
        }
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        .........
}

View中通过mLeftmTopmBottommRight四个变量来保存该View到父View的距离,在setFrame()方法中更新这几个变量的值,来确定该View在父布局中的位置。

如果ViewViewGroup类型,需要重写onLayout()方法对子控件进行布局(默认情况下onLayout()方法是个空实现),以FrameLayout为例,调用layoutChildren()遍历所有子View,计算出子View与父View的相对位置,然后调用child#layout()方法对子View进行布局。

Layout阶段会设置ViewmLeftmTopmBottommRight四个变量的值,然后可以通过调用View#getWidth()View#getHeight()方法来获取控件实际的宽高。

Draw

相比于measure和layout阶段,draw阶段中View和ViewGroup变得没那么紧密了,View的绘制过程中不需要考虑ViewGroup,而ViewGroup也只需触发子View的绘制方法即可。

performDraw()执行后同样会从根布局开始逐层对每个View进行draw操作,在View中绘制操作时通过draw()进行,来看一下其主要源码:

public void draw(Canvas canvas) {
     ........
    // 绘制背景
    drawBackground(canvas);
    // 绘制内容,每个控件自己实现自己的绘制逻辑
    onDraw(canvas);
    // 绘制子View
    dispatchDraw(canvas);
    // 绘制装饰,如scrollBar
    onDrawForeground(canvas)
    ........
}

一些常用的绘制内容:

  • Canvas:画布,不管是文字,图形,图片都要通过画布绘制而成

  • Paint:画笔,可设置颜色,粗细,大小,阴影等等等等,一般配合画布使用

  • Path:路径,用于形成一些不规则图形。

  • Matrix:矩阵,可实现对画布的几何变换。

总结

从ViewRoot#perfromTraversals()方法开始View的绘制,分别调用performMeasure()、performLayout()、performDraw()进行View的测量、布局、绘制。

Measure:首先调用根布局DecorView#measure()方法开始,实际测量逻辑在onMeasure()方法中,measure()方法被final修饰,因此如果我们要重写测量的逻辑只能重写onMeasure()方法,父View先遍历子View,然后调用子View#measure()方法,测量完子View之后才测量父View自己。

Layout:首先调用DecorView#layout()方法开始布局,layout()方法同样被final修饰,因此如果我们要自定义View的布局只能在onLayout方法中完成。在父View中调用setFrame()方法完成自身布局,然后调用onLayout()方法对子View进行布局,调用layoutChildren() -> child#layout()

Draw:首先调用DecorView#draw(),如果是ViewGroup判断是否需要跳过自身绘制,是则调用dispatchDraw(),否则调用onDraw()绘制本身,然后再dispatchDraw()。

自定义View重写方法:

  • onMeasure():自定义View的大小

  • onLayout():改变View在父控件中的位置

  • onDraw():绘制View图像

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值