Android View繪制流程


 

 

Android View繪制流程

框架分析

在之前的下拉刷新中,小結過觸屏消息先到WindowManagerServiceWms)然后順次傳遞給ViewRoot(派生自Handler),經decor viewActivity再傳遞給指定的View,這次整理View的繪制流程,通過源碼可知,這個過程應該沒有涉及到IPC(或者我沒有發現),需要繪制時在UI線程中通過ViewRoot發送一個異步請求消息,然后ViewRoot自己接收並不處理這個消息。

在正式進入View繪制之前,首先需要明確一下Android UI的架構組成,偷圖如下:

 

上述架構很清晰的呈現了ActivityWindowDecorView(及其組成)、ViewRootWMS之間的關系,我通過源碼簡單理了下從啟動Activity到創建View的過程,大致如下

 

在上圖中,performLaunchActivity函數是關鍵函數,除了新建被調用的Activity實例外,還負責確保Activity所在的應用程序啟動、讀取manifest中關於此activity設置的主題信息以及上圖中對“6.onCreate”調用也是通過對mInstrumentation.callActivityOnCreate來實現的。圖中的“8. mContentParent.addView”其實就是架構圖中phoneWindowDecorView里面的ContentViews,該對象是一個ViewGroup類實例。在調用AddView之后,最終就會觸發ViewRoot中的scheduleTraversals這個異步函數,從而進入ViewRootperformTraversals函數,在performTraversals函數中就啟動了View的繪制流程。

performTraversals函數在2.3.5版本源碼中就有近六百行的代碼,跟我們繪制view相關的可以抽象成如下的簡單流程圖

 

流程圖中的host其實就是mView,而ViewRoot中的這個mView其實就是DecorView,之所以這么說,又得具體看源碼中ActivityThreadhandleResumeActivity函數,在這里我就不展開了。上述流程主要調用了Viewmeasurelayoutdraw三個函數。

measure過程分析

因為DecorView實際上是派生自FrameLayout的類,也即一個ViewGroup實例,該ViewGroup內部的ContentViews又是一個ViewGroup實例,依次內嵌ViewViewGroup形成一個View樹。所以measure函數的作用是為整個View樹計算實際的大小,設置每個View對象的布局大小(“窗口”大小)。實際對應屬性就是View中的mMeasuredHeight(高)和mMeasureWidth(寬)。

View類中measure過程主要涉及三個函數,函數原型分別為

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

前面兩個函數都是final類型的,不能重載,為此在ViewGroup派生的非抽象類中我們必須重載onMeasure函數,實現measure的原理是:假如View還有子View,則measureView,直到所有的子View完成measure操作之后,再measure自己。ViewGroup中提供的measureChildmeasureChildWithMargins就是實現這個功能的。

在具體介紹測量原理之前還是先了解些基礎知識,即measure函數的參數由類measureSpecmakeMeasureSpec函數方法生成的一個32位整數,該整數的高兩位表示模式(Mode),低30位則是具體的尺寸大小(specSize)。

MeasureSpec有三種模式分別是UNSPECIFIED, EXACTLYAT_MOST,各表示的意義如下

如果是AT_MOSTspecSize代表的是最大可獲得的尺寸;

如果是EXACTLYspecSize代表的是精確的尺寸;

如果是UNSPECIFIED,對於控件尺寸來說,沒有任何參考意義。

那么對於一個View的上述ModespecSize值默認是怎么獲取的呢,他們是根據ViewLayoutParams參數來獲取的:

參數為fill_parent/match_parent時,ModeEXACTLYspecSize為剩余的所有空間;

參數為具體的數值,比如像素值(pxdp),ModeEXACTLYspecSize為傳入的值;

參數為wrap_contentModeAT_MOSTspecSize運行時決定。

具體測量原理

上面提供的ModespecSize只是程序員對View的一個期望尺寸,最終一個View對象能從父視圖得到多大的允許尺寸則由子視圖期望尺寸和父視圖能力尺寸(可提供的尺寸)兩方面決定。關於期望尺寸的設定,可以通過在布局資源文件中定義的android:layout_widthandroid:layout_height來設定,也可以通過代碼在addView函數調用時傳入的LayoutParams參數來設定。父View的能力尺寸歸根到最后就是DecorView尺寸,這個尺寸是全屏,由手機的分辨率決定。期望尺寸、能力尺寸和最終允許尺寸的關系,我們可以通過閱讀measureChildmeasureChildWithMargins都會調用的getChildMeasureSpec函數的源碼來獲得,下面簡單列表說明下三者的關系

父視圖能力尺寸

子視圖期望尺寸

子視圖最終允許尺寸

EXACTLY + Size1

EXACTLY + Size2

EXACTLY + Size2

EXACTLY + Size1

fill_parent/match_parent

EXACTLY+Size1

EXACTLY + Size1

wrap_content

AT_MOST+Size1

AT_MOST+Size1

EXACTLY + Size2

EXACTLY+Size2

AT_MOST+Size1

fill_parent/match_parent

AT_MOST+Size1

AT_MOST+Size1

wrap_content

AT_MOST+Size1

UNSPECIFIED+Size1

EXACTLY + Size2

EXACTLY + Size2

UNSPECIFIED+Size1

fill_parent/match_parent

UNSPECIFIED+0

UNSPECIFIED+Size1

wrap_content

UNSPECIFIED+0

上述表格展現的是子視圖最終允許得到的尺寸,顯然147三項沒有對Size1Size2進行比較,所以允許尺寸是可以大於父視圖的能力尺寸的,這個時候最終的視圖尺寸該是多少呢?AT_MOSTUNSPECIFIEDView又該如何決策最終的尺寸呢? 

通過Demo演示的得到的結果,假如Size2Size1的尺寸大,假如不使用滾動效果的話,子視圖超出部分將被裁剪掉,該父視圖中如果在該子視圖后面還有其他視圖,那么也將被裁剪掉,但是通過調用其getVisibility還是顯示該控件是可見的,所以裁剪后控件依然是有的,只是用戶沒辦法觀察到;在使用滾動效果的情況下,就能將原本被裁剪掉的控件通過滾動顯示出來。

對於第二個問題,根據源碼ViewOnMeasure函數調用的getDefaultSize函數獲知,默認情況下,控件都有一個最小尺寸,該值可以通過設置android:minHeightandroid:minWidth來設置(無設置時缺省為0);在設置了背景的情況下,背景drawable的最小尺寸與前面設置的最小尺寸比較,兩者取大者,作為控件的最小尺寸。在UNSPECIFIED情況下就選用這個最小尺寸,其它情況則根據允許尺寸來。不過這個是默認規則,通過demo發現,TextViewAT_MOST+Size情況下,並不是以Size作為控件的最終尺寸,結果發現在TextView的源碼中,重載了onMeasure函數,有價值的代碼如下:

……

int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int heightSize = MeasureSpec.getSize(heightMeasureSpec);

……

if (widthMode == MeasureSpec.AT_MOST) {

    width = Math.min(widthSize, width);

}

……

if (heightMode == MeasureSpec.AT_MOST) {

    height = Math.min(desired, heightSize);

}

……

至於其中的widthdesired值,感興趣的同學可以具體關注下。雖然FrameWork提供了視圖默認的尺寸計算規則,但是最終的視圖布局大小可以重載onMeasure函數來修改計算規則,當然也可以不計算直接通過setMeasuredDimension來設置(需要注意的是,如果通過setMeasuredDimension的同時還要調用父類的onMeasure函數,那么在調用父類函數之前調用的setMeasuredDimension會無效果)。

layout過程分析

上述measure過程達到的結果是設定了視圖的高和寬,layout過程的作用就是設定視圖在父視圖中的四個點(分別對應View四個成員變量mLeftmTopmLeftmBottom)。同樣layout也是被fianl修飾符限定為不能重載,不過在ViewGrouponLayout函數被abstract修飾,即所有派生自ViewGroup的類必須實現onLayout函數,從而實現對其包含的所有子視圖的布局設定。

那么上述的measure結果與layout有什么關系,截取ViewRootFrameLayout兩個類中onLayout函數的部分代碼如下:

//ViewRootperformTraversals函數measure之后對layout的調用代碼

host.layout(0, 0, host.mMeasuredWidthhost.mMeasuredHeight);

//FrameLayouonLayout函數部分源碼

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

        final int count = getChildCount();

        ……

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

            final View child = getChildAt(i);

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

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

                final int width = child.getMeasuredWidth();

                final int height = child.getMeasuredHeight();

                int childLeft = parentLeft;

                int childTop = parentTop;

                final int gravity = lp.gravity;

 

                if (gravity != -1) {

                    final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;

                    final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

 

                    switch (horizontalGravity) {

                        case Gravity.LEFT:

                            childLeft = parentLeft + lp.leftMargin;

                            break;

                        case Gravity.CENTER_HORIZONTAL:

                            childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin;

                            break;

                        case Gravity.RIGHT:

                            childLeft = parentRight - width - lp.rightMargin;

                            break;

                        default:

                            childLeft = parentLeft + lp.leftMargin;

                    }

 

                    switch (verticalGravity) {

                        case Gravity.TOP:

                            childTop = parentTop + lp.topMargin;

                            break;

                        case Gravity.CENTER_VERTICAL:

                            childTop = parentTop + (parentBottom - parentTop - height) / 2 + lp.topMargin - lp.bottomMargin;

                            break;

                        case Gravity.BOTTOM:

                            childTop = parentBottom - height - lp.bottomMargin;

                            break;

                        default:

                            childTop = parentTop + lp.topMargin;

                    }

                }

 

                child.layout(childLeft, childTop, childLeft + width, childTop + height);

            }

        }

    }

從代碼顯然可知具體layout布局時,就是根據measure過程設置的高和寬,結合視圖在父視圖中的起始位置,再外加視圖的layoutgravity屬性來設置四個點的具體位置(在LinearLayout中還會增加對layoutweight屬性的考慮)。這個過程相對沒有measure那么復雜。

需要注意的是在自定義組合控件的時候,我們可以根據需要不用或只用部分measure過程計算得到的尺寸,具體可以看下之前做的下拉刷新控件直接重載的onLayout函數:

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

    if (getChildCount() > 2) {

        throw new IllegalStateException("NPullToFreshContainer can host only two direct child");

    }

        

    View headView = getChildAt(0);

    View contentView = getChildAt(1);

    if(headView != null){

     headView.layout(0, -HEAD_VIEW_HEIGHT + mTatolScroll, getMeasuredWidth(), mTatolScroll);// mTatolScroll是下拉的位移值

    }

   

    if(contentView != null){

    contentView.layout(0, mTatolScroll, getMeasuredWidth(), getMeasuredHeight());

    }

        

    if (mFirstLayout) {        

     HEAD_VIEW_HEIGHT = getChildAt(0).getMeasuredHeight();

       mFirstLayout = false;

    }

}

draw過程分析

ViewDraw過程,其實相對來說應該比measure過程更為復雜,正因為其很復雜,所以android框架層已經將draw過程考慮得相當周全,雖然view類的Draw函數沒用final修飾,但是我們自定義的View,一般也不需要去重載實現它,自己目前也沒有自己去draw過界面,對整個過程,只能偷別人整理的邏輯,結合源碼瀏覽了一下,在這里做個標注。

draw()方法實現的功能流程如下:

1、調用background.draw(canvas)繪制該View的背景

2、調用onDraw(canvas)方法繪制視圖本身(每個View都需要重載該方法,ViewGroup不需要實現該方法)

3、調用dispatchDraw(canvas)方法繪制子視圖(ViewGroup類已經為我們重寫了dispatchDraw ()的功能實現,其內部會遍歷每個子視圖,調用drawChild()去重新回調每個子視圖的draw()方法)

4、調用onDrawScrollBars(canvas)繪制滾動條

為了說明measurelayoutdraw過程的連續性,摘得draw中的源碼如下

……

if (mBackgroundSizeChanged) {

    background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);

    mBackgroundSizeChanged = false;

}

……

上述的mLeftmTopmLeftmBottom就是我們在layout是設定的結果值,這里之所以要用減法獲取高寬尺寸而不用measure過程設定的mMeasuredHeightmMeasureWidth,個人感覺就是因為我們可以在代碼中通過直接調用Viewlayout函數避開measure測算結果而導致真實高寬不等於mMeasuredHeightmMeasureWidth這種情況。

上述代碼中的mBackgroundSizeChanged是個私有成員變量,源碼中只能在ViewonScrollChanged(int l, int t, int oldl, int oldt) layout過程調用的setFrame(int left, int top, int right, int bottom) setBackgroundDrawable(Drawable d)這三個函數中對其修改為true

到這里,除了具體的繪制外,我們對從ActivityView的繪制流程應該比較清楚了。

 

 

 

本文除了參閱源碼,發現下面兩篇博文幫助很大,有興趣可以詳細閱讀

http://blog.csdn.net/qinjuning/article/details/7110211

http://www.cnblogs.com/bastard/archive/2012/04/10/2440577.html

驗證View measure現象的demo見 http://files.cnblogs.com/franksunny/ViewDemo.rar

由於文檔中的圖片沒有顯示出來,所以上傳一個pdf文檔,方便查閱 http://files.cnblogs.com/franksunny/AndroidView%E7%BB%98%E5%88%B6%E6%B5%81%E7%A8%8B.pdf

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM