目錄java
Composite設計模式,將物體組合成一個樹結構,讓單個對象和組合對象使用起來都同樣,組合對象負責將實際的操做分發給每個組件。node
這篇博文分析了安卓的View相關的類,它們能夠說是用了Composite設計模式。其中分析View的measure,layout,draw是如何從組合對象分發給單個對象。android
Android Framework中View相關的類,就是用Composite設計模式組織起來的。因爲這涉及到了不少份源代碼,若是一頭扎進去看源碼,心中想必是一團亂麻(碼)。我們帶着問題去看源代碼,效率會高一點。下面的問題分紅兩個類別。關於View流程的問題,每個幾乎均可以寫一篇很長的博文,網上的大神們寫了許多,這裏我就簡單的歸納它的核心要義,截取看到的源代碼。爲了對Composite設計模式有一個更好的認識,這裏仍是要去認識一下View這個類。canvas
關於View的流程的:設計模式
關於設計模式的:數組
廢話很少說,先來一段官方文檔。數據結構
This class represents the basic building block for user interface components. A View occupies a rectangular area on the screen and is responsible for drawing and event handling. View is the base class for widgets, which are used to create interactive UI components (buttons, text fields, etc.). The ViewGroup subclass is the base class for layouts, which are invisible containers that hold other Views (or other ViewGroups) and define their layout properties.app
View是構建UI組件的基本單元,是一個負責繪製(drawing)和事件處理(event handling)的方形區域。View是一些widgets的基類,widgets表明着Composite設計模式中的基本組件。View是ViewGroup的基類,ViewGroup是一個不可見的佈局容器,在Composite設計模式中表明着容器。ide
這裏主要參考[1]。函數
先來看看[1]繪製的層次結構。這裏的層級關係是,外面的框框包含裏面的框框的引用。好比Activity裏面有一個Window對象,PhoneWindow裏面有一個DecorView對象。
這裏給出一個類圖,關注其中的數據結構和依賴關係。
大體流程
這裏大概講一講流程,具體的細節要進入到下面的源代碼去看。
結合類圖來看這個分析。首先在Activity中調用setContentView以後,Activity裏調用Window的setContentView。實際的工做在PhoneWindow中進行。第一次調用,主要作三件事情。一,初始化mDecor和mContentParent。這個經過調用installDecor來完成。二,經過LayoutInflater將setContentView的參數(layoutResID)指向的這個資源,設置到mContentParent裏。三,增長回調函數。
@Override public void setContentView(int layoutResID) { // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window // decor, when theme attributes and the like are crystalized. Do not check the feature // before this happens. if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext()); transitionTo(newScene); } else { mLayoutInflater.inflate(layoutResID, mContentParent); } mContentParent.requestApplyInsets(); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } mContentParentExplicitlySet = true; }
installDecor函數
這裏進一步分析一,installDecor函數。installDecor作兩件事情:1,初始化mDecor;2,初始化mContentParent。
private void installDecor() { mForceDecorInstall = false; if (mDecor == null) { mDecor = generateDecor(-1); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } else { mDecor.setWindow(this); } if (mContentParent == null) { mContentParent = generateLayout(mDecor); ... } }
使用generateDecor函數初始化mDecor
generateDecor函數至關於一個工廠方法。獲取Context以後,調用DecorView的構造器
protected DecorView generateDecor(int featureId) { // System process doesn't have application context and in that case we need to directly use // the context we have. Otherwise we want the application context, so we don't cling to the // activity. Context context; if (mUseDecorContext) { Context applicationContext = getContext().getApplicationContext(); if (applicationContext == null) { context = getContext(); } else { context = new DecorContext(applicationContext, getContext()); if (mTheme != -1) { context.setTheme(mTheme); } } } else { context = getContext(); } return new DecorView(context, featureId, this, getAttributes()); }
使用generateLayout函數初始化mContentParent
初始化各類參數,最後根據調用Window的getLocalFeatures方法獲取features。根據features去找到一個R.layout,這個layout就是mDecor的佈局了。調用DecorView的onResourcesLoaded函數來設置mDecor的mContentRoot。設置好了mDecor以後,調用Window的findViewById,初始化contentParent。
protected ViewGroup generateLayout(DecorView decor) { ... int layoutResource; int features = getLocalFeatures(); // System.out.println("Features: 0x" + Integer.toHexString(features)); if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { layoutResource = R.layout.screen_swipe_dismiss; setCloseOnSwipeEnabled(true); } else if... ... mDecor.startChanging(); mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); ... mDecor.finishChanging(); return contentParent; }
DecorView.java
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) { if (mBackdropFrameRenderer != null) { loadBackgroundDrawablesIfNeeded(); mBackdropFrameRenderer.onResourcesLoaded( this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable, mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState), getCurrentColor(mNavigationColorViewState)); } mDecorCaptionView = createDecorCaptionView(inflater); final View root = inflater.inflate(layoutResource, null); if (mDecorCaptionView != null) { if (mDecorCaptionView.getParent() == null) { addView(mDecorCaptionView, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } mDecorCaptionView.addView(root, new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT)); } else { // Put it below the color views. addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } mContentRoot = (ViewGroup) root; initializeElevation(); }
層級結構如何?
[1]中給出了一個圖。想要本身看看這個圖的方法,單步調試進入了Window.java以後,監視getWindow().getDecorView(),能夠看到它的結構。
DecorView內有個ViewGroup成員mContentRoot。DecorView使用了裝飾者模式,這裏暫且不講。把握好ViewGroup的結構,mContentRoot就是下面的這個結構,首先它自己是一個LinearLayout,而後它有mChildren數組,其中的一個成員是咱們setContentView輸入的layout文件加載的地方。須要注意的是ID_ANDROID_CONTENT指向的,mContentRoot的一個children,它是FrameLayout佈局,是PhoneWindow的mContentParent。
android.widget.LinearLayout{375bc8 V.E...... ......I. 0,0-0,0}
|----- android.view.ViewStub{55a31c3 G.E...... ......I. 0,0-0,0 #1020192 android:id/action_mode_bar_stub}
|----- android.widget.FrameLayout{262ca40 V.E...... ......I. 0,0-0,0 #1020002 android:id/content}
|------|----- android.support.constraint.ConstraintLayout{46206e9 V.E...... ......I. 0,0-0,0}
前面不少地方都看到了LayoutInflater,使用這個類,能夠將佈局資源文件轉爲對象,這些對象像一棵樹同樣被組織起來。這裏就不講具體的代碼分析了(看[1]有詳細的分析),咱們講講前面調用到這個類的inflate方法時候的意義。下面截取兩行,咱們須要搞清楚兩個問題:
PhoneWindow.java mLayoutInflater.inflate(layoutResID, mContentParent); DecorView.java final View root = inflater.inflate(layoutResource, null);
下面的內容節選自LayoutInflater.java。分析:這個方法是上面兩個調用指向的,意義很明顯,若是第一個參數爲ViewGroup,那麼咱們將parse出來的View加入到ViewGroup的孩子中。若是第二個參數爲null,那麼咱們直接返回parse出來的東西。下面有一句註釋值得注意:Temp is the root view that was found in the xml。
結合以前的代碼,咱們能夠知道mDecor的mContentRoot,是根據Window的features找到的xml的root view。PhoneWindow的mContentParent,是根據Window的ID_ANDROID_CONTENT找到的View,指向的是mContentRoot的下的main layout。mContentParent是一個FrameLayout,而後將咱們開發中的佈局文件(如activity_main.xml)加入到這個Framelayout的下面。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { ... try { ... if (TAG_MERGE.equals(name)) { ... } else { // Temp is the root view that was found in the xml final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; ... // We are supposed to attach all the views we found (int temp) // to root. Do that now. 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. if (root == null || !attachToRoot) { result = temp; } } } ... return result; } }
跳轉來跳轉去的,細節不少。這些細節終將被遺忘,咱們能從這裏獲取到什麼知識呢?或者能獲取到對開發有幫助的哪些結論呢?
清楚了它的層次結構,知道咱們的佈局文件最終是在什麼地方。大體瞭解這個流程。
[1]中提到的一些對開發有幫助的結論,具體看[1]。
先來一段官方文檔[4]。
When an Activity receives focus, it will be requested to draw its layout. The Android framework will handle the procedure for drawing, but the Activity must provide the root node of its layout hierarchy.
Drawing begins with the root node of the layout. It is requested to measure and draw the layout tree. Drawing is handled by walking the tree and rendering each View that intersects the invalid region. In turn, each ViewGroup is responsible for requesting each of its children to be drawn (with the draw() method) and each View is responsible for drawing itself. Because the tree is traversed pre-order, this means that parents will be drawn before (i.e., behind) their children, with siblings drawn in the order they appear in the tree.
Drawing the layout is a two pass process: a measure pass and a layout pass.
The measuring pass is implemented in measure(int, int) and is a top-down traversal of the View tree. Each View pushes dimension specifications down the tree during the recursion. At the end of the measure pass, every View has stored its measurements. The second pass happens in layout(int, int, int, int) and is also top-down. During this pass each parent is responsible for positioning all of its children using the sizes computed in the measure pass.
當獲取到focus的時候,請求繪製layout。先序遍歷這個layout的樹結構,調用measure和layout兩個過程。在這兩個過程以後是draw。(文檔的一部分,intersects the invalid,和無效區域判交,有點奇怪?)draw的過程是,每一個ViewGroup調用它的孩子的draw方法,每一個View自己負責draw。
後面主要參考[3]。
[3]中,總結了每一個View都要通過的三個主要的階段:measure, layout, draw。
三個過程的做用:
measure: Measure the view and its content to determine the measured width and the measured height.測量整個View及其內容的寬度和高度。
layout: Assign a size and position to a view and all of its descendants. In this phase, each parent calls layout on all of its children to position them.給View分配大小和位置,若是是一個Parent,那麼還要給它的Children放置位置。
draw: Manually render this view (and all of its children) to the given Canvas.渲染View及其children的內容。
[3]中,分析了在setContentView,調用到了這些過程。
這裏有一個疑惑,若是是這以後已經調用了measure,那麼爲何在setContentView這個函數以後,獲取width和height,獲得0呢?
後來在[3]下面的評論裏,找到了[5],指出了[3]中微小的錯誤,專門分析了什麼時候繪製View。。
[5]對爲何onCreate沒有觸發這三個流程,再補充一篇[6]。
[6]給出告終論:
具體請參見[3]。
View中的measure方法是final的,子類不能覆蓋,這個方法裏面有一段調用了onMeasure方法,子類經過覆蓋onMeasure來實現本身的測量邏輯。
好比TextView本身實現的邏輯(這裏就不給了),好比FrameLayout本身實現的onMeasure。
FrameLayout是一個ViewGroup,它的onMeasure主要的任務是遍歷mChildren去measure。若是有match_parent屬性的children,從新設定MeasureSpec來measure。
調用的思路和邏輯基本和measure同樣。這裏也沒什麼好說的,直接拿來[3]的結論。
再次引用[3]的結論:
關於第6點,補充一下,官方文檔提到的,draw的順序是先序遍歷。
是時候回到咱們的設計模式上來了。
下面分析View的繪製是如何使用Composite設計模式的。首先,對於全部的View子類,它們都有一些公共的方法measure, layout, draw。不論是單個對象仍是組合對象,使用這些方法的邏輯是同樣的。就好像單個對象和組合對象是同樣的。其次,對於組合對象,這裏是ViewGroup,定義了接口ViewManager要實現。內部是一些組合對象須要擁有的方法,好比添加View,移除View。然而,既然View定義了measure, layout, draw爲final方法,那麼單個對象和組合對象不就沒有區別了嗎?組合對象又要怎麼調用孩子去doSomething呢?其實,在View中定義了onMeasure, onLayout, onDraw三個方法,在measure, layout, draw的調用過程當中,都會去調用對應的onXXX。這樣繼承View的單個對象實現本身的邏輯,繼承View的組合對象不只要實現本身的邏輯,還有實現對孩子們的調用。
View內部定義了三個方法,measure, layout, draw。這三個都作到了對擴展開發,對修改封閉。每一個View要有本身的measure,layout邏輯,該怎麼辦呢?
解決辦法就是View中的measure和layout去調用一個能夠覆蓋的方法onMeasure,onLayout。在本質上,onMeasure, onLayout, onDraw這三個方法的做用是擴展View。
ViewGroup中沒有具體的onMeasure和onLayout,一個Layout繼承ViewGroup,實現本身的onMeasure和onLayout。這樣就能夠定義出不少不一樣種類的layout結構,好比RelativeLayout,LinearLayout。相對佈局和線性佈局,它們都要有本身的layout邏輯,這些都放到onLayout中本身去定義。在定義本身的layout邏輯以外,還要負責調用孩子的measure方法,layout方法。
draw的邏輯有些許不同。View中定義了draw的邏輯,裏面有一些通用的邏輯,下面截取了View中的註釋。2~5步,若是須要就跳過。對於第4步,View定義了dispatchDraw的空方法,ViewGroup覆蓋它來實現調用孩子的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) */
Composite設計模式,核心要義在於,不論是單個對象,仍是組合對象,使用起來都同樣。
這篇博客分析了Android的View類,它的實現就是Composite設計模式。
設計模式,它是前人總結出來的打怪(解決問題)的套路。有些時候一直在使用一些套路,可是沒有意識到。因而有人總結出來,下一次遇到同一個問題的時候,類似的情景,用這個套路就能夠很好的切入問題。
這篇博客寫的又臭又長,不免存在錯誤,歡迎理性討論。若是能指正個人錯誤,那是我最大的榮幸。