Android View的绘制过程

DecorView是一个应用窗口的根容器,它本质上是一个FrameLayout.

DecorView有唯一一个子View,是一个垂直的LinearLayout,包含两个子元素:TitleView(ActionBar的容器) 和 ContentView(窗口内容的容器).

ContentView是一个FrameLayout(android.R.id.content),我们平时用的setContentView就是设置它的子View.

上图还表达了每个Activity都与一个Window(具体来说是PhoneView)相关联,用户界面则由Window所承载.

Window

Window即窗口,这个概念在Android Framework中的实现为android.view.Window这个抽象类,这个抽象类是对Android系统中的窗口的抽象.

实际上 , 窗口是一个宏观的思想 ,它是屏幕上用于绘制各种UI元素和响应用户输入事件的一个矩形区域.

特点:

  • 独立绘制 , 不与其他界面相互影响
  • 不会触发其他界面是输入事件

setContentView

在分析setContentView()方法前,我们需要明确:这个方法只是完成了Activity的ContentView的创建,而并没有执行View的绘制流程.

当我们自定义Activity继承自android.app.Activity时候,调用的setContentView()方法是Activity类的,源码如下:

public void setContentView(@LayoutRes int layoutResID) {      getWindow().setContentView(layoutResID);      . . .}

getWindow()会返回Activity所关联的PhoneWindow , 所以实际上调用的是PhoneWindow的setContentView()方法.

ViewRoot

View的绘制是由ViewRoot来负责的.每个应用窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的 , 是在Activity启动时建立的.ActivityThread.handleResumeActivity()方法建立了二者的关联关系.

当建立了decorView和ViewRoot的关联关系后,ViewRoot的requestLayout()方法会被调用,以完成应用程序用户界面的初次布局.

实际被调用的是ViewRootImpl类的requestLayout()方法.

@Overridepublic void requestLayout() {  if (!mHandlingLayoutInLayoutRequest) {    // 检查发起布局请求的线程是否为主线程      checkThread();    mLayoutRequested = true;    scheduleTraversals();  }}

scheduleTraversals()会向主线程发送一个"遍历"消息,最终会导致ViewRootImpl的performTraversals()方法被调用.

View的绘制流程开始于ViewRoot的performTraversals()方法

performTraversals()依次调用performMeasure()、performLayout()和performDraw()三个方法,分别完成顶级View的绘制。其中performMeasure()会调用measure(),measure()中又调用onMeasure(),实现对其所有子元素的measure过程,这样就完成了一次measure过程;接着子元素会重复父容器的measure过程,如此反复至完成整个View树的遍历(layout和draw同理)。

View的绘制

View的整个绘制流程可以分为以下3个阶段:

  • measure:判断是否需要重新计算view的大小,需要的话则计算
  • layoiut: 判断是否需要重新计算view的位置,需要的话则计算
  • draw: 判断是否需要重新绘制view,需要的话则重新绘制

measure阶段

此阶段的目的是计算出控件树中的各个控件要显示其内容的话,需要多少尺寸.

起点是ViewRootImpl的measureHierarchy()方法:

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res,     final int desiredWindowWidth, final int desiredWindowHeight) {  // 传入的desiredWindowXxx为窗口尺寸  int childWidthMeasureSpec;  int childHeightMeasureSpec;  boolean windowSizeMayChange = false;  . . .  boolean goodMeasure = false;  if (!goodMeasure) {    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);    if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {      windowSizeMayChange = true;    }  }  return windowSizeMayChange;}

measure源码:

/** * 调用这个方法来算出一个View应该为多大。参数为父View对其宽高的约束信息。 * 实际的测量工作在onMeasure()方法中进行 */public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  . . .   // 判断是否需要重新布局  // 若mPrivateFlags中包含PFLAG_FORCE_LAYOUT标记,则强制重新布局  // 比如调用View.requestLayout()会在mPrivateFlags中加入此标记  final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;  final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec      || heightMeasureSpec != mOldHeightMeasureSpec;  final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY      && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;  final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)      && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);  final boolean needsLayout = specChanged      && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);  // 需要重新布局    if (forceLayout || needsLayout) {    . . .    // 先尝试从缓从中获取,若forceLayout为true或是缓存中不存在或是    // 忽略缓存,则调用onMeasure()重新进行测量工作    int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);    if (cacheIndex < 0 || sIgnoreMeasureCache) {      // measure ourselves, this should set the measured dimension flag back      onMeasure(widthMeasureSpec, heightMeasureSpec);      . . .    } else {      // 缓存命中,直接从缓存中取值即可,不必再测量      long value = mMeasureCache.valueAt(cacheIndex);      // Casting a long to int drops the high 32 bits, no mask needed      setMeasuredDimensionRaw((int) (value >> 32), (int) value);      . . .    }    . . .  }  mOldWidthMeasureSpec = widthMeasureSpec;  mOldHeightMeasureSpec = heightMeasureSpec;  mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |      (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension}

从measure()方法的源码中我们可以知道,只有以下两种情况之一,才会进行实际的测量工作:

  • forceLayout为true : 这表示强制重新布局,可以通过View.requestLayout()来实现
  • needsLayout为true : 这需要specChanged为true(表示本次传入的MeasureSpec与上次传入的不同),并且以下3个条件有一个成立
  • sAlwaysRemeasureExactly为true: 该变量默认为false;
  • isSpecExactly为false: 若父View对子View提出了精确的宽高约束,则该变量为true,否则为false
  • matchesSpecSize为false: 表示父View的宽高尺寸要求与上次测量的结果不同

对于decorView来说,实际执行测量工作的是FrameLayout的onMeasure()方法,该方法的源码如下:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  int count = getChildCount();  . . .  int maxHeight = 0;  int maxWidth = 0;  int childState = 0;  for (int i = 0; i < count; i++) {    final View child = getChildAt(i);    if (mMeasureAllChildren || child.getVisibility() != GONE) {      measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);      final LayoutParams lp = (LayoutParams) child.getLayoutParams();      maxWidth = Math.max(maxWidth,          child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);      maxHeight = Math.max(maxHeight,          child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);      childState = combineMeasuredStates(childState, child.getMeasuredState());      . . .    }  }  // Account for padding too  maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();  maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();  // Check against our minimum height and width  maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());  maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());  // Check against our foreground's minimum height and width  final Drawable drawable = getForeground();  if (drawable != null) {    maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());    maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());  }  setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),        resolveSizeAndState(maxHeight, heightMeasureSpec,        childState << MEASURED_HEIGHT_STATE_SHIFT));  . . . }

View的measure过程

在单一View的测量过程中实际起主要作用的方法有两个:
getDefaultSize():获取View的实际测量宽高;
setMeasuredDimension():存储View的实际测量宽高;

ViewGroup的measure过程

ViewGroup的测量过程除了完成自身的测量之外,还会遍历去调用子View的measure()方法。ViewGroup是一个抽象类,没有重写View的onMeasure()方法,所以需要子类去实现onMeasure()方法规定具体的测量规则。

ViewGroup子类复写onMeasure()方法有3个步骤:

  • 遍历所有子View并测量其宽高 , 直接调用ViewGroup的measureChildren()方法
  • 合并计算所有子view的宽高,最终得到父view的测量宽高
  • 存储父View的宽高

ViewGroup中提供了**measureChildren()方法,该方法主要遍历所有的子View并调用其measureChild()**方法.

在Activity启动时,如何正确获取一个View的宽高:由于View的measure过程和Activity的生命周期是不同步的,所以无法保证Activity的onCreate()或者onResume()方法执行时某个View已经测量完毕,可以通过以下方法来解决:
(1)在onWindowFocusChanged()方法中获取View的宽高,该方法可能会被频繁调用;
(2)通过ViewTreeObserver的OnGlobalLayoutListener监听接口,当View树的状态发生改变或者View树内部的View的可见性发生改变时,就会回调onGlobalLayout()方法,在该方法中可以准确获取View的实际宽高.

Layout过程

View的Layout过程主要是确定View的四个顶点位置,从而确定其在容器中的位置,具体的layout过程和measure过程大致相似。

1、View的layout过程

对于单一View的layout过程,首先调用View的layout()方法,在该方法中通过setFrame()方法来设定View的四个顶点的位置,即 初始化mLeft、mTop、mRight、mBottom这四个值,View的四个顶点一旦确定,那么View在父容器的位置也就确定了。接着会调用**onLayout()**方法确定所有子View在父容器中的位置,由于是单一View的layout过程,所以 onLayout()方法为空实现,因为没有子View(如果是ViewGroup需要子类实现 onLayout()方法)。

2、ViewGroup的layout过程

ViewGroup的layout过程首先会调用自身layout()方法,但和View的layout过程不一样的是,ViewGroup需要子类实现onLayout()方法,循环遍历所有的子View并调用其layout()方法确定子View的位置,从而最终确定ViewGroup在父容器的位置。

在onLayout()中主要遍历所有子View并调用setChildFrame(),在setChildFrame()中调用子View的layout()来确定每个子View的位置,从而最终确定自身的位置。

draw过程

Draw过程主要是绘制View的过程,也分为单一View的绘制和ViewGroup的绘制。

View的draw过程都是从调用draw()方法开始的,该方法主要完成如下工作流程:

(1) drawBackground():绘制背景;

(2) 保存当前的canvas层(不是必须的);

(3) onDraw(): 绘制View的内容,这是一个空实现,需要子View根据要绘制的颜色、线条等样式去具体实现,所以要在子View里重写该方法;

(4) dispatchDraw(): 对所有子View进行绘制;单一View的dispatchDraw()方法是一个空方法,因为单一View没有子View,不需要实现dispatchDraw ()方法,而ViewGroup就不一样了,它实现了dispatchDraw()方法去遍历所有子View进行绘制;

(5) onDrawForeground():绘制装饰,比如滚动条.

1、View的draw过程

2、ViewGroup的draw过程

draw两个容易混淆的方法,两者都是刷新View的方法:

invalidate(): 不会经过measure和layout过程,只会调用draw过程;

requestLayout() :会调用measure和layout过程重新测量大小和确定位置,不会调用draw过程;

所以当我们进行View更新时,若仅View的显示内容发生改变且新显示内容不影响View的大小、位置,则只需调用invalidate方法;若View宽高、位置发生改变且显示内容不变,只需调用requestLayout方法;若两者均发生改变,则需调用两者,按照View的绘制流程,推荐先调用requestLayout方法再调用invalidate方法。

更多相关文章

  1. Android新线程中更新主线程UI中的View方法汇总
  2. Android(安卓)进程间通信 实例分析
  3. Android(安卓)ContentProvider和Uri详解
  4. Android(安卓)更新UI的两种方法——handler和runOnUiThread()
  5. Android消息处理机制
  6. android 已知资源名称获取资源ID
  7. Android调用C++实现共享内存(Native层)
  8. 浅谈Java中Collections.sort对List排序的两种方法
  9. Python list sort方法的具体使用

随机推荐

  1. Android(安卓)Nine Patch图片及按钮背景
  2. Android(安卓)Configuration change引发
  3. 再谈android studio抽取字符串方法
  4. ZRAM SWAP内存管理讲解
  5. 2018-6月Android试题整理
  6. MIPI-DSI转HDMI
  7. 漫谈Android网络编程
  8. Android(安卓)点击通知栏消息 跳转到指定
  9. Android:Android(安卓)6.0+权限适配--简单
  10. Android开发规范之编码规范