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閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容

  • View的繪制和事件處理是兩個重要的主題,上一篇《圖解 Android事件分發機制》已經把事件的分發機制講得比較詳...
    Kelin閱讀 120,059評論 100 845
  • DecorView 在了解view的繪制流程之前,首先我們要知道一個DecorView的概念,什么是DecorVi...
    Cris_Ma閱讀 6,178評論 0 8
  • 1.前&ensp言 對View的繪制流程設計思想,運用場景來次梳理。以前學只是套公式用,比較淺。 2.ViewRo...
    xwp閱讀 716評論 0 1
  • Android的UI管理系統層級關系 PhoneWindow是Adroid系統中最基本的窗口系統,每個Activi...
    凱玲之戀閱讀 1,896評論 0 2
  • 概述 本篇文章會從源碼(基于Android 6.0)角度分析Android中View的繪制流程,側重于對整體流程的...
    absfree閱讀 77,064評論 24 273