View 的繪制流程分為三步:measure(測量)、layout(布局)、draw(繪制)
measure是確定view的大小,layout是計算在界面中顯示的位置,draw便是最后的繪制步驟了。三者是先后執行的。
大致流程如下:
自定義 View 的第一步,肯定是明確的寬高,位置坐標,寬高是在測量階段得出。然后在布局階段,確定好位置信息,對矩形布局,之后的視覺效果就交給繪制流程了。
流程是很簡單的,但是實際的操作卻是很復雜的。
布局涉及兩個過程:測量過程和布局過程。測量過程通過 measure 方法實現,是 View 樹自頂向下的遍歷,每個 View 在循環過程中將尺寸細節往下傳遞,當測量過程完成之后,所有的 View 都存儲了自己的尺寸。第二個過程則是通過方法 layout 來實現的,也是自頂向下的。在這個過程中,每個父 View 負責通過計算好的尺寸放置它的子 View。
MeasureSpec
測量過程中,有一個很重要的類:MeasureSpec。MeasureSpec 是 View 中一個靜態類,代表測量規則,而它的手段則是用一個 int 數值來實現。我們知道一個 int 數值有 32 bit。MeasureSpec 將它的高 2 位用來代表測量模式 Mode,低 30 位用來代表數值大小 Size。
測量的尺寸好理解。說明下測量模式,測量模式可以取三個值,其含義如下:
子 View 在 xml 中的布局參數,對應的測量模式如下:
- wrap_content ---> MeasureSpec.AT_MOST
- match_parent -> MeasureSpec.EXACTLY
- 具體值 -> MeasureSpec.EXACTLY
對於 UNSPECIFIED 模式,一般的 View 不會用上,在滾動組件或者列表中可能會用上。而這部分屬於比較深入的內容了,此處我們不細講。
MeasureSpec 的源碼如下:
/**
* MeasureSpec類的源碼分析
**/
public class MeasureSpec {
// 進位大小 = 2的30次方
// int的大小為32位,所以進位30位 = 使用int的32和31位做標志位
private static final int MODE_SHIFT = 30;
// 運算遮罩:0x3為16進制,10進制為3,二進制為11
// 3向左進位30 = 11 00000000000(11后跟30個0)
// 作用:用1標注需要的值,0標注不要的值。因1與任何數做與運算都得任何數、0與任何數做與運算都得0
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// UNSPECIFIED的模式設置:0向左進位30 = 00后跟30個0,即00 00000000000
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// EXACTLY的模式設置:1向左進位30 = 01后跟30個0 ,即01 00000000000
public static final int EXACTLY = 1 << MODE_SHIFT;
// AT_MOST的模式設置:2向左進位30 = 10后跟30個0,即10 00000000000
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* makeMeasureSpec()方法
* 作用:根據提供的size和mode得到一個詳細的測量結果嗎,即measureSpec
**/
public static int makeMeasureSpec(int size, int mode) {
// 設計目的:使用一個32位的二進制數,其中:第32和第31位代表測量模式(mode)、后30位代表測量大小(size)
// ~ 表示按位取反
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
/**
* getMode()方法
* 作用:通過measureSpec獲得測量模式(mode)
**/
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
// 即:測量模式(mode) = measureSpec & MODE_MASK;
// MODE_MASK = 運算遮罩 = 11 00000000000(11后跟30個0)
//原理:保留measureSpec的高2位(即測量模式)、使用0替換后30位
// 例如10 00..00100 & 11 00..00(11后跟30個0) = 10 00..00(AT_MOST),這樣就得到了mode的值
}
/**
* getSize方法
* 作用:通過measureSpec獲得測量大小size
**/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
// size = measureSpec & ~MODE_MASK;
// 原理類似上面,即 將MODE_MASK取反,也就是變成了00 111111(00后跟30個1),將32,31替換成0也就是去掉mode,保留后30位的size
}
}
MeasureSpec 值是如何計算得來? 其實,子 View 的 MeasureSpec 值根據子 View 的布局參數(LayoutParams)和父容器的 MeasureSpec 值計算得來的,具體計算邏輯封裝在 ViewGroup 的 getChildMeasureSpec() 里。即子 view 的大小由父 view 的 MeasureSpec 值 和 子 view 自身的 LayoutParams 屬性共同決定。
下面,我們來看 getChildMeasureSpec() 的源碼分析:
/**
* 方法所在類:ViewGroup
* 參數說明
*
* @param spec 父 view 的詳細測量值(MeasureSpec)
* @param padding view 當前尺寸的的內邊距
* @param childDimension 子視圖的尺寸(寬/高),如果子 View 未測量完成,則該值為子 View 的布局參數。測量完成則是子 View 的尺寸
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//父view的測量模式
int specMode = MeasureSpec.getMode(spec);
//父view的大小
int specSize = MeasureSpec.getSize(spec);
//通過父view計算出的子view = 父大小-邊距(父要求的大小,但子view不一定用這個值)
int size = Math.max(0, specSize - padding);
//子view想要的實際大小和模式(需要計算)
int resultSize = 0;
int resultMode = 0;
// 當父 View 的模式為 EXACITY 時,父 View 強加給子 View 確切的值
//一般是父 View 設置為 match_parent 或者固定值的 ViewGroup
switch (specMode) {
case MeasureSpec.EXACTLY:
// 當子 View 測量完成,即有確切的值
// 子 View 大小為子自身所賦的值,模式大小為 EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 測量未完成
// 當子 View 的 LayoutParams 為 MATCH_PARENT 時(-1)
//子 view 大小為父 view 大小,模式為 EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 測量未完成
// 當子view的LayoutParams為WRAP_CONTENT時(-2)
// 子 view 決定自己的大小,但最大不能超過父 view,模式為 AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
// 當父 View 的模式為 AT_MOST 時,父 view 強加給子 View 一個最大的值。(一般是父 view 設置為 wrap_content)
// 代碼含義同上
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED:
// 當父 View 的模式為 UNSPECIFIED 時,父容器不對 View 有任何限制,要多大給多大
// 多見於 ListView、GridView
if (childDimension >= 0) {
// 子 view 大小為子自身所賦的值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 因為父 View 為 UNSPECIFIED,API 大於23時,可以傳遞 hint 值用於測量,詳見 View.sUseZeroUnspecifiedMeasureSpec 的賦值處。通常 resultSize 為 0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 說明同上
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上述流程很簡單,可以用下面的流程圖概括:
得到了 MeasureSpec,我們就可以講講繪制流程了。不過不同的組件繪制方式不同,View 和 ViewGroup 的繪制流程又不同,下面我們會挑幾個特例,講講 View 的 measure 和 ViewGroup 的 layout 過程。
View 的繪制流程
View 的繪制流程比較簡單,我們先了解。通常在實現自定義 View 時,我們會終點關注 measure 和 draw 過程,draw 過程比較復雜,暫時不涉及。
measure
我們自定義一個 View,關鍵方法是 measure,但 measure 方法是 final 的,我們不能繼承更改,但 measure 中使用了一個 onMeasure() 方法。onMeasure() 是一個關鍵方法,也是本文重點研究內容,是官方暴露出來給我們使用的。該方法會測量 View 自己的大小,為正式布局提供建議。(注意,只是建議,至於用不用,要看onLayout)。
View 的 onMeasure 方法是默認實現,此處跳過。下面我們重點說明一下 ImageView 的測量流程,明白了 ImageView 的測量過程,也就明白了如何通過測量模式得到最終尺寸,也就明白了測量模式是怎么一回事。首先我們明確一個方法:setMeasuredDimension
。使用該方法可以存儲測量出來的大小結果。
ImageView.onMeasure
代碼可以可能有點長,可以看看注釋:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 解析為 ImageView 自動的 uri,並更新用於顯示的 Drawable
resolveUri();
// View 測量的寬高
int w;
int h;
// View 顯示的內容的比例(不包括 padding)
float desiredAspect = 0.0f;
// 是否允許改變 View 的寬高
boolean resizeWidth = false;
boolean resizeHeight = false;
// 布局模式
final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (mDrawable == null) {
// 沒有可顯示的 Drawable,Drawable 的寬高設置為 -1,View 的寬高設置為 0
mDrawableWidth = -1;
mDrawableHeight = -1;
w = h = 0;
} else {
// 有可顯示的 Drawable,View 的寬高設置為 Drawable 的寬高
w = mDrawableWidth;
h = mDrawableHeight;
// 寬高進行過濾操作,最小為 1(防止 Drawable 異常)
if (w <= 0) w = 1;
if (h <= 0) h = 1;
// 是否需要根據 Drawable 的寬高比例更改 View 的范圍
if (mAdjustViewBounds) {
// View 的寬高的布局模式不為精准模式時,才能更改
resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
desiredAspect = (float) w / (float) h;
}
}
// 上下左右的 padding
final int pleft = mPaddingLeft;
final int pright = mPaddingRight;
final int ptop = mPaddingTop;
final int pbottom = mPaddingBottom;
int widthSize;
int heightSize;
if (resizeWidth || resizeHeight) {
// View 的寬高需要二次更改
// 首次測量,會根據最大尺寸獲取目標尺寸
widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec);
heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec);
// 比例不為 0 才二次測量,避免異常情況
if (desiredAspect != 0.0f) {
// 圖片實際的調整比例
final float actualAspect = (float)(widthSize - pleft - pright) /
(heightSize - ptop - pbottom);
// 實際的寬高比例和預期的寬高比例不等,需要重新調整尺寸
// 注意 float 的不等於的比較方式,!= 並不一定准確
if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {
boolean done = false;
// 需要調整寬度
if (resizeWidth) {
int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) +
pleft + pright;
// 此代碼 API 大於 17 時生效,否則不生效
if (!resizeHeight && !sCompatAdjustViewBounds) {
widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec);
}
// 新的尺寸小於等於原尺寸,才重新賦值。因為 width 前一次測量已經得到了最大的可能寬度
if (newWidth <= widthSize) {
widthSize = newWidth;
// 寬度按照 desiredAspect 比例改過,此時view是符合 desiredAspect 的。
done = true;
}
}
// 需要調整高度,寬度按照比例該過后,就不再更改了。=
if (!done && resizeHeight) {
int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) +
ptop + pbottom;
// 說明同上
if (!resizeWidth && !sCompatAdjustViewBounds) {
heightSize = resolveAdjustedSize(newHeight, mMaxHeight, heightMeasureSpec);
}
if (newHeight <= heightSize) {
heightSize = newHeight;
}
}
}
}
} else {
// View 的寬高不能更改,走正常的測量流程
// View 的寬高加上 padding
w += pleft + pright;
h += ptop + pbottom;
// 測量出來的寬高不能小於設置的 View 的最小值
// 該最小寬度由 minWidth(minHeight) 和背景 Drawable 決定
w = Math.max(w, getSuggestedMinimumWidth());
h = Math.max(h, getSuggestedMinimumHeight());
// 根據結果,獲取最終的值
widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
}
// 保存存儲的結果
setMeasuredDimension(widthSize, heightSize);
}
上面 ImageView 的測量流程其實很簡單,可以用下面的流程圖描述:
其實,上面的測量邏輯,還是很簡單的。並不復雜,主要是賦值過程的計算,是在當前的測量結果和限定值之間的取舍(自身設置的最大/最小值,父類給予的限定值)。而賦值過程的重點其實在 resolveAdjustedSize
這個方法。從代碼中的使用可以看出來,ImageView的寬高是在 測量值/自身最大值/父類限定值 三者間得出的。
// 值通過測量值(帶上padding)/自身設置的最大值/父類的布局要求,三者計算
widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec);
heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec);
下面我們來看看這個方法的具體實現,注意:可以看看這個方法的源碼,官方給出的注釋說明是 measure 過程的核心思想的體現。
/**
* 解析得到最終的結果
*
* @param desiredSize ImageView 自身測量出的尺寸
* @param maxSize ImageView 布局參數傳入的最大尺寸
* @param measureSpec 父布局對 ImageView 的測量要求
*/
private int resolveAdjustedSize(int desiredSize, int maxSize, int measureSpec) {
int result = desiredSize;
final int specMode = MeasureSpec.getMode(measureSpec);
// 父 View 對子 View 的尺寸限制
final int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
// 父布局對測量無限制,則用自身測量的尺寸,但不應該超過最大值
result = Math.min(desiredSize, maxSize);
break;
case MeasureSpec.AT_MOST:
// 父布局對測量規定了最大值,測量的結果可以盡可能的大,但是不能超過 specSize,
// 也不能超過自身規定的最大尺寸 maxSize。則在三者中取最小值
result = Math.min(Math.min(desiredSize, specSize), maxSize);
break;
case MeasureSpec.EXACTLY:
// 父布局對測量要求是精確的,沒得選,只能使用父布局傳入的值
result = specSize;
break;
}
return result;
}
講解了 ImageView 的 measure 過程,我們來看看 View Group 的布局過程。ViewGroup 的繪制和布局過程主要是對子 View 操作。可以理解成它並不太會關注自己的事,因為它是它父 View 的子 View,他的測量是在其父 View 中調用的,當然,會有一個根布局。這就是一個遞歸調用的過程。
按照流程,我們知道 View 的布局,最終會走到 onLayout 方法,此處就以 FrameLayout 為例,講解下布局操作。
FrameLayout.onLayout
按照慣例,先上源碼,再上圖。FrameLayout.onLayout 的主要代碼是在 layoutChildren 這個方法中,下面我們講講 layoutChildren 這個方法。
/**
* 布局 FrameLayout
*
* @param left 當前 ViewGroup 距父布局左邊界的距離
× @param top 當前 ViewGroup 距父布局上邊界的距離
* @param right 當前 ViewGroup 距父布局有邊界的距離
× @param bottom 當前 ViewGroup 距父布局下邊界的距離
* @param forceLeftGravity 暫未用上的參數
* */
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
// 獲得子 View 的數量
final int count = getChildCount();
// 當前布局的左側布局起點(加上左 padding)
final int parentLeft = getPaddingLeftWithForeground();
// 當前布局的右側布局終點(減去右 padding)
final int parentRight = right - left - getPaddingRightWithForeground();
// 當前布局的上側布局起點(加上上 padding)
final int parentTop = getPaddingTopWithForeground();
// 當前布局的下側布局終點(減去下 padding)
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
// 子 View 不設置為 GONE,才進行布局。即 GONE 屬性不占用任何空間
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 獲取測量出的尺寸,布局前已先測量
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
// 對齊方式默認是左上角
gravity = DEFAULT_CHILD_GRAVITY;
}
// 獲取布局方向,RTL 還是 LTR
final int layoutDirection = getLayoutDirection();
// 獲取水平方向上的對齊方式
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
// 獲取豎直方向上的對齊樣式
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
// 先判斷水平方向上的對齊方式
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
// 子 View 居中
case Gravity.CENTER_HORIZONTAL:
// 參見代碼下的圖,有助於理解
// parentRight - parentLeft - width 可以簡單的理解為左右 margin 和
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
// 子 View 右對齊
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
// 子 View 左對齊
case Gravity.LEFT:
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;
}
// 計算出了子 View 的位置,布局子 View(即調用子 View 的布局方法)
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
上面的代碼,結合圖片便能很輕松的理解。就不詳講了。此處聊點其他的---子 View 的對齊方式哪里來的?從上面的代碼中,可以看出,是從 View 的布局參數中取的,而 View 的布局參數是怎么來的呢?
要想了解布局參數怎么來的,我們就得首先了解下系統是怎么向一個 ViewGroup 中添加 View 的。我們知道,一個界面的布局,我們通常是在 xml 中設計的。而在所有的 ViewGroup 中,我們都可以加入子 View,並為子 View 加入約束。即 加入 View ---> 加入約束的流程。此處加入約束的流程便是加入布局參數的流程。布局參數是子 View 告訴父 View 自己如何布局的途徑。
讓我們來看看加入 View 的流程,向 ViewGroup 中加入 View 是調用了 ViewGroup 的 addView 方法,addView 有好幾個同名方法。我們來看看。
ViewGroup.addView
我們在 ViewGroup 的源碼中搜索,會首先搜索到一個單參的 addView 方法。
// 很簡單,點擊進入雙參的方法
public void addView(View child) {
addView(child, -1);
}
public void addView(View child, int index) {
// 待添加的 View 不能為空
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// 獲取布局參數
LayoutParams params = child.getLayoutParams();
if (params == null) {
// 取布局參數為空,則生成默認的布局參數
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
雙參的方法,其實很簡單。主要就是做了兩個限制:
- 加入 ViewGroup 的 View 不能為空
- View 如果是無布局參數,會生成一個默認的。如果無法生成默認的布局參數,則會拋異常,無法加入 ViewGroup 中。
即待加入 ViewGroup 中的 View,不能為空,且布局參數不能為空。從上面的代碼中,我們可以知道,加入約束,是在加入 View 的過程中便加入了。下面讓我們來看看 ViewGroup 的 generateDefaultLayoutParams 方法。
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
上面代碼中的 LayoutParams 是何方神聖?其實 LayoutParams 是 ViewGroup 的一個公共內部類,它描述了 ViewGroup 的子 View 的尺寸(寬/高)。而它還有個子類:MarginLayoutParams。顧名思義,其是在 LayoutParams 加入了子 View 的 margin 描述。ViewGroup 中的子類就這兩個了。也沒有看到對齊方式的相關描述呀?不急,LayoutParams 旁邊是有箭頭的
點擊箭頭,我們找到了熟悉的身影---FrameLayout。讓我們點擊進去看看。
public static class LayoutParams extends MarginLayoutParams {
public static final int UNSPECIFIED_GRAVITY = -1;
@InspectableProperty(name = "layout_gravity",
valueType = InspectableProperty.ValueType.GRAVITY)
public int gravity = UNSPECIFIED_GRAVITY;
public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
super(c, attrs);
final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FrameLayout_Layout);
gravity = a.getInt(R.styleable.FrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY);
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(int width, int height, int gravity) {
super(width, height);
this.gravity = gravity;
}
public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(@NonNull ViewGroup.MarginLayoutParams source) {
super(source);
}
// 此處的 LayoutParams 是FrameLayout 中的,不是 ViewGroup 中的
public LayoutParams(@NonNull LayoutParams source) {
super(source);
this.gravity = source.gravity;
}
}
上面便是 FrameLayout 中的 LayoutParams,我們發現其實際上是繼承自 MarginLayoutParams 的。在 Margin 的基礎上,增加了對齊方式的描述。實際上,每一個繼承自 ViewGroup 的容器類,如果想要實現自己的布局規則,都必須照着這個模版,先在 LayoutParams 中定義自己的布局參數,再在 onLayout 方法中定義自己的規則。每個容器類都是照着這個模版來的。
可以看出,如何精進自己,最好的方式還是閱讀源碼。但是,閱讀源碼也要有條件。
- 你會用了。再去讀源碼,了解為什么要這樣。否則就很容易事倍功半,效果奇差。
- 帶着目的讀源碼,比如我這次,就是為了了解 View 的繪制流程,才找了很簡單的兩個官方實現:ImageView 和 FrameLayout。讀了源碼,一下子就明白了 measure 和 layout 在干什么,以及怎么干。
實際上,上面的 LayoutParam部分,已屬於自定義 ViewGroup 的內容了。這里算是小試牛刀,拋磚引玉。下一講,我們來講講如何自定義 View 和 ViewGroup。