概述
Android中View框架的工作機制中,主要有三個過程:
1、View樹的測量(measure) Android View框架的measure機制
2、View樹的布局(layout)Android View框架的layout機制
3、View樹的繪制(draw)Android View框架的draw機制
View框架的工作流程為:測量每個View大小(measure)-->把每個View放置到相應的位置(layout)-->繪制每個View(draw)。
本文主要講述三大流程中的layout過程。不清楚measure過程的,可以參考這篇文章 Android View框架的measure機制。
帶着問題來思考整個layout過程。
1、系統為什么要有layout過程?
View框架在經過第一步的measure過程后,成功計算了每一個View的尺寸。但是要成功的把View繪制到屏幕上,只有View的尺寸還不行,還需要准確的知道該View應該被繪制到什么位置。除此之外,對一個ViewGroup而言,還需要根據自己特定的layout規則,來正確的計算出子View的繪制位置,已達到正確的layout目的。這也就是layout過程的職責。
該位置是View相對於父布局坐標系的相對位置,而不是以屏幕坐標系為准的絕對位置。這樣更容易保持樹型結構的遞歸性和內部自治性。而View的位置,可以無限大,超出當前ViewGroup的可視范圍,這也是通過改變View位置而實現滑動效果的原理。
2、layout過程都干了點什么事?
由於View是以樹結構進行存儲,所以典型的數據操作就是遞歸操作,所以,View框架中,采用了內部自治的layout過程。
每個葉子節點根據父節點傳遞過來的位置信息,設置自己的位置數據,每個非葉子節點,除了負責根據父節點傳遞過來的位置信息,設置自己的位置數據外(如果有父節點的話),還需要根據自己內部的layout規則(比如垂直排布等),計算出每一個子節點的位置信息,然后向子節點傳遞layout過程。
對於ViewGroup,除了根據自己的parent傳遞的位置信息,來設置自己的位置之外,還需要根據自己的layout規則,為每一個子View計算出准確的位置(相對於子View的父布局的位置)。
對於View,根據自己的parent傳遞的位置信息,來設置自己的位置。
View對象的位置信息,在內部是以4個成員變量的保存的,分別是mLeft、mRight、mTop、mBottom。他們的含義如圖所示。
源代碼分析
在View的源代碼中,提取到了下面一些關於layout過程的信息。
我們知道,整棵View樹的根節點是DecorView,它是一個FrameLayout,所以它是一個ViewGroup,所以整棵View樹的測量是從一個ViewGroup對象的layout方法開始的。
View:
1、layout
/**
分配一個位置信息到一個View上面,每個parent會調用children的layout方法來設置children的位置。最好不要覆寫該方法,有children的viewGroup,應該覆寫onLayout方法
*/
public void layout(int l, int t, int r, int b) ;
源代碼:
這里不給出,如果有興趣,自行查閱SDK。
偽代碼:
public void layout(int l, int t, int r, int b) { if (根據一些flag,發現需要進一步measure) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); } //暫存舊的位置信息 int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; //設置新的位置信息 mLeft = l; mTop = t; mBottom = b; mRight = r; if (layout改變了 || 需要layout) { onLayout(changed, l, t, r, b); //回調layoutChange事件 for (遍歷監聽對象) { listener.onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } 標記為已經執行過layout; }
2、onLayout
/** 根據布局規則,計算每一個子View的位置,View類默認是空實現。 所以這里沒有源代碼*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom);
ViewGroup:
ViewGroup中,只需要覆寫onLayout方法,來計算出每一個子View的位置,並且把layout流程傳遞給子View。
源代碼:
ViewGroup沒有實現,具體可以參考LinearLayout和RelativeLayout的onLayout方法。雖然各個具體實現都很復雜,但是基本流程是一樣的,可以參考下面的偽代碼。
偽代碼:
protected void onLayout(boolean changed, int l, int t, int r, int b) { for (遍歷子View) { /** 根據如下數據計算。 1、自己當前布局規則。比如垂直排放或者水平排放。 2、子View的測量尺寸。 3、子View在所有子View中的位置。比如位置索引,第一個或者第二個等。 */ 計算每一個子View的位置信息; child.layout(上面計算出來的位置信息); } }
結論
一般來說,自定義View,如果該View不包含子View,類似於TextView這種的,是不需要覆寫onLayout方法的。而含有子View的,比如LinearLayout這種,就需要根據自己的布局規則,來計算每一個子View的位置。
動手操作
下面我們自己寫一個自定義的ViewGroup,讓它內部的每一個子View都垂直排布,並且讓每一個子View的左邊界都距離上一個子View的左邊界一定的距離,大概看起來如下圖所示:
實際運行效果如下:
代碼如下:
public class VerticalOffsetLayout extends ViewGroup { private static final int OFFSET = 100; public VerticalOffsetLayout(Context context) { super(context); } public VerticalOffsetLayout(Context context, AttributeSet attrs) { super(context, attrs); } public VerticalOffsetLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width = 0; int height = 0; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); ViewGroup.LayoutParams lp = child.getLayoutParams(); int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width); int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height); child.measure(childWidthSpec, childHeightSpec); } switch (widthMode) { case MeasureSpec.EXACTLY: width = widthSize; break; case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: for (int i = 0; i < childCount; i++) { View child = getChildAt(i); int widthAddOffset = i * OFFSET + child.getMeasuredWidth(); width = Math.max(width, widthAddOffset); } break; default: break; } switch (heightMode) { case MeasureSpec.EXACTLY: height = heightSize; break; case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: for (int i = 0; i < childCount; i++) { View child = getChildAt(i); height = height + child.getMeasuredHeight(); } break; default: break; } setMeasuredDimension(width, height); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int left = 0; int right = 0; int top = 0; int bottom = 0; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); left = i * OFFSET; right = left + child.getMeasuredWidth(); bottom = top + child.getMeasuredHeight(); child.layout(left, top, right, bottom); top += child.getMeasuredHeight(); } } }