1.作用
計算視圖(View
)的位置
即計算
View
的四個頂點位置:Left
、Top
、Right
、Bottom
2.layout過程詳解
類似measure
過程,layout
過程根據View
的類型分爲2種情況:
3.1單一View的layout過程
- 應用場景:在無現成的控件
View
滿足需求、需要自己實現時,則使用自定義單一View
- 具體使用:繼承自
View
、SurfaceView
、或其他View
;不包含子View
- 具體流程
- 源碼分析
layout
過程的入口=layout()
,具體如下:
/**
* 源碼分析:layout()
* 作用:確定View本身的位置,即設置View本身的四個頂點位置
*/
public void layout(int l, int t, int r, int b) {
// 當前視圖的四個頂點
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// 1. 確定View的位置:setFrame() / setOpticalFrame()
// 即初始化四個頂點的值、判斷當前View大小和位置是否發生了變化 & 返回
// ->>分析1、分析2
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 2. 若視圖的大小 & 位置發生變化
// 會重新確定該View所有的子View在父容器的位置:onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
// 對於單一View的laytou過程:由於單一View是沒有子View的,故onLayout()是一個空實現->>分析3
// 對於ViewGroup的laytou過程:由於確定位置與具體佈局有關,所以onLayout()在ViewGroup爲1個抽象方法,需重寫實現(後面會詳細說)
...
}
/**
* 分析1:setFrame()
* 作用:根據傳入的4個位置值,設置View本身的四個頂點位置
* 即:最終確定View本身的位置
*/
protected boolean setFrame(int left, int top, int right, int bottom) {
...
// 通過以下賦值語句記錄下了視圖的位置信息,即確定View的四個頂點
// 從而確定了視圖的位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}
/**
* 分析2:setOpticalFrame()
* 作用:根據傳入的4個位置值,設置View本身的四個頂點位置
* 即:最終確定View本身的位置
*/
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets();
// 內部實際上是調用setFrame()
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
// 回到調用原處
/**
* 分析3:onLayout()
* 注:對於單一View的laytou過程
* a. 由於單一View是沒有子View的,故onLayout()是一個空實現
* b. 由於在layout()中已經對自身View進行了位置計算,所以單一View的layout過程在layout()後就已完成了
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 參數說明
// changed 當前View的大小和位置改變了
// left 左部位置
// top 頂部位置
// right 右部位置
// bottom 底部位置
}
至此,單一View
的layout
過程已分析完畢。
-
總結
單一View
的layout
過程解析如下:
3.2 ViewGroup的layout過程
-
應用場景
利用現有的組件根據特定的佈局方式來組成新的組件 -
具體使用
繼承自ViewGroup
或各種Layout
;含有子View
-
原理(步驟)
- 計算自身
ViewGroup
的位置:layout()
- 遍歷子
View
&確定自身子View
在ViewGroup
的位置(調用子View
的layout()
):onLayout()
a. 步驟 2 類似於 單一
View
的layout
過程
b. 自上而下、一層層地傳遞下去,直到完成整個View
樹的layout()
過程
- 流程
此處需注意:
ViewGroup
和View
同樣擁有layout()
和onLayout()
,但二者不同的: - 一開始計算
ViewGroup
位置時,調用的是ViewGroup
的layout()
和onLayout()
; - 當開始遍歷子
View
&計算子View
位置時,調用的是子View
的layout()
和onLayout()
類似於單一
View
的layout
過程
- 下面進行詳細分析,
layout
過程入口爲layout()
/**
* 源碼分析:layout()
* 作用:確定View本身的位置,即設置View本身的四個頂點位置
* 注:與單一View的layout()源碼一致
*/
public void layout(int l, int t, int r, int b) {
// 當前視圖的四個頂點
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// 1. 確定View的位置:setFrame() / setOpticalFrame()
// 即初始化四個頂點的值、判斷當前View大小和位置是否發生了變化 & 返回
// ->>分析1、分析2
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 2. 若視圖的大小 & 位置發生變化
// 會重新確定該View所有的子View在父容器的位置:onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
// 對於單一View的laytou過程:由於單一View是沒有子View的,故onLayout()是一個空實現(上面已分析完畢)
// 對於ViewGroup的laytou過程:由於確定位置與具體佈局有關,所以onLayout()在ViewGroup爲1個抽象方法,需重寫實現 ->>分析3
...
}
/**
* 分析1:setFrame()
* 作用:確定View本身的位置,即設置View本身的四個頂點位置
*/
protected boolean setFrame(int left, int top, int right, int bottom) {
...
// 通過以下賦值語句記錄下了視圖的位置信息,即確定View的四個頂點
// 從而確定了視圖的位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}
/**
* 分析2:setOpticalFrame()
* 作用:確定View本身的位置,即設置View本身的四個頂點位置
*/
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets();
// 內部實際上是調用setFrame()
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
// 回到調用原處
/**
* 分析3:onLayout()
* 作用:計算該ViewGroup包含所有的子View在父容器的位置()
* 注:
* a. 定義爲抽象方法,需重寫,因:子View的確定位置與具體佈局有關,所以onLayout()在ViewGroup沒有實現
* b. 在自定義ViewGroup時必須複寫onLayout()!!!!!
* c. 複寫原理:遍歷子View 、計算當前子View的四個位置值 & 確定自身子View的位置(調用子View layout())
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 參數說明
// changed 當前View的大小和位置改變了
// left 左部位置
// top 頂部位置
// right 右部位置
// bottom 底部位置
// 1. 遍歷子View:循環所有子View
for (int i=0; i<getChildCount(); i++) {
View child = getChildAt(i);
// 2. 計算當前子View的四個位置值
// 2.1 位置的計算邏輯
...// 需自己實現,也是自定義View的關鍵
// 2.2 對計算後的位置值進行賦值
int mLeft = Left
int mTop = Top
int mRight = Right
int mBottom = Bottom
// 3. 根據上述4個位置的計算值,設置子View的4個頂點:調用子view的layout() & 傳遞計算過的參數
// 即確定了子View在父容器的位置
child.layout(mLeft, mTop, mRight, mBottom);
// 該過程類似於單一View的layout過程中的layout()和onLayout(),此處不作過多描述
}
}
}
總結
對於ViewGroup
的layout
過程,如下:
4. 實例講解
- 爲了更好理解
ViewGroup
的layout
過程(特別是複寫onLayout()
) - 下面,我將用2個實例來加深對
ViewGroup layout
過程的理解
- 系統提供的
ViewGroup
的子類:LinearLayout
- 自定義
View
(繼承了ViewGroup
)
4.1 實例解析1(LinearLayout)
4.1.1原理
- 計算出LinearLayout本身在父佈局的位置
- 計算出LinearLayout中所有子View在容器中的位置
4.1.2 具體流程
** 4.1.3 源碼分析**
- 在上述流程中,對於
LinearLayout
的layout()
的實現與上面所說是一樣的,此處不做過多闡述 - 故直接進入
LinearLayout
複寫的onLayout()
分析
/**
* 源碼分析:LinearLayout複寫的onLayout()
* 注:複寫的邏輯 和 LinearLayout measure過程的 onMeasure()類似
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 根據自身方向屬性,而選擇不同的處理方式
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
// 由於垂直 / 水平方向類似,所以此處僅分析垂直方向(Vertical)的處理過程 ->>分析1
/**
* 分析1:layoutVertical(l, t, r, b)
*/
void layoutVertical(int left, int top, int right, int bottom) {
// 子View的數量
final int count = getVirtualChildCount();
// 1. 遍歷子View
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
// 2. 計算子View的測量寬 / 高值
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
// 3. 確定自身子View的位置
// 即:遞歸調用子View的setChildFrame(),實際上是調用了子View的layout() ->>分析2
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
// childTop逐漸增大,即後面的子元素會被放置在靠下的位置
// 這符合垂直方向的LinearLayout的特性
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
/**
* 分析2:setChildFrame()
*/
private void setChildFrame( View child, int left, int top, int width, int height){
// setChildFrame()僅僅只是調用了子View的layout()而已
child.layout(left, top, left ++ width, top + height);
}
// 在子View的layout()又通過調用setFrame()確定View的四個頂點
// 即確定了子View的位置
// 如此不斷循環確定所有子View的位置,最終確定ViewGroup的位置
4.2 實例解析2:自定義View
- 上面講的例子是系統提供的、已經封裝好的
ViewGroup
子類:LinearLayout
- 但是,一般來說我們使用的都是自定義
View
; - 接下來,我用一個簡單的例子講解下自定義
View
的layout()
過程
實例視圖說明
實例視圖=1個ViewGroup
(灰色視圖),包含1個黃色的子View,如下圖:
4.2.2 原理
- 計算出
ViewGroup
在父佈局的位置 - 計算出
ViewGroup
中子View
在容器中的位置
4.2.3 具體計算邏輯
- 具體計算邏輯是指計算子
View
的位置,即計算四頂點位置=計算Left
、Top
、Right
和Bottom
- 主要是寫在複寫的
onLayout()
- 計算公式如下:
r = Left + width + Left;// 因左右間距一樣
b = Top + height + Top;// 因上下間距一樣
Left = (r - width) / 2;
Top = (b - height) / 2;
Right = width + Left;
Bottom = height + Top;
4.2.3代碼分析
因爲其餘方法同上,這裏不做過多描述,這裏只分析複寫onLayout()
/**
* 源碼分析:LinearLayout複寫的onLayout()
* 注:複寫的邏輯 和 LinearLayout measure過程的 onMeasure()類似
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 參數說明
// changed 當前View的大小和位置改變了
// left 左部位置
// top 頂部位置
// right 右部位置
// bottom 底部位置
// 1. 遍歷子View:循環所有子View
// 注:本例中其實只有一個
for (int i=0; i<getChildCount(); i++) {
View child = getChildAt(i);
// 取出當前子View寬 / 高
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
// 2. 計算當前子View的四個位置值
// 2.1 位置的計算邏輯
int mLeft = (r - width) / 2;
int mTop = (b - height) / 2;
int mRight = mLeft + width;
int mBottom = mTop + height;
// 3. 根據上述4個位置的計算值,設置子View的4個頂點
// 即確定了子View在父容器的位置
child.layout(mLeft, mTop, mRight,mBottom);
}
}
}
佈局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<scut.Demo_ViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#eee998"
tools:context="scut.carson_ho.layout_demo.MainActivity">
<Button
android:text="ChildView"
android:layout_width="200dip"
android:layout_height="200dip"
android:background="#333444"
android:id="@+id/ChildView" />
</scut.Demo_ViewGroup >
5.細節:getWidth()(getHeight())與getMeasuredWidth()(getMeasuredHeight())獲取的寬(高)有什麼區別?
首先明確定義:
getWidth()
/getHeight()
:獲得View
最終的寬/高getMeasuredWidth()
/getMeasuredHeight()
:獲得View
測量的寬/高
先看下各自得源碼:
// 獲得View測量的寬 / 高
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
// measure過程中返回的mMeasuredWidth
}
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
// measure過程中返回的mMeasuredHeight
}
// 獲得View最終的寬 / 高
public final int getWidth() {
return mRight - mLeft;
// View最終的寬 = 子View的右邊界 - 子view的左邊界。
}
public final int getHeight() {
return mBottom - mTop;
// View最終的高 = 子View的下邊界 - 子view的上邊界。
}
二者的區別:
上面標紅:一般情況下,二者獲取的寬高是相等的。那麼,"非一般"情況是什麼?
答:認爲設置:通過重寫View
的layout()
強行設置
@Override
public void layout( int l , int t, int r , int b){
// 改變傳入的頂點位置參數
super.layout(l,t,r+100,b+100);
// 如此一來,在任何情況下,getWidth() / getHeight()獲得的寬/高 總比 getMeasuredWidth() / getMeasuredHeight()獲取的寬/高大100px
// 即:View的最終寬/高 總比 測量寬/高 大100px
}
雖然這樣的人爲設置無實際意義,但證明了View
的最終寬高與測量寬高是可以不一樣的。
6. 總結
- 本文主要講解了自定義
View
中的Layout
過程,總結如下: