自定義View Layout過程
前言
- 自定義
View
是Android
開發者必須瞭解的基礎 - 網上有大量關於自定義
View
原理的文章,但存在一些問題:內容不全、思路不清晰、無原始碼分析、簡單問題複雜化 等 - 今天,我將全面總結自定義View原理中的
Layout
過程,我能保證這是市面上的最全面、最清晰、最易懂的
目錄
1. 作用
計算檢視(View)
的位置
即計算
View
的四個頂點位置:Left
、Top
、Right
和Bottom
2. 知識儲備
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
過程,總結如下:
請幫頂 / 評論點贊!因為你們的贊同/鼓勵是我寫作的最大動力!
相關推薦
自定義View Layout過程
前言 自定義View是Android開發者必須瞭解的基礎 網上有大量關於自定義View原理的文章,但存在一些問題:內容不全、思路不清晰、無原始碼分析、簡單問題複雜化 等 今天,我將全面總結自定義View原理中的Layout過程,我能保證這是市面上的最全面、
自定義view佈局過程詳解
佈局過程,就是程式在執行時利用佈局檔案的程式碼來計算出實際尺寸的過程。 佈局分為兩個階段:測量階段和佈局階段。 測量階段:從上到下遞迴地呼叫每個 View 或者 ViewGroup 的 measure() 方法,測量他們的尺寸並計算它們的位置; 佈局階段:從上到下遞迴地呼叫每個 View 或
Android自定義View-Layout原理篇
Android自定義View通常需要經過measure、layout和draw過程,如果你沒有了解過measure過程,可以先看看這篇文章。 一、Layout的作用:計算檢視的位置,即Left、Top、Right、Bottom四點的位置 二、layout過程:跟measu
自定義View Draw過程- 最易懂的自定義View原理系列(4)
前言 自定義View是Android開發者必須瞭解的基礎 網上有大量關於自定義View原理的文章,但存在一些問題:內容不全、思路不清晰、無原始碼分析、簡單問題複雜化 等 今天,我將全面總結自定義View原理中的Draw過程,我能保證這是市面上的最全面、最清
自定義View原理篇(2)- layout過程
1. 簡介 View的繪製過程分為三部分:measure、layout、draw。 measure用來測量View的寬和高。 layout用來計算View的位置。 draw用來繪製View。 經過measure之後就進入了layout過
自定義View來畫大轉盤進行抽獎的過程
package com.example.app_bingtu_work; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import andro
自定義View:View的Measure測量過程解析
相信絕大多數Android開發者都有自定義View來滿足各種各樣需求的經歷,也知道一個View的繪製展示要經過measure、layout、draw三大流程,三者中measure的過程相比是稍微複雜一點點的。這篇文章作為一個Android基礎的分享,分享
自定義View:測量measure,佈局layout,繪製draw
1. 什麼是View 在Android的官方文件中是這樣描述的:表示了使用者介面的基本構建模組。一個View佔用了螢幕上的一個矩形區域並且負責介面繪製和事件處理。 手機螢幕上所有看得見摸得著的都是View。這一點對所有圖形系統來說都一樣,例如ios的UIVi
Android中View的繪製過程 onMeasure方法簡述 附有自定義View例子
/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use thi
Android-自定義View前傳-View的三大流程-Layout
defs llc 由於 就是 ready views spec protect wro Android-自定義View前傳-View的三大流程-Layout 參考 《Android開發藝術探索》 https://github.com/hongyangAndroid/Flo
【自定義view系列】View的measure過程
View的測量過程是三大流程中最複雜的。 在現實生活中,如果我們要去畫一個圖形,就必須知道他的大小和位置。測量(測量view的寬和高),知道view的大小。 一.LayoutParams LayoutParams繼承於Android.View.
Android 自定義View
wid declare created odi lex getwidth 實現 tdi led 最近在看鴻洋大神的博客,在看到自定義部分View部分時,想到之前案子中經常會要用到"圖片 + 文字"這類控件,如下圖所示: 之前的做法是在布局文件中,將一個Imag
2.Border Layout 自定義一個Layout來完成布局。
log 自定義 min int size ger 官方文檔 implement for 目標: 1.每一個被添加到布局裏的控件都是QLayoutItem,我們根據方位添加。 2.定義一個結構體 ItemWrapper。裏面包含QLayoutItem
自定義VIew方法
bili change 鍵盤 boolean eve 失去 nat finish bool onFinishInflate() 回調方法,當應用從XML加載該組件並用它構建界面之後調用的方法 onMeasure() 檢測View組件及其子組件的大小 onLayout() 當
自定義View總結
class net lin 定義 img view mage .net 技術分享 寫的很好,代你分析原碼,關於 View Measure 測量機制,讓我一次把話說完 自定義View總結
Android零基礎入門第24節:自定義View簡單使用
子類 protect jin 討論 我們 @+ amp 進階 運行程序 當我們開發中遇到Android原生的組件無法滿足需求時,這時候就應該自定義View來滿足這些特殊的組件需求。 一、概述 很多初入Android開發的程序員,對於Android自定義View可能比較
簡單自定義VIEW報錯問題
nfc 定義 http dnf androi dem and .com android aNDROIDNFC%E8%AF%BB%E5%8D%A1%E5%99%A8%E7%9A%84DEMO http://music.baidu.com/songlist/495819911
Android自定義view詳解
this boolean mar 處理 都是 並且 jdk text 命名 從繼承開始 懂點面向對象語言知識的都知道:封裝,繼承和多態,這是面向對象的三個基本特征,所以在自定義View的時候,最簡單的方法就是繼承現有的View 通過上面這段代碼,我定義了一個Ske
Android -- 自定義view實現keep歡迎頁倒計時效果
super onfinish -m use new getc awt ttr alt 1,最近打開keep的app的時候,發現它的歡迎頁面的倒計時效果還不錯,所以打算自己來寫寫,然後就有了這篇文章。 2,還是老規矩,先看一下我們今天實現的效果 相較於我們常見的倒計時
Android自定義View效果目錄
class 重寫 自定義 textview 居中 url 冒泡 and 雷達圖 1、絢麗的loading動效的實現 2、Android自定義View:進度條+冒泡文本 3、Android雷達圖(蜘蛛網圖) 4、Android文本閃爍 5、Android繪制圓形進度條 6、重