1. 程式人生 > >View繪製機制和LayoutInflater動態載入以及三種繪圖介面更新區別

View繪製機制和LayoutInflater動態載入以及三種繪圖介面更新區別

View繪製流程及機制

流程研究

場景:最外層自定義MaxViewGroup繼承自LinearLayout+內層自定義ViewGroup繼承自LinearLayout+自定義View
注:1.LinearLayout的onMearsure過程為兩遍,每次呼叫View的onMeasure一遍。
2.RelativeLayout的onMeasure過程為三遍,每次呼叫View的onMeasure兩遍

ViewGroup/View繪製過程:
START:
MaxViewGroup-onMeasure–>
ViewGroup–onMeasure–>
View–onMeasure–>
View

–onMeasure完成–>
ViewGroup–onMeasure完成–>
MaxViewGroup–onMeasure完成–>
---------重複上面過程一遍---------
MaxViewGroup-onLayout–>
ViewGroup–onLayout–>
View–onLayout–>
View–onLayout完成–>
ViewGroup–onLayout完成–>
MaxViewGroup–onLayout完成–>
MaxViewGroup-draw
–>onDraw
–>onDraw完成–>
ViewGroup
-draw
–>onDraw
–>onDraw完成–>
View-draw
–>onDraw
–>onDraw完成–>
draw完成–>
ViewGroup-draw完成–>
MaxViewGroup-draw完成–>
END

結論:①所以在不增加巢狀層數的時候LinearLayout比RelativeLayout的效能好,主要體現在測量過程上。
②繪製過程中測量順序為父容器呼叫onMeasure然後依次呼叫子View的onMeasure,然後在最底層子View測量完成後在依次測量其父容器,(和View事件分發很像)。下來定位,父容器呼叫onLayout,然後依次呼叫子View的onLayout,然後在最底層子View定位完成後在依次定位其父容器的位置。最後繪製,父容器呼叫draw方法,draw會呼叫父容器的onDraw,在自己onDraw結束後會呼叫子View的draw方法,子view會呼叫自己的onDraw方法,這個方法完了後會呼叫子View的子View的draw方法,直到最底層的View呼叫draw方法,呼叫onDraw方法,onDraw完成,返回父容器的draw方法,父容器的draw方法完成,依次向上,最終完成整個View的繪製。
View的draw方法遵循如下規則:

1. Draw the background //繪製背景
2. If necessary, save the canvas’ layers to prepare for fading //如果有必要,儲存畫布的圖層以備褪色
3. Draw view’s content //繪製檢視的內容
4. Draw children //繪製子View
5. If necessary, draw the fading edges and restore layers //如有必要,繪製褪色邊並還原圖層
6. Draw decorations (scrollbars for instance) //繪製裝飾(例如滾動條)

機制研究

1.onMeasure
measure是測量的意思,那麼onMeasure()方法顧名思義就是用於測量檢視的大小的。View系統的繪製流程會從ViewRoot的performTraversals()方法中開始,在其內部呼叫View的measure()方法。measure()方法接收兩個引數,widthMeasureSpec和heightMeasureSpec,這兩個值分別用於確定檢視的寬度和高度的規格和大小。

MeasureSpec的值由specSize和specMode共同組成的,其中specSize記錄的是大小,specMode記錄的是規格。specMode一共有三種類型

  1. EXACTLY:表示父檢視希望子檢視的大小應該是由specSize的值來決定的,系統預設會按照這個規則來設定子檢視的大小,開發人員當然也可以按照自己的意願設定成任意的大小。

  2. AT_MOST:表示子檢視最多隻能是specSize中指定的大小,開發人員應該儘可能小得去設定這個檢視,並且保證不會超過specSize。系統預設會按照這個規則來設定子檢視的大小,開發人員當然也可以按照自己的意願設定成任意的大小。

  3. UNSPECIFIED:表示開發人員可以將檢視按照自己的意願設定成任意的大小,沒有任何限制。這種情況比較少見,不太會用到。

view的measure()這個方法是final的,因此我們無法在子類中去重寫這個方法,說明Android是不允許我們改變View的measure框架的。然後呼叫了onMeasure()方法,這裡才是真正去測量並設定View大小的地方,預設會呼叫getDefaultSize()方法來獲取檢視的大小。最後在onMeasure()方法中呼叫setMeasuredDimension()方法來設定測量出的大小,這樣一次measure過程就結束了。
ViewGroup有一個measureChildren()方法來去測量子檢視的大小。

需要注意的是,在setMeasuredDimension()方法呼叫之後,我們才能使用getMeasuredWidth()和getMeasuredHeight()來獲取檢視測量出的寬高,以此之前呼叫這兩個方法得到的值都會是0。

2.onLayout
ViewRoot的performTraversals()方法會在measure結束後繼續執行,並呼叫View的layout()方法來執行此過程,layout()方法接收四個引數,分別代表著左、上、右、下的座標,當然這個座標是相對於當前檢視的父檢視而言的。可以看到,這裡還把剛才測量出的寬度和高度傳到了layout()方法中,在layout()方法中,首先會呼叫setFrame()方法來判斷檢視的大小是否發生過變化,以確定有沒有必要對當前的檢視進行重繪,同時還會在這裡把傳遞過來的四個引數分別賦值給mLeft、mTop、mRight和mBottom這幾個變數。接下來會呼叫onLayout()方法,但它是一個空方法,留給開發者自己實現。

onLayout()過程結束後,我們就可以呼叫getWidth()方法和getHeight()方法來獲取檢視的寬高了
getMeasureWidth()和getWidth()的區別:首先getMeasureWidth()方法在measure()過程結束後就可以獲取到了,而getWidth()方法要在layout()過程結束後才能獲取到。另外,getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進行設定的,而getWidth()方法中的值則是通過檢視右邊的座標減去左邊的座標計算出來的。

3.onDraw
第一步對檢視的背景進行繪製。這裡會先得到一個mBGDrawable物件,然後根據layout過程確定的檢視位置來設定背景的繪製區域,之後再呼叫Drawable的draw()方法來完成背景的繪製工作。那麼這個mBGDrawable物件是從哪裡來的呢?其實就是在XML中通過android:background屬性設定的圖片或顏色。當然你也可以在程式碼中通過setBackgroundColor()、setBackgroundResource()等方法進行賦值。接下來的第三步 ,這一步的作用是對檢視的內容進行繪製。呼叫了onDraw()方法,並且是一個空方法,因為每個檢視的內容部分肯定都是各不相同的,這部分的功能交給子類來去實現也是理所當然的。第三步完成之後緊接著會執行第四步,這一步的作用是對當前檢視的所有子檢視進行繪製。但如果當前的檢視沒有子檢視,那麼也就不需要進行繪製了。因此你會發現View中的dispatchDraw()方法又是一個空方法,而ViewGroup的dispatchDraw()方法中就會有具體的繪製程式碼。以上都執行完後就會進入到第六步,也是最後一步,這一步的作用是對檢視的滾動條進行繪製。任何一個檢視都是有滾動條的,只是一般情況下我們都沒有讓它顯示出來而已。通過以上流程分析,相信大家已經知道,View是不會幫我們繪製內容部分的,因此需要每個檢視根據想要展示的內容來自行繪製。如果你去觀察TextView、ImageView等類的原始碼,你會發現它們都有重寫onDraw()這個方法,並且在裡面執行了相當不少的繪製邏輯。繪製的方式主要是藉助Canvas這個類,它會作為引數傳入到onDraw()方法中,供給每個檢視使用。Canvas這個類的用法非常豐富,基本可以把它當成一塊畫布,在上面繪製任意的東西。
主要參考:參考郭霖大神的部落格(我只是簡單整理了一下,因為他的部落格講的很詳細了)

LayoutInflater工作原理

使用:

//inflate引數型別:int res,ViewGroup,boolean attahToRoot
Inflater inflater = LayoutInflater.from(getContext()).inflate(res,root,attahToRoot);

我們看一下inflate方法:

 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
	//一些初始化,傳值等操作
        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        View result = root;

        try {
            // Look for the root node.
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }

            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }

            final String name = parser.getName();

            if (DEBUG) {
                System.out.println("**************************");
                System.out.println("Creating root view: "
                        + name);
                System.out.println("**************************");
            }
	//對marge型別xml進行檢查是否符合要求,root不能為null並且attachToRoot要為true才可以
            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
                 //下面方法執行的條件:root不為空,attachToRoot為true,此時root就是根佈局了
		//內部實現:遞迴方法載入root下的佈局
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // Temp is the root view that was found in the xml
                //這個是根據tag建立View,即建立root
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    if (DEBUG) {
                        System.out.println("Creating params from root: " +
                                root);
                    }
                    // Create layout params that match root, if supplied 建立與根匹配的佈局引數(如果提供的話)
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }

                if (DEBUG) {
                    System.out.println("-----> start inflating children");
                }

                // Inflate all children under temp against its context. 載入所有位於temp下的子View
                rInflateChildren(parser, temp, attrs, true);

                if (DEBUG) {
                    System.out.println("-----> done inflating children");
                }

                // We are supposed to attach all the views we found (int temp)
                // to root. Do that now.
                //當root不為null並且attachToRoot為true則把temp新增到root下
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }

                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                //沒有root,並且attachToRoot(附加到root下)為false
                //則temp就是最終結果
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {
            final InflateException ie = new InflateException(e.getMessage(), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } catch (Exception e) {
            final InflateException ie = new InflateException(parser.getPositionDescription()
                    + ": " + e.getMessage(), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } finally {
            // Don't retain static reference on context.
            mConstructorArgs[0] = lastContext;
            mConstructorArgs[1] = null;

            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        return result;
    }
}

在這個載入的方法裡首先用pull的方法解析xml。然後根據root和attachToRoot這兩引數分情況處理。
有以下情況:
1.root = null ,attachToRoot= true: 則temp為最終結果,在不是marge的情況下
2.root =null ,attachToRoot= false: 則temp為最終結果
3.root != null ,attachToRoot= true: 則root為最終結果
4.root != null ,attachToRoot= false: 則temp為最終結果,在不是marge的情況下
在不是mage標籤的情況下,會呼叫createViewFromTag()這個方法,它把節點名和引數傳了進去,它是根據節點名來建立View物件的然後返回值給temp。注意在createViewFromTag()方法的內部又會去呼叫createView()方法,然後使用反射的方式創建出View的例項並返回。當然,這裡只是創建出了一個根佈局的例項而已,接下來會呼叫rInflateChildren()方法其內部又呼叫rInflate()方法,來迴圈遍歷這個根佈局下的子元素並addView到ViewGroup中。最終完成了動態佈局載入。

三種繪圖介面更新

1.requesLayout()
子View呼叫requestLayout方法,會標記當前View及父容器,同時逐層向上提交,直到ViewRootImpl處理該事件,ViewRootImpl會呼叫三大流程,從measure開始,對於每一個含有標記位的view及其子View都會進行測量、佈局、繪製。

2.invalidate()
該方法的呼叫會引起View樹的重繪,常用於內部呼叫(比如 setVisiblity())或者需要重新整理介面的時候,需要在主執行緒(即UI執行緒)中呼叫該方法,當子View呼叫了invalidate方法後,會為該View新增一個標記位,同時不斷向父容器請求重新整理,父容器通過計算得出自身需要重繪的區域,直到傳遞到ViewRootImpl中,最終觸發performTraversals方法,進行開始View樹重繪流程由於沒有新增measure和layout的標記位,因此measure、layout流程不會執行,而是直接從draw流程開始(只繪製需要重繪的檢視)。

3.postInvalidate()
這個方法與invalidate方法的作用是一樣的,都是使View樹重繪,但兩者的使用條件不同,postInvalidate是在非UI執行緒中呼叫,這裡用了Handler,傳送了一個非同步訊息到主執行緒,這裡傳送的是MSG_INVALIDATE,即通知主執行緒重新整理檢視。

最後
一般來說,如果View確定自身不再適合當前區域,比如說它的LayoutParams發生了改變,需要父佈局對其進行重新測量、佈局、繪製這三個流程,往往使用requestLayout。而invalidate則是重新整理當前View,使當前View進行重繪,不會進行測量、佈局流程,因此如果View只需要重繪而不需要測量,佈局的時候,使用invalidate方法往往比requestLayout方法更高效。