Android View的绘制过程复习

Android View的绘制过程

Android View的绘制过程复习_第1张图片

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()方法.

@Override
public 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,需要的话则重新绘制

Android View的绘制过程复习_第2张图片

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()方法,该方法的源码如下:

@Override
protected 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过程

Android View的绘制过程复习_第3张图片

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

ViewGroup的measure过程

Android View的绘制过程复习_第4张图片

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过程

Android View的绘制过程复习_第5张图片

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

2、ViewGroup的layout过程

Android View的绘制过程复习_第6张图片

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过程

Android View的绘制过程复习_第7张图片

2、ViewGroup的draw过程

Android View的绘制过程复习_第8张图片

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

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

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

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

你可能感兴趣的:(android,android,view)