《Android 開發(fā)藝術(shù)探索》筆記5--View工作原理

View工作原理.png

ViewRoot和DecorView

這是在View三大流程之前(measure, layout, draw),需要了解的概念.

ViewRoot對應(yīng)于ViewRootImpl, 它是連接WindowManagerDecorView的紐帶. View的三大流程都是通過ViewRoot來完成的. 當(dāng)一個Activity對象在ActivityThread被創(chuàng)建后. 會將DecorView添加到Window中, 同時會創(chuàng)建ViewRootImp對象, 并將ViewRootImpl對象和DecorView建立關(guān)聯(lián).

View繪制流程是從ViewRoot的PerformTraversals()開始的. 經(jīng)過三大流程才能將一個View繪制出來.

PerformTraversals()會依次調(diào)用performMeasure, performLayout, performDraw. 而前兩種內(nèi)部的調(diào)用基本一致,都是先調(diào)用measure()/layout(),然后再調(diào)用onMeasure()/onLayout()在這個方法中會對所有子元素進(jìn)行測量和繪制.依次向內(nèi)部傳遞. performDraw()有點(diǎn)不同是在draw調(diào)用的dispatchDraw().

  • measure過程: 決定了View寬高, measure后可以通過getMeasureWidth和getMeasureHeight來獲取View的寬高. 一般情況下是最終寬高.
  • layout過程: 決定了View的頂點(diǎn)坐標(biāo)和實際View的寬高. 完成后通過getTop, getBottom, getLeft, getRight獲得四個頂點(diǎn), 通過getWidth,和getHeight獲得寬高
  • draw過程: 只有draw()方法完成之后View的內(nèi)容才會顯示出來.

setContentView(R.layout.activity_inside_intercept);

((ViewGroup) getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);

上面第一行可以說無時無刻不存在. 而下面這行在上一章說過就是獲得我們設(shè)置的布局.那DecorView布局究竟是怎么樣的, 下圖.

image

DecorView就是一個FrameLayout. 而一般情況下它的布局就如上面圖那樣(具體和主題有關(guān)系). 而我們經(jīng)常setContentView(xxx). 就是把我們編寫的xml的布局添加到了DecorViewandroid.R.id.content的控件布局中. 所以也就能說通為什么getChildAt(0)會獲得我們的的布局.
并且為什么我們用的關(guān)聯(lián)布局的方法是setContent…

總結(jié)圖:

measure流程.png

MeasureSpec

很大程度上決定一個View的尺寸規(guī)格, 之所以不是絕對, 是因為這個過程還受父容器的影響.

理解MeasureSpec

MeasureSpec本身是一個32位的int值, 但是卻表示了兩種信息.

  • 高2位: 代表了SpecMode, 測量模式
  • 低30位: 代表了SpecSize, 在上述測量模式中的大小

public static class MeasureSpec {

    private static final int MODE_SHIFT = 30;

    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    public static final int EXACTLY     = 1 << MODE_SHIFT;

    public static final int AT_MOST     = 2 << MODE_SHIFT;

    public static int makeMeasureSpec(int size, int mode) {

      if (sUseBrokenMakeMeasureSpec) {

          return size + mode;

      } else {

          return (size & ~MODE_MASK) | (mode & MODE_MASK);

      }

    }

    public static int makeSafeMeasureSpec(int size, int mode) {

      if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {

          return 0;

      }

      return makeMeasureSpec(size, mode);

    }

    public static int getMode(int measureSpec) {

      return (measureSpec & MODE_MASK);

    }

    public static int getSize(int measureSpec) {

      return (measureSpec & ~MODE_MASK);

   }

   .....

}

是不是挺有意思. 三種類型分別高二位01, 00, 10來代表. 直接利用位運(yùn)算. 來實現(xiàn)可以讓頻繁計算的東西使用最接近計算機(jī)的運(yùn)算方式. 不需要額外的轉(zhuǎn)換. 也避免了過多的對象內(nèi)存分配.

說一下SpecMode的三種模式

  • UNSPECIFIED: 父容器不對View有任何的限制,要多大就給多大, 這種情況一般用于系統(tǒng)內(nèi)部,表示一中測量狀態(tài)
  • EXACTLY: 父容器已經(jīng)檢測出View所需要的精確大小, 這個時候View的最終大小就是SpecSize所指定的值. 對應(yīng)著LayoutParams中的match_parent和具體的數(shù)值.
  • AT_MOST: 父容器制定了一個可用的大小及SpecSize, View的大小不能超過這個值, 它對應(yīng)與LayoutParams中的wrap_content
image.png

MeasureSpec和LayoutParams關(guān)系

通常設(shè)置的LayoutParams,系統(tǒng)會在父容器的的約束下轉(zhuǎn)換成對應(yīng)的MeasureSpec,然后根據(jù)這個MeasureSpec來確定View測量后的寬高. 所以View自身的MeasureSpec是需要LayoutParams和父容器一起組合生成的.

上面講述的是普通View, 但是頂級View(DecorView)有所不同. DecorView是物理窗口尺寸和自身的LayoutParams決定的. 具體在ViewRootImpl類measureHierarchy()進(jìn)行生成的.

MeasureSpec一旦確定, onMeasure中就可以測量View的寬高.

對于我們?nèi)粘2僮鞯腣iew

View的measure過程是由ViewGroup傳遞而來的. 看ViewGroup#measureChildWithMargins()方法

protected void measureChildWithMargins(View child,

          int parentWidthMeasureSpec, int widthUsed,

          int parentHeightMeasureSpec, int heightUsed) {

      final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

      final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,

              mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin

                      + widthUsed, lp.width);

      final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,

              mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin

                      + heightUsed, lp.height);

      child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

  }

上面會對子元素進(jìn)行measure, 而在此之前,會通過getChildMeasureSpec()來得到子元素的MeasureSpec. 通過調(diào)用方法傳入的參數(shù)看到. 生成View的MeasureSpec和父容器的MeasureSpec, View自身方向的padding``margin, 和自身的LayoutParams這三個因素相關(guān)聯(lián).

而其中的getChildmeasureSpec()方法: 就是根據(jù)父容器的MeasureSpec同時結(jié)合View自身的LayoutParams來確定子元素的MeasureSpec.這個方法總結(jié)如下:

  • dp/px: 不管父容器的MeasureSpec是什么. View都是EXACTLY(精確模式), 而大小遵循自身LayoutParams的大小.
  • match_parent: 如果父容器是EXACTLY(精確模式),那么子View也是EXACTLY(精確模式)并且大小是父容器的剩余空間. 如果父容器是AT_MOST(最大模式),那么子View也是AT_MOST(最大模式)并且大小不會超過父容器的剩余空間.
  • wrap_content: 不管父容器是什么. View都是AT_MOST(最大模式), 并且大小不能超過父容器剩余空間.

上述沒有說明UNSPECIFIEDmatch_parentwrap_content中. 因為這個模式主要用于系統(tǒng)多次Measure的情形,一般來說不需要關(guān)注.

View的工作流程

主要指measure, layout, draw三大流程. 即測量,布局,繪制.

measure過程

這里面存在兩種場景:

  • View: 通過了measure方法就完成了測量過程
  • ViewGroup: 除了測量自己,還會遍歷去調(diào)用所有子元素的measure方法. 各個子元素在遞歸去執(zhí)行這個流程

View的measure過程

View的measure過程由其measure()方法來完成, measure()方法是一個final類型, 而在內(nèi)部調(diào)用了onMeasure()這個可不是final, 所以也可以自定義的時候復(fù)寫. 看一下內(nèi)部.

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

   setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

           getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}

setMeasureDimension()會設(shè)置View寬高的測量值.

這里需要看一下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;

}

看到如果這個view是EXACTLY(精準(zhǔn)模式), 那么返回的大小就是SpecSize. UNSPECIFIED一般用于系統(tǒng)測量先不說. 而AT_MOST(最大模式)的時候. 雖然是不同模式但是默認(rèn)情況下和精確模式是一樣的結(jié)果.

getSuggestedMinimumWidth()getSuggestedMinimumHeight(). 看一下實現(xiàn).

protected int getSuggestedMinimumWidth() {

    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());

}

protected int getSuggestedMinimumHeight() {

    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

}

首先會看是否設(shè)置了背景.

  • 無背景: 那么寬度為mMinWidth,這個值對應(yīng)布局中的android:minWidth屬性,默認(rèn)為0.
  • 有背景: 那么取mMinWidthmBackground.getMinimumHeight()最大值.

getMinimumHeight()根據(jù)看一下:


public int getMinimumHeight() {

   final int intrinsicHeight = getIntrinsicHeight();

   return intrinsicHeight > 0 ? intrinsicHeight : 0;

}

原來getMinimumHeight()返回的就是Drawable的原始高度. 如果沒有就返回0. 關(guān)于原始高度舉個例子ShapeDrawable無原始寬高, BitmapDrawble有原始寬高就是圖片的尺寸.

整理getDefaultSize(): 直接繼承View的自定義控件需要重寫onMeasure()方法并設(shè)置wrap_content時的自身大小,否則在布局中使用wrap_content雖然View自身的MeasureSpec的低30位保存了父容器計算自身的剩余大小. 但是在自定義的時候如果不進(jìn)行處理wrap_content,那么就會調(diào)用默認(rèn)setMeasureDimension()方法. 而默認(rèn)中方法的實參傳遞的是getDefaultSize()這個方法中對AT_MOST這種模式?jīng)]有處理. 直接沿用和精確模式的大小(相當(dāng)于設(shè)置了wrap_content卻得到了match_parent的顯示結(jié)果)

可以針對這個問題, 做出對應(yīng)的編碼進(jìn)行解決:


@Override

   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

       super.onMeasure(widthMeasureSpec, heightMeasureSpec);

       int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);

       int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);

       int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);

       int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);

       //設(shè)置兩個默認(rèn)值寬高

       int defaultHeight = 100;

       int defaultWidth = 100;

       // 針對AT_MOST模式進(jìn)行特殊處理

       if (widthSpaceMode == MeasureSpec.AT_MOST 

               && heightSpaceMode == MeasureSpec.AT_MOST){

           setMeasuredDimension(defaultWidth, defaultHeight);

       }else if (widthSpaceMode == MeasureSpec.AT_MOST){

           setMeasuredDimension(defaultWidth, heightSpaceSize);

       }else if (heightSpaceMode == MeasureSpec.AT_MOST){

           setMeasuredDimension(widthMeasureSpec, defaultHeight);

       }

   }

ViewGroup的Measure

對于ViewGroup不光會測量自己,還會遍歷調(diào)用所有的子元素的measure(). 和View不同的是ViewGroup是一個抽象類,它沒有重寫onMeasure,但提供了measureChildren()的方法.

這個measureChildren()方法內(nèi)部比較簡單就是遍歷自己的孩子然后調(diào)用->measureChild()

這個measureChild()這個方法前面貼過源碼. 就是取出子元素的LayoutParams,并調(diào)用->getChildMeasureSpec(). 通過傳入子元素的LayoutParams里面的寬高屬性, 子元素的padding和margin, 父元素當(dāng)前(當(dāng)前ViewGroup)的MeasureSpec屬性來計算出子元素的MeasureSpec最后調(diào)用->child.measure()傳入之前計算的測量規(guī)格.

ViewGroup為什么沒有定義測量的具體過程? 因為具體的測量過程需要交給子類去實現(xiàn)的. 比如LinearLayout,RelativeLayout.

看一下LinearLayoutonMeasure()是如何定義的.


@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

   if (mOrientation == VERTICAL) {

       measureVertical(widthMeasureSpec, heightMeasureSpec);

   } else {

       measureHorizontal(widthMeasureSpec, heightMeasureSpec);

   }

}

根據(jù)設(shè)置的排列方式這里分之了兩種測量方法. 稍微看一下大概輪廓,選擇measureVertical()不貼源碼了這個方法300行呢!

首先這個方法會遍歷每個子元素并執(zhí)行->measureChildBeforeLayout()方法.這個方法內(nèi)部會調(diào)用子元素的measure(), 這樣子元素會依次測量. 并且會通過mTotalLenght這個變量來存儲LinearLayout在豎直方向上的初步高度, 每測量一個就會增加. 當(dāng)子元素測量完之后,LinearLayout會測量自己的大小.


在對自己進(jìn)行測量的時候. 如果布局中的高度采用的是match_parent或者具體數(shù)值, 那么它的測量過程和View一樣,即高度為specSize. 如果布局中采用wrap_content那么高度就是所有的子元素總和但是不能超過父元素剩余空間, 還有豎直方向LinearLayout的padding. 具體可參考resolveSizeAndState()的實現(xiàn).

到這里基本上measure測量過程已經(jīng)做了比較詳細(xì)的分析. 這個過程也是三大過程中最復(fù)雜的一個. 在measure完成之后就可以通過getMeasuredWidth/Height方法獲取View的測量寬高. 但是請注意:某些極端情況下,measure可能執(zhí)行多次. 所以盡量在onLayout()方法中去獲得最終寬高.

image.png

正確獲取寬高方法

首先明確一點(diǎn):View的measure和Activity的生命周期方法不是同步執(zhí)行.所以無法保證在某個生命周期(onCreate,onStart)獲取到正確的測量寬高

  • onWindowFocusChanged()
  • view.post(runnable)
  • ViewTreeObserve
  • view.measure()
  1. onWindowFocusChanged():View已經(jīng)初始化完畢,寬高已經(jīng)準(zhǔn)備好. 這里需要注意只要Activity的焦點(diǎn)發(fā)生變化此方法就會被調(diào)用.所以如果你的界面會頻繁的進(jìn)行onPauseonResume.并且里面有很多關(guān)聯(lián)依賴的方法. 那就請注意這不是一個好辦法.
  2. 通過post可以將一個runnable投遞到消息隊列的尾部,然后等待Looper調(diào)用此runnable的時候.View已經(jīng)初始化完畢.
  3. 使用ViewTreeObserver. 當(dāng)View的可見性發(fā)生了改變的時候.onGlobalLayout()將發(fā)生回調(diào).注意伴隨著View樹的狀態(tài)改變等,這個回調(diào)方法可能會被調(diào)用多次. 使用代碼如下

ViewTreeObserver viewTreeObserver = tv_main.getViewTreeObserver();

       viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

           @Override

           public void onGlobalLayout() {

               tv_main.getViewTreeObserver().removeOnGlobalLayoutListener(this);

               tv_main.getMeasuredHeight();

               tv_main.getMeasuredWidth();

           }

       });

  1. view.measure(widthMeasureSpec, heightMeasureSpec)

也可以手動進(jìn)行測量,但是需要分情況處理.

match_parent

當(dāng)View是此屬性的時候無法使用measure(),首先使用這種方法需要的參數(shù),是通過父容器和子元素組合來生成的子元素的MeasureSpec屬性. 所以在外部我們不知道父元素的參數(shù)值得時候只能處理不需要父元素數(shù)據(jù)就可以生成子元素的MeasureSpec的模式

所以很清楚, 這個match_patch這個模式,在給其子元素構(gòu)造MeasureSpec的時候需要得值parentSize,所以得到的也是無效.

具體數(shù)值px/dx

假設(shè)這里是100px, 首先構(gòu)成寬高對應(yīng)的MeasureSpec屬性

int widthSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
tv_main.measure(widthSpec, heightSpec);

wrap_content

int widthSpec = View.MeasureSpec.makeMeasureSpec(((1 << 30)-1), View.MeasureSpec.AT_MOST);
int heightSpec = View.MeasureSpec.makeMeasureSpec(((1 << 30)-1), View.MeasureSpec.AT_MOST);
tv_main.measure(widthSpec, heightSpec);

通過(1<<30)-1 可以構(gòu)成一個MeasureSpec低30位的最大值. 用理論上View能支持的最大值去構(gòu)造

關(guān)于網(wǎng)上一些在make的使用傳入UNSPECIFIED,屬于違背了內(nèi)部實現(xiàn)的規(guī)范.不用最好

關(guān)于網(wǎng)上另一種measure()直接傳入LayoutParams.WRAP_CONTENT. 其實也只有當(dāng)子元素為wrap_content和子元素為match_parent并且父元素是wrap_conetnt時會碰巧有效.

layout過程

ViewGroup中會先通過layout()方法確定本身的位置. 然后調(diào)用onLayout()方法遍歷所有的子元素,并調(diào)用子元素的layout()方法確定子元素的位置…依次循環(huán).

提出Viewlayout方法, 這里抽取部分代碼

public void layout(int l, int t, int r, int b) {

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

   }

這樣來看,大致流程通過setFrame()方法來設(shè)定View的四個頂點(diǎn)的位置, 即mLeft,mTop,mBottom,mRight,這四個頂點(diǎn)一旦確定.當(dāng)前View的位置也就確定. 然后會調(diào)用onLayout()方法. 這個方法是確定子元素的View位置.

這里的和onMeasure()類似, onLayout()具體實現(xiàn)和具體的布局有關(guān), 所以View和ViewGroup均沒有真正實現(xiàn)onLayout()方法.

看一下LinearLayoutonLayout()源碼

@Override

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

   }

}

onMeasure()一樣分支,接下來跟進(jìn)layoutVertical()貼出主要代碼

void layoutVertical(int left, int top, int right, int bottom) {

           //省略一部分...

       for (int i = 0; i < count; i++) {

           final View child = getVirtualChildAt(i);

           if (child == null) {

               childTop += measureNullChild(i);

           } else if (child.getVisibility() != GONE) {

               final int childWidth = child.getMeasuredWidth();

               final int childHeight = child.getMeasuredHeight();

               final LinearLayout.LayoutParams lp =

                       (LinearLayout.LayoutParams) child.getLayoutParams();

               int gravity = lp.gravity;

               if (gravity < 0) {

                   gravity = minorGravity;

               }

                //省略一部分...

               if (hasDividerBeforeChildAt(i)) {

                   childTop += mDividerHeight;

               }

               childTop += lp.topMargin;

               setChildFrame(child, childLeft, childTop + getLocationOffset(child),

                       childWidth, childHeight);

               childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

               i += getChildrenSkipCount(child, i);

           }

       }

   }

上面代碼大體邏輯: 首先遍歷所有孩子并調(diào)用setChildFrame()來為子元素指定對應(yīng)的位置. 其中childTop會逐漸增大, 這就意味著后面的子元素會被放置在靠下的位置. 而setChildFrame()內(nèi)部僅有一行代碼, 就是調(diào)用子元素的layout()并傳入它自身應(yīng)該存放的位置.

private void setChildFrame(View child, int left, int top, int width, int height) {        

     child.layout(left, top, left + width, top + height);

 }

而在setChildFrame()中傳入的寬高就是子元素的測量寬高.

而在子元素的layout()中通過setFrame()來設(shè)置元素的四個頂點(diǎn).

getWidth()layout中的寬 和getMeasureWidth()中的寬永遠(yuǎn)一樣么?

在一般情況下,測量measure和layout時候的值是完全一樣的. 因為layout()中接受的參數(shù)就是通過測量的結(jié)果獲取到的. 并且內(nèi)部直接通過setFrame()賦值到自己的四個成員變量上. 但是如果對layout()進(jìn)行了復(fù)寫.如下

 @Override

protected void layout(int l, int t, int r, int b) {

   super.layout( l,  t+200,  r,  b+200);

}

如果進(jìn)行了這樣的復(fù)寫, 那么最終寬高永遠(yuǎn)會與測量的出來的值相差200.

layout流程.png

draw過程

這個過程只是將View繪制到屏幕上面.

  1. 繪制背景background.draw(canvas)
  2. 繪制自己onDraw()
  3. 繪制childrendispatchDraw()
  4. 繪制裝飾onDrawScrollBars()

View繪制過程傳遞是通過dispatchDraw()實現(xiàn)的. 傳遞了自己的畫布. 這個方法會遍歷子元素并且調(diào)用元素的draw()

View一個特有的方法setWillNotDraw(), 這個方法是設(shè)置了true那么系統(tǒng)會進(jìn)行相應(yīng)的優(yōu)化. 在View中默認(rèn)是關(guān)閉的. 而ViewGroup默認(rèn)是開啟的. 如果我們繼承了自定義ViewGroup如果還需要繪制自己的內(nèi)容那么需要顯示的關(guān)閉此標(biāo)記.

draw過程.png

參看文章

《Android 開發(fā)藝術(shù)探索》書集
《Android 開發(fā)藝術(shù)探索》 04-View的工作原理
View的繪制-measure流程詳解
View的繪制-layout流程詳解
View的繪制-draw流程詳解

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容