View的绘制流程

View的绘制流程主要分为散步,measure,layout以及draw,接下来,我们就从源码角度分析这个步骤。

测量(measure)

测量是绘制的第一步,用来决定View或者ViewGroup的测量宽高,这里讲View和ViewGroup分别讨论下

1.View的测量过程

概述:View的测量过程相对简单,直接可以通过onMeasure方法完成测量工作。

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

首先通过getSuggestedMinimumHeight获取View的最小高度(宽度),如果没设置背景则返回minHeight,否则返回背景的原始高度和minHeight的较大值

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

其次,计算出View的测量宽度

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

2.ViewGroup的测量过程

概述:ViewGroup的测量工作分为两步,完成自己的测量工作,遍历子View并完成他们的测量工作,因此对应的方法有两个onMeasuremeasureChildren

measureChildren

遍历测量每一个子View

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];        
    //View的状态不能是GONE
    if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {            
      measureChild(child, widthMeasureSpec, heightMeasureSpec);        
    }
  }
}

每一个子View调用自己的measure方法

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

}

onMeasure

测量自己的过程需要根据不同的ViewGroup自己实现,相对简单的ViewGroup有FrameLayout和LinearLayout,但是FrameLayout的重叠属性导致分析起来不太清晰,这里拿LinearLayout举个例子。

区分LinearLayout的布局类型

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {   
  if (mOrientation == VERTICAL) { //垂直布局       
    measureVertical(widthMeasureSpec, heightMeasureSpec);    
  } else { //水平布局
    measureHorizontal(widthMeasureSpec, heightMeasureSpec);    
  }
}

拿垂直方向距离,计算高度和宽度

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {    
  final int count = getVirtualChildCount();
  final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
  final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
  for (int i = 0; i < count; ++i) {    
    final View child = getVirtualChildAt(i);    
    //View为null则直接跳过    
    if (child == null) {    
      mTotalLength += measureNullChild(i);        
      continue;    
    }
     //View为GONE也直接跳过 
    if (child.getVisibility() == View.GONE) { 
      i += getChildrenSkipCount(child, i);   
      continue;
    }
    //将divider的高度算到最终高度里
    if (hasDividerBeforeChildAt(i)) {    
      mTotalLength += mDividerHeight;
    }
    //如果通过权重来分配空间,则根据权重计算mTotalLength
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    totalWeight += lp.weight;
    if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {   
      ...
    } else {
      measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight);
      final int childHeight = child.getMeasuredHeight();        
      final int totalLength = mTotalLength;
      mTotalLength = Math.max(totalLength, totalLength + childHeight + 
        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
    }
    // Add in our padding
    mTotalLength += mPaddingTop + mPaddingBottom;
    int heightSize = mTotalLength;
    // Check against our minimum height
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

  //计算LinearLayout的宽度
  ...
  setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), 
    heightSizeAndState);
}

measure过程完成后,我们就可以获取到View的测量宽高,但是,这还不是View的最终宽高,最终宽高是在下面layout中完成的。

布局(layout)

布局用来确定View或者ViewGroup的位置,如果是ViewGroup的话,与measure一样会遍历所有的子元素并调用layout方法。

1.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) {    
    //调用View的onLayout方法将View放置到指定位置。
    onLayout(changed, l, t, r, b);    
    mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;    
    ListenerInfo li = mListenerInfo;    
    //执行onLayoutChangeListener
    if (li != null && li.mOnLayoutChangeListeners != null) {        
      ...   
      for (int i = 0; i < numListeners; ++i) {            
        listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);        
      }    
    }
  }
}

获取四个顶点的位置

protected boolean setFrame(int left, int top, int right, int bottom) {      
 ...
 if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {             
    ...    
    //计算新的宽高
    int oldWidth = mRight - mLeft;        
    int oldHeight = mBottom - mTop;        
    int newWidth = right - left;        
    int newHeight = bottom - top;    
    //View的大小是否变化    
    boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
    //刷新
    invalidate(sizeChanged);
    //设置新的四个顶点的位置
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;
    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
    ...
}

2.ViewGroup的layout过程

这里依然以LinearLayout为例,首先区分线性布局类型

protected void onLayout(boolean changed, int l, int t, int r, int b) {    
  if (mOrientation == VERTICAL) {        
    layoutVertical(l, t, r, b);    
  } else {        
    layoutHorizontal(l, t, r, b);    
  }
}

计算自身的位置以及遍历子View并计算他们的位置

void layoutVertical(int left, int top, int right, int bottom) {    
  final int paddingLeft = mPaddingLeft;    
  int childTop;    
  int childLeft;        
  // 计算View右侧的结束位置
  final int width = right - left;    
  int childRight = width - mPaddingRight;        
  // 计算留给子View的剩余空间
  int childSpace = width - paddingLeft - mPaddingRight;
  //计算子View个数
  final int count = getVirtualChildCount();
  //根据Gravity计算childTop
  ...
  for (int i = 0; i < count; i++) {    
    final View child = getVirtualChildAt(i);    
    if (child == null) {        
      childTop += measureNullChild(i);    
    } else if (child.getVisibility() != GONE) {
      //计算子View的测量宽高
      final int childWidth = child.getMeasuredWidth();
      final int childHeight = child.getMeasuredHeight();
      final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
      //计算childLeft
      ...
      //如果有divider,那么childTop要加上它。
      if (hasDividerBeforeChildAt(i)) {    
        childTop += mDividerHeight;
      }

      childTop += lp.topMargin;
      //调用子View的layout方法。
      setChildFrame(child, childLeft, childTop + getLocationOffset(child),        childWidth, childHeight);    
      childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
    }
}

绘制(draw)

绘制过程比较简单,主要6步

public void draw(Canvas canvas) {
  ...
  //画背景
  if (!dirtyOpaque) {    
    drawBackground(canvas);
  }
  //绘制自己的内容
  if (!dirtyOpaque) onDraw(canvas);
  //绘制子View(如果有的话。)
  dispatchDraw(canvas);
  ...
  //绘制前景和滚动条
  onDrawForeground(canvas);

}

获取View宽高的方法

1.onWindowFocusChanged

该方法的注释:该方法在得到或者失去焦点时调用,这是确定activity是否对用户可见的指示器。不过该方法随着focus的得到和失去会多次调用。

2.ViewTreeObserver

可以通过ViewTreeObserver的接口回调完成View宽高的获取

3.View.post

通过post将一个runnable扔到消息队列的末尾,然后调用

测量宽高和真实宽高的区别

测量宽高和真实宽高基本一样,只不过测量宽高是在measure时期确定的,而真实宽高是在layout时期决定的,两者的确定时期不同。日常开发过程中,我们可以认为是测量宽高就是真实宽高。但不绝对,我们可以在View的Layout方法中手动修改left,top,right,bottom的值导致他们不一样。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • View的绘制和事件处理是两个重要的主题,上一篇《图解 Android事件分发机制》已经把事件的分发机制讲得比较详...
    Kelin阅读 118,326评论 100 844
  • DecorView 在了解view的绘制流程之前,首先我们要知道一个DecorView的概念,什么是DecorVi...
    Cris_Ma阅读 6,100评论 0 8
  • 1.前&ensp言 对View的绘制流程设计思想,运用场景来次梳理。以前学只是套公式用,比较浅。 2.ViewRo...
    xwp阅读 672评论 0 1
  • Android的UI管理系统层级关系 PhoneWindow是Adroid系统中最基本的窗口系统,每个Activi...
    凯玲之恋阅读 1,814评论 0 2
  • 概述 本篇文章会从源码(基于Android 6.0)角度分析Android中View的绘制流程,侧重于对整体流程的...
    absfree阅读 76,247评论 24 272