反思 系列博客是個人一種新學習方式的嘗試,該系列起源和目錄請參考 這裏 。html
Android
自己的View
體系很是宏大,源碼中值得思考和借鑑之處衆多,以View
自己的繪製流程爲例,其通過measure
測量、layout
佈局、draw
繪製三個過程,最終纔可以將其繪製出來並展現在用戶面前。android
相比 測量流程 ,佈局流程 相對簡單不少,若是讀者不瞭解 測量流程 ,建議閱讀這篇文章:git
反思 | Android View機制設計與實現:測量流程github
測量流程 的目的是 測量控件寬高 ,但只獲取控件的寬高其實是不夠的,對於ViewGroup
而言還須要一套額外的邏輯,負責對全部子控件進行對應策略的佈局,這就是 佈局流程(layout)。markdown
View
而言,其自己沒有子控件,所以通常狀況下僅須要記錄本身在父控件的位置信息,並不須要處理爲子控件佈局的邏輯;Android
中佈局流程中也使用了遞歸思想:對於一個完整的界面而言,每一個頁面都映射了一個View
樹,其最頂端的父控件開始佈局時,會經過自身的佈局策略依次計算出每一個子控件的位置——值得一提的是,爲了保證控件樹形結構的 內部自治性,每一個子控件的位置爲 相對於父控件座標系的相對位置 ,而不是以屏幕座標系爲準的絕對位置。位置計算完畢後,做爲參數交給子控件,令子控件開始佈局;如此往復一直到最底層的控件,當全部控件都佈局完畢,整個佈局流程結束。對於佈局流程不甚熟悉的開發者而言,上述文字彷佛晦澀難懂,但這些文字的歸納其本質倒是佈局流程總體的設計思想,讀者不該該將本文視爲源碼分析,而應該將本身代入到設計的過程當中 ,當深入理解整個流程的設計思路以後,佈局流程代碼地設計和編寫天然行雲流水一鼓作氣。app
首先思考一個問題,佈局流程的本質是測量結束以後,將每一個子控件分配到對應的位置上去——既然有子控件,那說明進行佈局流程的主體理應是ViewGroup
,那麼做爲葉子節點的單個View
來講,爲何也會有佈局流程呢?框架
讀者認真思考能夠得出,佈局流程其實是一個複雜的過程,整個流程主要邏輯順序以下:ide
onMeasure()
;整個佈局過程當中,除了4是ViewGroup
自身須要作的,其它邏輯對於View
和ViewGroup
而言都是公共的——這說明單個View
也是有佈局流程的需求的。函數
如今將整個佈局過程定義三個重要的函數,分別爲:oop
void layout(int l, int t, int r, int b)
:控件自身整個佈局流程的函數;void onLayout(boolean changed, int left, int top, int right, int bottom)
:ViewGroup佈局邏輯的函數,開發者須要本身實現自定義佈局邏輯;void setFrame(int left, int top, int right, int bottom)
:保存最新佈局位置信息的函數;爲何須要定義這樣三個函數?
如今咱們站在單個View
的角度,首先父控件須要經過調用子控件的layout()
函數,並同時將子控件的位置(left、right、top、bottom
)做爲參數傳入,標誌子控件自己佈局流程的開始:
// 僞代碼實現 public void layout(int l, int t, int r, int b) { // 1.決定是否須要從新進行測量流程(onMeasure) if(needMeasureBeforeLayout) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec) } // 先將以前的位置信息進行保存 int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; // 2.將自身所在的位置信息進行保存; // 3.判斷本次佈局流程是否引起了佈局的改變; boolean changed = setFrame(l, t, r, b); if (changed) { // 4.若佈局發生了改變,令全部子控件從新佈局; onLayout(changed, l, t, r, b); // 5.若佈局發生了改變,通知全部觀察佈局改變的監聽發送通知 mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } 複製代碼
這裏筆者經過僞代碼的方式對佈局流程進行了描述,實際上View
自己的layout()
函數內部雖然多處不一樣,但核心思想是一致的——layout()
函數實際上表明瞭控件自身佈局的整個流程,setFrame()
和onLayout()
函數都是layout()
中的一個步驟。
爲何須要保存佈局信息?由於咱們老是有獲取控件的寬和高的需求——好比接下來的onDraw()
繪製階段;而保存了佈局信息,就能經過這些值計算控件自己的寬高:
public final int getWidth() { return mWidth; } public final int getHeight() { return mHeight; } 複製代碼
因而可知,保存控件的佈局信息確實頗有必要,Android中將layout()
函數的四個參數所表明的位置信息,交給了setFrame()
函數去保存:
protected boolean setFrame(int left, int top, int right, int bottom) { // 佈局是否發生了改變 boolean changed = false; // 若最新的佈局信息和以前的佈局信息不一樣,則保存最新的佈局信息 if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { changed = true; mLeft = left; mTop = top; mRight = right; mBottom = bottom; } return changed; } 複製代碼
setFrame()
函數被protected
修飾,這意味着開發者能夠經過重寫該函數來定義View
自己保存佈局信息的邏輯,如今將目光轉到mLeft、mTop、mRight、mBottom
四個變量上。
顧名思義,這四個變量對應的天然是View
自身所在的位置,那麼View
是如何經過這四個變量描述控件的位置信息呢?
經過一張圖來看一下這四個變量所表明的意義:
這時候不可避免的會面臨另一個問題,這個mLeft、mTop、mRight、mBottom
的值所對應的座標系是哪裏呢?
這裏須要注意的是,爲了保證控件樹形結構的 內部自治性,每一個子控件的位置爲 相對於父控件座標系的相對位置 ,而不是以屏幕座標系爲準的絕對位置:
反過來想,若是這些位置信息是以屏幕座標系爲準,那麼就意味着每一個葉子節點的
View
會持有保存從根節點ViewGroup
直到自身父ViewGroup
每一個控件的位置信息,在計算佈局時則更爲繁瑣,很明顯是不合理的設計。
既然View
自身持有了這樣的位置信息,實際上前文中獲取控件自身寬高的getWidth()
和getHeight()
方法就能夠從新這樣定義:
public final int getWidth() { return mRight - mLeft; } public final int getHeight() { return mBottom - mTop; } 複製代碼
這也說明了在佈局流程中的setFrame()
函數執行完畢後(且佈局確實發生了改變),開發者才能經過getWidth()
和getHeight()
方法獲取控件正確的寬高值。
對於葉子節點的View
而言,其並無子控件,所以通常狀況下並無爲子控件佈局的意義(特殊狀況請參考AppCompatTextView
等類),所以View
的onLayout()
函數被設計爲一個空的實現:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { } 複製代碼
而在ViewGroup
中,不一樣類型的ViewGroup
有不一樣的佈局策略,這些佈局策略的邏輯各不相同,所以該方法被設計爲抽象接口,開發者必須實現這個方法以定義ViewGroup
的佈局策略:
@Override protected abstract void onLayout(boolean changed,int l, int t, int r, int b); 複製代碼
以
LinearLayout
爲例,其佈局策略爲 根據排布方向,將其全部子控件按照指定方向依次排列布局
至此單個View
的測量流程結束,關於ViewGroup
的onLayout
函數細節將在下文進行描述。
相比較測量流程,佈局流程相對比較簡單,總體思路是,對於一個完整的界面而言,每一個頁面都映射了一個View
樹,最頂端的父控件開始佈局時,會經過自身的佈局策略依次計算出每一個子控件的位置。位置計算完畢後,做爲參數交給子控件,令子控件開始佈局;如此往復一直到最底層的控件,當全部控件都佈局完畢,整個佈局流程結束。
ViewGroup
雖然重寫了View
的layout()
函數,但實質上並未進行大的變更,咱們大抵能夠認爲ViewGroup
和View
的layout()
邏輯一致:
@Override public final void layout(int l, int t, int r, int b) { if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) { if (mTransition != null) { mTransition.layoutChange(this); } // 仍然是執行View層的layout函數 super.layout(l, t, r, b); } else { mLayoutCalledWhileSuppressed = true; } } 複製代碼
惟一須要注意的是,開發者必須實現onLayout()
函數以定義ViewGroup
的佈局策略,這裏以 豎直佈局 的LinearLayout
的僞代碼爲例:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childTop; int childLeft; // 遍歷全部子View for (int i = 0; i < count; i++) { // 獲取子View final View child = getVirtualChildAt(i); // 獲取子View寬高,注意這裏使用的是 getMeasuredWidth 而不是 getWidth final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); // 令全部子控件開始佈局 setChildFrame(child, childLeft, childTop, childWidth, childHeight); // 高度累加,下一個子View的 top 就等於上一個子View的 bottom ,符合豎直線性佈局從上到下的佈局策略 childTop += childHeight; } } private void setChildFrame(View child, int left, int top, int width, int height) { // 這裏能夠看到,子控件的mRight實際上就是 mLeft + getMeasuredWidth() // 而在getWidth()函數中,mRight-mLeft的結果就是getMeasuredWidth() // 所以,getWidth() 和 getMeasuredWidth() 是一致的 child.layout(left, top, left + width, top + height); } 複製代碼
讀者須要注意到一個細節,子控件的寬度的獲取,咱們並未使用getWidth()
,而是使用了getMeasuredWidth()
,這就引起了另一個疑問,這兩個函數的區別在哪裏。
首先,從上文中咱們得知,getWidth()
和getHeight()
函數的相關信息其實是在setFrame()
函數執行完畢才準備完畢的——咱們大體能夠認爲是這兩個函數 只有佈局流程(layout)執行完畢才能調用,而在父控件的onLayout()
函數中,獲取子控件寬度和高度時,子控件還並未開始進行佈局流程,所以此時不能調用getWidth()
函數,而只能經過getMeasuredWidth()
函數獲取控件測量階段結果的寬度。
那麼當控件繪製流程執行完畢後,getWidth()
和getMeasuredWidth()
函數的值有什麼區別呢?從上述setChildFrame()
函數中的源碼能夠得知,佈局流程執行後,getWidth()
返回值的本質其實就是getMeasuredWidth()
——所以本質上,當咱們沒有手動調用layout()
函數強制修改控件的佈局信息的話,兩個函數的返回值大小是徹底一致的。
在整個佈局流程的設計中,設計者將流程中公共的業務邏輯(保存佈局信息、通知佈局發生改變的監聽等)經過layout()
函數進行了整合,同時,將ViewGroup
額外須要的自定義佈局策略經過onLayout()
函數向外暴露出來,針對組件中代碼的可複用性和可擴展性進行了合理的設計。
至此,佈局流程總體實現完畢。借用 carson_ho 繪製的流程圖對總體佈局流程作一個總結:
Hello,我是 卻把清梅嗅 ,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人 博客 或者 Github。
若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?