溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

淺談Android View繪制三大流程探索及常見問題

發(fā)布時間:2020-10-23 12:23:11 來源:腳本之家 閱讀:176 作者:jingxian 欄目:移動開發(fā)

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

measure負責確定View的測量寬/高,也就是該View需要占用屏幕的大小,確定完View需要占用的屏幕大小后,就會通過layout確定View的最終寬/高和四個頂點在手機界面上的位置,等通過measure和layout過程確定了View的寬高和要顯示的位置后,就會執(zhí)行draw繪制View的內(nèi)容到手機屏幕上。

在詳細介紹這三大流程之前,需要簡單了解一下ViewRootImpl,View繪制的三大步驟都是通過ViewRootImpl實現(xiàn)的,ViewRootImpl是連接WindowManager窗口管理和DecorView頂層視圖的紐帶。View的繪制流程從ViewRootImpl的performTraversals方法開始,順序執(zhí)行measure、layout、draw這三個流程,最終完成對View的繪制工作,在performTraversals方法中,會調(diào)用measure、layout、draw這三個方法,這三個方法內(nèi)部也會調(diào)用其對應(yīng)的onMeasure、onLayout、onDraw方法,通常我們在自定義View時,也就是重寫的這三個方法來實現(xiàn)View的具體繪制邏輯

下面詳細了解下各個步驟經(jīng)歷的主要方法(這里貼的源碼版本為API 23)

一、measure

在performTraversals方法中,第一個需要進行的就是measure過程,獲取到必要信息后,performTraversals方法中首先會調(diào)用measureHierarchy方法,接著measureHierarchy方法里再去調(diào)用performMeasure方法,在performMeasure方法中最終就會去調(diào)用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的測量方法有一定區(qū)別,View通過measure方法就可以完成自身的測量過程,而ViewGroup不僅需要調(diào)用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方法調(diào)用的,ViewGroup在調(diào)用該子View的measure方法的同時還傳入了子View的widthMeasureSpec和heightMeasureSpec值。該方法被定義為final類型,也就是說其measure過程是固定的,在measure中調(diào)用了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.

該方法需要被子類覆蓋,讓子類提供精準、有效的測量數(shù)據(jù),所以我們一般在進行自定義View開發(fā)時,需要自定義測量過程就需要復寫此方法。

setMeasuredDimension方法的作用就是設(shè)置View的測量寬高,其實我們在使用getMeasuredWidth/getMeasuredHeight 方法獲取的寬高值就是此處設(shè)置的值。

如果不復寫此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;
 }

可以發(fā)現(xiàn),傳入的measureSpec數(shù)值被MeasureSpec解析成了對應(yīng)的數(shù)據(jù),這里簡單介紹下MeasureSpec,它的作用就是告訴View應(yīng)該以哪一種模式測量這個View,SpecMode有三種模式:

• UNSPECIFIED:表示父容器不對View有任何限制,這種模式主要用于系統(tǒng)內(nèi)部多次Measure的情況,不需要過多關(guān)注

• AT_MOST:父容器已經(jīng)指定了大小,View的大小不能大于這個值,相當于布局中使用的wrap_content模式

• EXACTLY:表示View已經(jīng)定義了精確的大小,使用這個指定的精確大小specSize作為該View的大小,相當于布局中我們指定了66dp這種精確數(shù)值或者match_parent模式

傳入的measureSpec值經(jīng)過MeasureSpec.getMode方法獲取它的測量模式,MeasureSpec.getSize方法獲取對應(yīng)模式下的規(guī)格大小,從而確定了其最終的測量大小。

ViewGroup

ViewGroup是一個繼承至View的抽象類,ViewGroup沒有實現(xiàn)測量自己的具體過程,因為其過程是需要各個子類根據(jù)自己的需要再具體實現(xiàn),比如LinearLayout、RelativeLayout等布局的特性都是不同的,不能統(tǒng)一的去管理,所以就交給其子類自己去實現(xiàn)

ViewGroup在measure時,除了實現(xiàn)自身的測量,還需要對它的每個子元素進行measure,在ViewGroup內(nèi)部提供了一個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所擁有的子元素的個數(shù),通過一個for循環(huán)調(diào)用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,然后調(diào)用getChildMeasureSpec方法計算出該子元素正確的MeasureSpec,再使用child.measure方法把這個MeasureSpec傳遞給View進行測量。

通過這一系列過程,就能讓各個子元素依次進入measure了

二、layout

通過之前的measure過程,View已經(jīng)測量出了自己需要的寬高大小,performTraversals方法接下來就會執(zhí)行l(wèi)ayout過程

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)方法里的四個參數(shù)分別指的是左、上、右、下的位置,這四個值是通過ViewRootImpl類里的performTraversals方法傳入的

layout方法用來確定View自身的位置,mLeft、mTop、mBottom、mRight的值最終會由setOpticalFrame和setFrame方法確定,其實setOpticalFrame內(nèi)部最后也是通過調(diào)用setFrame方法設(shè)置的

 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在父容器中的位置被確定了,接下來會調(diào)用onLayout方法,這個方法是沒有具體實現(xiàn)的

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

和ViewGroup的onMeasure類似,onLayout方法的具體實現(xiàn)也是需要根據(jù)各個View或ViewGroup的特性來決定的,所以源碼中是個空方法,有興趣的可以去看看LinearLayout、RelativeLayout等實現(xiàn)了onLayout方法的ViewGroup子類

之前的measure過程,得到的是測量寬高,而通過onLayout方法,進一步確定了View的最終寬高,一般情況下,measure過程的測量寬高和layout過程確定的最終寬高是一樣的

三、draw

經(jīng)過以上步驟,View已經(jīng)確定好了大小和屏幕中顯示的位置,接著就可以繪制自身需要顯示的內(nèi)容了

在performTraversals方法中,會調(diào)用performDraw方法,performDraw方法中調(diào)用draw方法,draw方法中接著調(diào)用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)頂層視圖去調(diào)用View的draw方法,并傳入一個Canvas畫布對象

其實Google的工程師已經(jīng)把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設(shè)置了背景,則繪制背景。此背景指的是我們在布局文件中通過android:background屬性,或代碼中使用setBackgroundResource、setBackgroundColor等方法設(shè)置的背景圖片或背景顏色

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

dirtyOpaque屬性用來判斷該View是否是透明的,如果是透明的則不執(zhí)行某些步驟,比如繪制背景,繪制內(nèi)容等

2. 如果有必要的話,保存這個canvas畫布,為該層邊緣的fading效果作準備

第2步和第5步是配套的,我們一般不用管2和5,源碼中的注釋也說了,其中的2和5方法在通常情況下是直接跳過的(skip step 2 & 5 if possible (common case)),其主要作用是實現(xiàn)一些如同View滑動到邊緣時產(chǎn)生的陰影效果,可以不用過多關(guān)注

3. 繪制View的內(nèi)容

該步驟調(diào)用了onDraw方法,這個方法是一個空實現(xiàn)

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

每個子View需要展示的內(nèi)容肯定是不相同的,所以onDraw的詳細過程需要子類自己去實現(xiàn)

4. 繪制子View

和第3步一樣,此方法也是一個空實現(xiàn)

 /**
  * 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的,所以不需要實現(xiàn)該方法,該方法主要是被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循環(huán)調(diào)用drawChild方法

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

drawChild方法里調(diào)用子視圖的draw方法,從而讓其子視圖進入draw過程

5. 繪制View邊緣的漸變褪色效果,類似于陰影效果

當?shù)?個步驟保存了canvas畫布后,就可以為這個畫布實現(xiàn)陰影效果

6. 繪制View的裝飾物

View的裝飾物,指的是View除了背景、內(nèi)容、子View的其它部分,比如滾動條這些

四、常見問題

1.在Activity中獲取View的寬高,得到的值為0

通過上面的measure分析可以知道,View的measure過程和Activity的生命周期方法不是同步的,所以無法保證Activity的某個生命周期執(zhí)行后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過程后觸發(fā),因為UI的事件隊列是按順序執(zhí)行的,所以任何post到隊列中的請求都會在Layout發(fā)生變化后執(zhí)行。

(2)使用View的觀察者ViewTreeObserver

ViewTreeObserver是視圖樹的觀察者,其中OnGlobalLayoutListener監(jiān)聽的是一個視圖樹中布局發(fā)生改變或某個視圖的可視狀態(tài)發(fā)生改變時,就會觸發(fā)此類監(jiān)聽事件,其中onGlobalLayout回調(diào)方法會在View完成layout過程后調(diào)用,此時是獲取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樹的狀態(tài)改變,onGlobalLayout方法會被回調(diào)多次,所以在進入onGlobalLayout回調(diào)方法時,就移除這個觀察者,保證onGlobalLayout方法只被執(zhí)行一次就好了

(3)在onWindowFocusChanged回調(diào)中獲取

此方法是在View已經(jīng)初始化完成,measure和layout過程已經(jīng)執(zhí)行完成,UI視圖已經(jīng)渲染完成時被回調(diào),此時View的寬高肯定也已經(jīng)被確定了,這個時候就可以去獲取View的寬高了

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

這個方法在Activity界面發(fā)生變化時也會被多次回調(diào),如果只需要獲取一次寬高的話,建議加上標記加以限制

除了以上方法,還有其它的方法也能獲取到寬高,比如在onClick方法中獲取,手動調(diào)用measure方法,使用postDelayed等,了解了View繪制原理后,這些都是很容易就能理解的。

以上這篇淺談Android View繪制三大流程探索及常見問題就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持億速云。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI