天天看點

View的繪制流程二(視圖的展示)

文章目錄

      • ViewRoot和DecorView
      • MeasureSpce
      • MeasureSpce和LayouParams
        • 普通View的MeasureSpce确定,父容器的MeasureSpce和自身的LayoutParams的關系如下圖:
      • ViewGroup的measure過程
      • View的measure過程
      • Layout過程
      • Draw過程
      • getMeasureWidth和getWidth的差別
      • 自定義View重寫onMeasure()後布局中wrap_content失效
      • 在activity中擷取View的寬高
      • invalidate、postInvalidate與requestLayout三個方法的差別:

ViewRoot和DecorView

整個界面的繪制是從ViewRoot開始的,ViewRoot的對應類是ViewRootlmp類,ViewRoot是連接配接DecorView和WindowManager的紐帶。

界面繪制首先會調用其performTraversals()函數,然後它需要經過三個流程才能夠将一個View繪制出來,分别經過measure,layout,draw三個流程,measure是測量View視圖的寬高,layout是設定View的位置,draw是最終将View繪制在螢幕上;

其具體邏輯關系如下圖:

View的繪制流程二(視圖的展示)

其中,performMeasure,performLayout,performDraw分别完成了頂級View的measure,layout,和draw的過程;

在performMeasure中又會調用measure,在measure中又會調用onMeasure函數,在onMeasure中,父容器會對所有的子元素進行measure過程,這個時候measure流程就就從父容器傳遞到了子容器中,這就完成了一個measure流程,在子容器中又會重複父容器的measure流程,反複循環直至周遊完整個View樹。

而performLayout,performDraw其傳遞的原理和performMeasure是一樣的。

MeasureSpce

MeasureSpce需要單獨進行說明一下,因為MeasureSpce很大程度上決定了一個View的尺寸大小,之是以說是很大程度是因為,View的尺寸大小除了受到MeasureSpce的影響之外,還受到父容器MeasureSpce和該View的layoutParams的影響,關于layoutParams系統會将layoutParams轉換成對應的MeasureSpce,再根據這個MeasureSpce來對該View進行測量.

MeasureSpce 是一個32位的int類型數值,前2位是SpceMode,後30位是SpceSize,SpceMode表示的是測量模式,SpceMode表示的是某種測量模式下規格的大小.

SpceMode的類型有三種,分别如下:

  • UNSPECIFIED:

    父容器不對View有任何影響,View要多大給多大,通常用于系統内部

  • EXACTLY:

    父容器已經檢測出了View的精确大小,而View大小具體的數值就是SpceSize的值,它對應LayoutParams中的match_parent和具體數值這兩種

  • AT_MOST:

    父容器指定了一個可用大小即SpceSize,View的大小不能大于這個值,具體什麼值需要看View的具體實作,它對應LayoutParams中的wrap_content

MeasureSpce和LayouParams

頂級View(DecorView)的MeasureSpce是取決于本身的LayoutParams.

普通View的MeasureSpce是取決于父容器的MeasureSpce和自身的LayoutParams.MeasureSpce一旦确定,就可以在onMeasure确定View的測量寬高.

普通View的MeasureSpce确定,父容器的MeasureSpce和自身的LayoutParams的關系如下圖:

View的繪制流程二(視圖的展示)
  • 當View的寬高采用的是固定數值的時候,不論父容器是精确模式還是最大模式,View的MeasureSpce都是精确模式,其大小數值是LayoutParams中的數值
  • 當View的寬高采用的是match_parent,當父容器是精确模式的時候,View的MeasureSpce是精确模式,其大小是父容器的剩餘空間,當父容器是最大模式的時候,View的MeasureSpce是精确模式是最大模式,其大小是父容器的剩餘空間
  • 當View的寬高采用的是wrap_content,不論父容器的MeasureSpce是什麼模式,View的MeasureSpce模式都是最大模式,且大小不超過父容器的剩餘空間
  • 以上并未介紹UNSPECIFIED,是因為通常用于系統内部多次measure的情形,是以沒有太關注該模式.

ViewGroup的measure過程

對于ViewGroup來說,它除了完成自身的measure流程之外,還會去周遊子所有的子元素,執行子元素的measure方法,ViewGroup與View與之不同的一點是,ViewGroup是一個抽象類,沒有onMeasure()函數,但ViewGroup有一個叫measureChildren的函數

protected void measureChildren(int widthMeasureSpec,int heightMeasureSpec){
final int size=mchi1drencount;
final view[] children =mChildren;

for (inti=0;i<size;++i){
final View chi1d=chi1dren[il;
if((child.mViewFlags & VISTBILITY MASK)!=GONE){
measureChild(child,widthMeasureSpec,heightMeasureSpec);
      }
   }
}
           

在measureChildren函數中完成了對每一個子元素的周遊工作,并且調用了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);
    }
           

在measureChild()中,首先擷取到了目前View的LayoutParams,通過形參擷取到了父類的MeasureSpec,然後通過getChildMeasureSpec()函數,傳入該View的LayoutParams和父元素的LayoutParams建立了該View的MeasureSpec.

View的measure過程

View的measure是一個被final修飾的函數,是以不能重新,不過在measure函數中還調用了onMeasure()函數,我們在複寫時,可以複寫onMeasure()函數.

onMeasure()函數代碼如下:

protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
setMeasuredDimension(getDefaultsize(getSuggestedMinimumwidth(),widthMeasureSpec),getDefaultsize(getSuggestedMinimumHeight(),heightMeasureSpec));
}
           

在onMeasure()函數中,通過調用setMeasureDimension()函數完成了對于View的測量,其四個參數分别是:width的預設值,widthMeasureSpec,height的預設值,heightMeasureSpec.

從這一段函數我們可以看出,測量的View的寬高值是存儲在MeasureSpec中的SpecSize中,而關于MeasureSpec的确定在父容器中完成的.

關于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中的SpecSize.

當測量模式是系統内部用到的UNSPECIFIED模式傳回值為getSuggestedMinimumWidth()或者getSuggestedMinimumHeight()

getSuggestedMinimumWidth()和getSuggestedMinimumHeight()代碼如下:

protected int getSuggestedMinimumwidth()(
return(mBackground==nul1)? mdinwidth:max(mMinWidth,mBackground.
getMinimumwidth());
}
protected int getsuggestedMinimumHeight(){
return(mBackground==nul1)?mMinleight:max(mMinHeight,mBackground.
getMinimumHeight());
}

//所調用到的getMinimumwidth()如下

public int getMinimumwldth()(
final int intrinaicwidth =getIntrinsicwidth();
return intrinsicwidth>0? intrinsicNidth:0;
}
           

由上可以看到,其實作邏輯是,首先判斷是否設定有背景圖檔,當沒有的時候就擷取android:minWidth屬性所設定的值,該值可以為0,當設定了背景圖檔就在android:minWidth和圖檔尺寸中選最大值進行傳回

Layout過程

Layout的作用就是ViewGroup用來确定子元素的位置,在onLayou()函數中會周遊所有的子元素并調用其layout()函數,在該layout()函數中又會調用onLayout()函數.

  • 大緻流程:從頂級View開始依次調用layout(),其中子View的layout()會調用setFrame()來設定自己的四個頂點(mLeft、mRight、mTop、mBottom),接着調用onLayout()來确定其坐标,注意該方法是空方法,因為不同的ViewGroup對其子View的布局是不相同的。
View的繪制流程二(視圖的展示)

Draw過程

Draw的繪制過程也比較簡答,其繪制流程遵循如下步驟:

  1. 繪制背景 background.draw(canvas) : 調用drawBackground方法繪制View的背景(内部調用Drawable的draw方法)
  2. 繪制自己(onDraw):調用onDraw方法繪制View的具體内容
  3. 繪制child(dispathDraw) : 調用dispatchDraw方法繪制子View
  4. 繪制裝飾(onDrawForeground) : 調用onDrawForeground方法繪制裝飾(例如,滾動條)

getMeasureWidth和getWidth的差別

getWidth代碼如下:

public final int getWindth(){
    return mRight - mLeft;
}
           

getWindth計算的傳回值和測量值是一樣的,是以,在View的預設實作下,測量的寬高和最終的寬高是一樣的,隻不過指派的時間不同,測量寬高的指派是在measure流程,而最終寬高的指派是在layout流程,相比測量指派的時間稍微晚一些.

自定義View重寫onMeasure()後布局中wrap_content失效

View的繪制流程二(視圖的展示)

在getDefaultSize()函數中有看到,精确模式和最大模式最受選擇都是MeasureSpec中的SpecSize,另在上圖給出的圖中,我們可以看到,wrap_content屬性占有的空間是父容器剩餘空間的大小,這就導緻,在這種情況下使用match_parent和wrap_content展現效果是一樣的,是以事wrap_content生效,我們需要為其設定最小固定值

示例代碼如下:

protected void onMeasure(int widthMeasureSpec,int heightMeasurespec){
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
int widthSpecsize=MeasureSpec.getsize(widthMeasureSpec);
int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);
int heightSpecsize=MeasureSpec.getSize (heightMeasureSpec);
if (widthspecMode =-MeasureSpec.AT_MoST && heightSpecMode ==
MeasureSpec.AT_MoST){
setMeasuredDimension(mWidth,mHeight);
)else if (widthspecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,heightSpecsize);
)else if (heightSpecMode==MeasureSpec.AT_MoST)
setMeasuredDimension(widthSpecsize,mHeight);}
}
}
           

在activity中擷取View的寬高

在activity中擷取某一View的寬高,需要注意的一點是,View的measure流程和activity的生命周期是不同步的,也就是說不能夠保證,在activity的onCreate,onStart,onResume中任一周期時,某個View的measure流程完成.

能夠在activity中擷取View寬高的方法有如下四種:

1. onWindowFocusChange:

onWindowFocusChange意思就是所有的View都初始化完成了可以調用了,其特性是activity在每次失去焦點和獲得焦點的時候都會調用該方法,示例代碼如下:

public void onWindwFcusChange(boolean hasFocus){
    if(hasFocus){
        int width = view.getMeasureWidth();
        int height = view.getMeasureHeight();
    }
}
           

2. view.post(runnable): 在runnable中包含有擷取view的寬高,通過post将該runnable投遞到隊列尾部,然後等待LOOP調用,當調用到該runnable時View的初始化已經完成,所有可以直接擷取到寬高.示例代碼如下:

proceted void onStart(){
    view.post(new Runnable(){
    @override
    public void run(){
        int width = view.getMeasureWidth();
        int height = view.getMeasureHeight();
    }
})
}
           

3. ViewTressObserver

可以使用ViewTressObserver中的onGlobalLayoutListener,在View樹的狀态發生改變或者View樹内部View的可見性發生改變,則會調用onGlobalLyout()函數,所有,也可以使用該方式擷取View的寬高,示例代碼如下:

ViewTressObserver observer =  view.getViewTreeeObserver();
observer.addGlobalLayoutListener(new GloblLayoutListener(){
    @suppressWarnings("deprecation")
    @override
    public void onGloablLayout(){
        view.getViewTreeeObserver().removeGlobalLayoutListener(this);
        int width = view.getMeasureWidth();
        int height = view.getMeasureHeight();
    }
})
           

4. 通過view.measure(int widthMeasureSpec,int heightMeasureSpec)函數手動設定

通過該方式需要分别按幾種方式讨論

  • match_parent:這種方式,其View的大小是父容器的剩餘空間,而我們目前是無法擷取到父容器的剩餘空間,是以無法測量出View的寬高
  • 具體數值:
widthMeasureSpec  = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
           
  • wrap_content:

    這種方式我們可以設定其最大值,其二進制方式最大是30個1,那麼就是2^30-1,也就是(1 << 30)-1;在最大化模式下,我們使用最大化值去建構,是合理的

widthMeasureSpec  = MeasureSpec.makeMeasureSpec((1 << 30)-1),MeasureSpec.EXACTLY);
heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30)-1,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
           

invalidate、postInvalidate與requestLayout三個方法的差別:

  1. invalidate隻會調onDraw方法且必須在UI線程中調用
  2. postInvalidate隻會調onDraw方法,在工作線程重新整理view
  3. requestLayout會調onMeasure、onLayout和onDraw(特定條件下)方法

SurfaceView和View的差別?

  • SurfaceView是從View基類中派生出來的顯示類,他和View的差別有:

    View需要在UI線程對畫面進行重新整理,而SurfaceView可在子線程進行頁面的重新整理

  • View适用于主動更新的情況,而SurfaceView适用于被動更新,如頻繁重新整理,這是因為如果使用View頻繁重新整理會阻塞主線程,導緻界面卡頓
  • SurfaceView在底層已實作雙緩沖機制,而View沒有,是以SurfaceView更适用于需要頻繁重新整理、重新整理時資料處理量很大的頁面

SurfaceView用法

繼續閱讀