1.3.2.3自定義View Layout過程
目錄
1. 作用
計算視圖(View)
的位置
即計算
View
的四個頂點位置:Left
、Top
、Right
和Bottom
2. 知識儲備
具體請看文章:(1)自定義View基礎 - 最易懂的自定義View原理系列
3. layout過程詳解
類似measure
過程,layout
過程根據View的類型分爲2種情況:
接下來,我將詳細分析這2種情況下的layout
過程
3.1 單一View的layout過程
- 應用場景
在無現成的控件View
滿足需求、需自己實現時,則使用自定義單一View
- 如:製作一個支持加載網絡圖片的
ImageView
控件- 注:自定義
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
如:底部導航條中的條目,一般都是上圖標(ImageView)、下文字(TextView),那麼這兩個就可以用自定義ViewGroup組合成爲一個Veiw,提供兩個屬性分別用來設置文字和圖片,使用起來會更加方便。
- 原理(步驟)
- 計算自身
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過程,如下:
示意圖
此處需注意:ViewGroup
和 View
同樣擁有layout()
和onLayout()
,但二者不同的:
- 一開始計算
ViewGroup
位置時,調用的是ViewGroup
的layout()
和onLayout()
; - 當開始遍歷子
View
& 計算子View
位置時,調用的是子View
的layout()
和onLayout()
類似於單一
View
的layout
過程
至此,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.2 源碼分析
- 在上述流程中,對於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()
過程
4.2.1 實例視圖說明
實例視圖 = 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 = mLeft + width;
// 3. 根據上述4個位置的計算值,設置子View的4個頂點
// 即確定了子View在父容器的位置
child.layout(mLeft, mTop, mRight,mBottom);
}
}
}
佈局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<scut.carson_ho.layout_demo.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.carson_ho.layout_demo.Demo_ViewGroup >
- 效果圖
好了,你是不是發現,粘了我的代碼但是畫不出來?!(如下圖)
因爲我還沒說draw流程啊哈哈哈!
draw流程:將
View
最終繪製出來
layout()
過程講到這裏講完了,接下來我將繼續將自定義View
的最後一個流程draw
流程,有興趣就繼續關注我啦啦!!
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
的最終寬 / 高 與 測量寬 / 高是可以不一樣
特別注意
網上流傳這麼一個原因描述:
- 實際上在當屏幕可包裹內容時,他們的值是相等的;
- 只有當view超出屏幕後,才能看出他們的區別:getMeasuredWidth()是實際View的大小,與屏幕無關,而getHeight的大小此時則是屏幕的大小。當超出屏幕後getMeasuredWidth()等於getWidth()加上屏幕之外沒有顯示的大小
這個結論是錯的!詳細請點擊文章
結論
在非人爲設置的情況下,View
的最終寬/高(getWidth()
/ getHeight()
)
與 View
的測量寬/高 (getMeasuredWidth()
/ getMeasuredHeight()
)永遠是相等
6. 總結
- 本文主要講解了自定義
View
中的Layout
過程,總結如下:
原文地址:原文地址:https://www.jianshu.com/p/158736a2549d
下一節:1.3.2.4自定義View 繪畫過程