浅谈Android View绘制三大流程探索及常见问题

 更新时间:2017年03月13日 09:46:55   投稿:jingxian   我要评论
下面小编就为大家带来一篇浅谈Android View绘制三大流程探索及常见问题。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧

View绘制的三大流程,指的是measure(测量)、layout(布局)、draw(绘制)

measure负责确定View的测量宽/高,也就是该View需要占用屏幕的大小,确定完View需要占用的屏幕大小后,就会通过layout确定View的最终宽/高和四个顶点在手机界面上的位置,等通过measure和layout过程确定了View的宽高和要显示的位置后,就会执行draw绘制View的内容到手机屏幕上。

在详细介绍这三大流程之前,需要简单了解一下ViewRootImpl,View绘制的三大步骤都是通过ViewRootImpl实现的,ViewRootImpl是连接WindowManager窗口管理和DecorView顶层视图的纽带。View的绘制流程从ViewRootImpl的performTraversals方法开始,顺序执行measure、layout、draw这三个流程,最终完成对View的绘制工作,在performTraversals方法中,会调用measure、layout、draw这三个方法,这三个方法内部也会调用其对应的onMeasure、onLayout、onDraw方法,通常我们在自定义View时,也就是重写的这三个方法来实现View的具体绘制逻辑

下面详细了解下各个步骤经历的主要方法(这里贴的源码版本为API 23)

一、measure

在performTraversals方法中,第一个需要进行的就是measure过程,获取到必要信息后,performTraversals方法中首先会调用measureHierarchy方法,接着measureHierarchy方法里再去调用performMeasure方法,在performMeasure方法中最终就会去调用View的measure方法,从而开始进行测量过程

 private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
  Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
  try {
   mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  } finally {
   Trace.traceEnd(Trace.TRACE_TAG_VIEW);
  }
 }

mView其实指的就是DecorView顶层视图,从源码可以看出,measure的递归过程就是从DecorView开始的

View和ViewGroup的测量方法有一定区别,View通过measure方法就可以完成自身的测量过程,而ViewGroup不仅需要调用measure方法测量自己,还需要去遍历其子元素的measure方法,其子元素如果是ViewGroup,则该子元素需使用同样的方法再次递归下去。

View

来看看View是如何测量自己的宽高的

先在View源码中找到measure方法

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  // ......

  if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
    widthMeasureSpec != mOldWidthMeasureSpec ||
    heightMeasureSpec != mOldHeightMeasureSpec) {
   // ......
   if (cacheIndex < 0 || sIgnoreMeasureCache) {
    // measure ourselves, this should set the measured dimension flag back
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
   }
   // .....
 }

View的measure过程就是通过measure方法来完成,View中的measure方法是由ViewGroup的measureChild方法调用的,ViewGroup在调用该子View的measure方法的同时还传入了子View的widthMeasureSpec和heightMeasureSpec值。该方法被定义为final类型,也就是说其measure过程是固定的,在measure中调用了onMeasure方法,如果想要自定义测量过程的话,需要重写onMeasure方法。

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
 }

Google在介绍该方法的时候也说了

Measure the view and its content to determine the measured width and the measured height. This method is invoked by {@link #measure(int, int)} and should be overridden by subclasses to provide accurate and efficient measurement of their contents.

该方法需要被子类覆盖,让子类提供精准、有效的测量数据,所以我们一般在进行自定义View开发时,需要自定义测量过程就需要复写此方法。

setMeasuredDimension方法的作用就是设置View的测量宽高,其实我们在使用getMeasuredWidth/getMeasuredHeight 方法获取的宽高值就是此处设置的值。

如果不复写此onMeasure方法,则默认使用getDefaultSize方法得到的值。

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;
 }

可以发现,传入的measureSpec数值被MeasureSpec解析成了对应的数据,这里简单介绍下MeasureSpec,它的作用就是告诉View应该以哪一种模式测量这个View,SpecMode有三种模式:

• UNSPECIFIED:表示父容器不对View有任何限制,这种模式主要用于系统内部多次Measure的情况,不需要过多关注

• AT_MOST:父容器已经指定了大小,View的大小不能大于这个值,相当于布局中使用的wrap_content模式

• EXACTLY:表示View已经定义了精确的大小,使用这个指定的精确大小specSize作为该View的大小,相当于布局中我们指定了66dp这种精确数值或者match_parent模式

传入的measureSpec值经过MeasureSpec.getMode方法获取它的测量模式,MeasureSpec.getSize方法获取对应模式下的规格大小,从而确定了其最终的测量大小。

ViewGroup

ViewGroup是一个继承至View的抽象类,ViewGroup没有实现测量自己的具体过程,因为其过程是需要各个子类根据自己的需要再具体实现,比如LinearLayout、RelativeLayout等布局的特性都是不同的,不能统一的去管理,所以就交给其子类自己去实现

ViewGroup在measure时,除了实现自身的测量,还需要对它的每个子元素进行measure,在ViewGroup内部提供了一个measureChildren的方法

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);
   }
  }
 }

其中,mChilderenCount指的是该ViewGroup所拥有的子元素的个数,通过一个for循环调用measureChild方法来测量其所有子元素

protected void measureChild(View child, int parentWidthMeasureSpec,
   int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
    mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
    mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

该方法先通过child.getLayoutParams方法取得子元素的LayoutParams,然后调用getChildMeasureSpec方法计算出该子元素正确的MeasureSpec,再使用child.measure方法把这个MeasureSpec传递给View进行测量。

通过这一系列过程,就能让各个子元素依次进入measure了

二、layout

通过之前的measure过程,View已经测量出了自己需要的宽高大小,performTraversals方法接下来就会执行layout过程

host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

layout的过程主要是用来确定View的四个顶点所在屏幕上的位置

layout过程首先从View中的layout方法开始

public void layout(int l, int t, int r, int b) {
  if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
   onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
   mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
  }

  int oldL = mLeft;
  int oldT = mTop;
  int oldB = mBottom;
  int oldR = mRight;

boolean changed = isLayoutModeOptical(mParent) ?
    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
   onLayout(changed, l, t, r, b);
   mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

   ListenerInfo li = mListenerInfo;
   if (li != null && li.mOnLayoutChangeListeners != null) {
    ArrayList<OnLayoutChangeListener> listenersCopy =
      (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
    int numListeners = listenersCopy.size();
    for (int i = 0; i < numListeners; ++i) {
     listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
    }
   }
  }

  mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
  mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

layout(int l, int t, int r, int b)方法里的四个参数分别指的是左、上、右、下的位置,这四个值是通过ViewRootImpl类里的performTraversals方法传入的

layout方法用来确定View自身的位置,mLeft、mTop、mBottom、mRight的值最终会由setOpticalFrame和setFrame方法确定,其实setOpticalFrame内部最后也是通过调用setFrame方法设置的

 private boolean setOpticalFrame(int left, int top, int right, int bottom) {
  Insets parentInsets = mParent instanceof View ?
    ((View) mParent).getOpticalInsets() : Insets.NONE;
  Insets childInsets = getOpticalInsets();
  return setFrame(
    left + parentInsets.left - childInsets.left,
    top + parentInsets.top - childInsets.top,
    right + parentInsets.left + childInsets.right,
    bottom + parentInsets.top + childInsets.bottom);
 }

确定完View的四个顶点位置后,就相当于View在父容器中的位置被确定了,接下来会调用onLayout方法,这个方法是没有具体实现的

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
 }

和ViewGroup的onMeasure类似,onLayout方法的具体实现也是需要根据各个View或ViewGroup的特性来决定的,所以源码中是个空方法,有兴趣的可以去看看LinearLayout、RelativeLayout等实现了onLayout方法的ViewGroup子类

之前的measure过程,得到的是测量宽高,而通过onLayout方法,进一步确定了View的最终宽高,一般情况下,measure过程的测量宽高和layout过程确定的最终宽高是一样的

三、draw

经过以上步骤,View已经确定好了大小和屏幕中显示的位置,接着就可以绘制自身需要显示的内容了

在performTraversals方法中,会调用performDraw方法,performDraw方法中调用draw方法,draw方法中接着调用drawSoftware方法

 private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
   boolean scalingRequired, Rect dirty) {

  // Draw with software renderer.
  final Canvas canvas;
  try {
   ......
   canvas = mSurface.lockCanvas(dirty);
  } 

  try {
   ......
   try {
    mView.draw(canvas);
   } 
  } 
 }

首先会通过lockCanvas方法取得一个Canvas画布对象,接着由mView(DecorView)顶层视图去调用View的draw方法,并传入一个Canvas画布对象

其实Google的工程师已经把draw的绘制过程注释的非常详细了

Draw traversal performs several drawing steps which must be executed

in the appropriate order:

1. Draw the background

2. If necessary, save the canvas' layers to prepare for fading

3. Draw view's content

4. Draw children

5. If necessary, draw the fading edges and restore layers

6. Draw decorations (scrollbars for instance)

1. 绘制View的背景

如果该View设置了背景,则绘制背景。此背景指的是我们在布局文件中通过android:background属性,或代码中使用setBackgroundResource、setBackgroundColor等方法设置的背景图片或背景颜色

  if (!dirtyOpaque) {
   drawBackground(canvas);
  }

dirtyOpaque属性用来判断该View是否是透明的,如果是透明的则不执行某些步骤,比如绘制背景,绘制内容等

2. 如果有必要的话,保存这个canvas画布,为该层边缘的fading效果作准备

第2步和第5步是配套的,我们一般不用管2和5,源码中的注释也说了,其中的2和5方法在通常情况下是直接跳过的(skip step 2 & 5 if possible (common case)),其主要作用是实现一些如同View滑动到边缘时产生的阴影效果,可以不用过多关注

3. 绘制View的内容

该步骤调用了onDraw方法,这个方法是一个空实现

 /**
  * Implement this to do your drawing.
  *
  * @param canvas the canvas on which the background will be drawn
  */
 protected void onDraw(Canvas canvas) {
 }

每个子View需要展示的内容肯定是不相同的,所以onDraw的详细过程需要子类自己去实现

4. 绘制子View

和第3步一样,此方法也是一个空实现

 /**
  * Called by draw to draw the child views. This may be overridden
  * by derived classes to gain control just before its children are drawn
  * (but after its own view has been drawn).
  * @param canvas the canvas on which to draw the view
  */
 protected void dispatchDraw(Canvas canvas) {

 }

对于单纯的View来说,它是没有子View的,所以不需要实现该方法,该方法主要是被ViewGroup重写了,找到ViewGroup中重写的dispatchDraw方法

 @Override
 protected void dispatchDraw(Canvas canvas) {
  ......
  for (int i = 0; i < childrenCount; i++) {
   while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
    final View transientChild = mTransientViews.get(transientIndex);
    if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
      transientChild.getAnimation() != null) {
     more |= drawChild(canvas, transientChild, drawingTime);
    }
    transientIndex++;
    if (transientIndex >= transientCount) {
     transientIndex = -1;
    }
   }
   int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
   final View child = (preorderedList == null)
     ? children[childIndex] : preorderedList.get(childIndex);
   if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
    more |= drawChild(canvas, child, drawingTime);
   }
  }
  ......
 }

在ViewGroup的dispatchDraw方法中通过for循环调用drawChild方法

 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
  return child.draw(canvas, this, drawingTime);
 }

drawChild方法里调用子视图的draw方法,从而让其子视图进入draw过程

5. 绘制View边缘的渐变褪色效果,类似于阴影效果

当第2个步骤保存了canvas画布后,就可以为这个画布实现阴影效果

6. 绘制View的装饰物

View的装饰物,指的是View除了背景、内容、子View的其它部分,比如滚动条这些

四、常见问题

1.在Activity中获取View的宽高,得到的值为0

通过上面的measure分析可以知道,View的measure过程和Activity的生命周期方法不是同步的,所以无法保证Activity的某个生命周期执行后View就一定能获取到值,当我们在View还没有完成measure过程就去获取它的宽高,当然获取不到了,解决这问题的方法有很多,这里推荐使用以下方法

(1)在View的post方法中获取:

这个方法简单快捷,推荐使用

  mView.post(new Runnable() {
   @Override
   public void run() {
    width = mView.getMeasuredWidth();
    height = mView.getMeasuredHeight();
   }
  });

post方法中传入的Runnable对象将会在View的measure、layout过程后触发,因为UI的事件队列是按顺序执行的,所以任何post到队列中的请求都会在Layout发生变化后执行。

(2)使用View的观察者ViewTreeObserver

ViewTreeObserver是视图树的观察者,其中OnGlobalLayoutListener监听的是一个视图树中布局发生改变或某个视图的可视状态发生改变时,就会触发此类监听事件,其中onGlobalLayout回调方法会在View完成layout过程后调用,此时是获取View宽高的好时机

 ViewTreeObserver observer = mView.getViewTreeObserver();
  observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
   @Override
   public void onGlobalLayout() {
    mView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
    width = mScanIv.getMeasuredWidth();
    height = mScanIv.getMeasuredHeight();
   }
  });

使用这个方法需要注意,随着View树的状态改变,onGlobalLayout方法会被回调多次,所以在进入onGlobalLayout回调方法时,就移除这个观察者,保证onGlobalLayout方法只被执行一次就好了

(3)在onWindowFocusChanged回调中获取

此方法是在View已经初始化完成,measure和layout过程已经执行完成,UI视图已经渲染完成时被回调,此时View的宽高肯定也已经被确定了,这个时候就可以去获取View的宽高了

 @Override
 public void onWindowFocusChanged(boolean hasFocus) {
  super.onWindowFocusChanged(hasFocus);
  if (hasFocus) {
   width = mView.getMeasuredWidth();
   height = mView.getMeasuredHeight();
  }
 }

这个方法在Activity界面发生变化时也会被多次回调,如果只需要获取一次宽高的话,建议加上标记加以限制

除了以上方法,还有其它的方法也能获取到宽高,比如在onClick方法中获取,手动调用measure方法,使用postDelayed等,了解了View绘制原理后,这些都是很容易就能理解的。

以上这篇浅谈Android View绘制三大流程探索及常见问题就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

最新评论