onMeasure、onLayout 能夠說是自定 View 的核心,可是不少開發者都沒能理解其含義與做用,也不理解 onMeasure 、 xml 指定大小這兩者的關係與差別,也不能區分 getMeasureWidth 與 getWidth 的本質區別又是什麼。本文將經過理論加實踐的方法帶領你們深刻理解 onMeasure 、onLayout 的定義、流程、具體使用方法與須要注意的細節。php
對於每個 View:java
運行前,開發者會根據本身的需求在 xml 文件中寫下對於 View 大小的指望值android
在運行的時候,父 View 會在 onMeaure()
中,根據開發者在 xml 中寫的對子 View 的要求, 和自身的實際可用空間,得出對於子 View 的具體尺寸要求git
子 View 在本身的 onMeasure
中,根據 xml 中指定的指望值和自身特色(指 View 的定義者在onMeasrue
中的聲明)算出本身的**指望*github
若是是 ViewGroup 還會在
onMeasure
中,調用每一個子 View 的 measure () 進行測量.面試
父 View 在子 View 計算出指望尺寸後,得出⼦ View 的實際尺⼨和位置canvas
⼦ View 在本身的 layout() ⽅法中將父 View 傳進來的實際尺寸和位置保存app
若是是 ViewGroup,還會在 onLayout() ⾥調用每一個字 View 的 layout() 把它們的尺寸 置傳給它們框架
measure 的測量過程可能不止一次,好比有三個子 View 在一個 ViewGroup 裏面,ViewGroup 的寬度是 warp_content,A 的寬度是 match_parent, B 和 C 是 warp_content, 此時 ViewGroup 的寬度是不固定的,怎麼肯定 A 的 match_parent 到底有多大呢?此時是如何測量的呢?ide
以 LinearLayout 爲例:第一次測量 LinearLayout 的大小也是沒有肯定的,因此沒法肯定 A 的 match_parent 到底有多大,這時候的 LinearLayout 會對 A 直接測量爲 0 ,而後測量 B、C 的寬度,由於 B、C 的大小是包裹內容的,在測量後就能夠肯定 LinearLayout 的寬度了:即爲最長的 B 的寬度。
這時候再對 A 進行第二次測量,直接設置爲與 LinearLayout 相同的寬度,至此達到了 match_parent 的效果。
若是將 measure 和 layout 的過程糅合在一塊兒,會致使兩次測量的時候進行無用的 layout,消耗了更多的資源,因此爲了性能,將其兩者分開。
也是兩者的職責相互獨立,分爲兩個過程,可使流程、代碼更加清晰。
上面例子中的狀況僅僅存在於 LinearLayout中,每種佈局的測量機制是不一樣的。那麼若是 A B C 三個 View 都是 match_parent LinearLayout 是如何作的呢?
第二輪測量:都沒有大小,LinearLayout 會讓全部子 View 自由測量(父 View 不限制寬度)。每一個測量以後都會變爲和最寬的同樣的寬度。
注意:
onMeasure 與 measure() 、onDraw 與 draw 的區別
onXX 方法是調度過程,而 measure、draw 纔是真正作事情的。能夠從源碼中看到 measure 中調用了 onMeasure 方法。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// ……………
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
// ………………
}
}
複製代碼
爲何不把對於尺寸的要求直接交個子 View 而是要交給父 View 呢?
由於有些場景子 View 的大小須要父 View 進行規劃,例如上面的例子中 LinearLayout 的子 View 設置了 weight。
##onMeasure 方法
要明確的一個問題是: 何時須要咱們本身實現 onMeasure 方法呢?
答:具體開發的時候有如下三種場景:
onMeasure()
和 onLayout()
方法。例以下文中的「綜合演練 —— 自定義 Layout」onLayout 方法是 ViewGroup 中用於控制子 View 位置的方法。放置子 View 位置的過程很簡單,只需重寫 onLayout 方法,而後獲取子 View 的實例,調用子 View 的 layout 方法實現佈局。在實際開發中,通常要配合 onMeasure 測量方法一塊兒使用。在下文「綜合演練 —— 自定義 Layout」中會詳細演示。
/** * 自定義正方形 ImageView * * Created by im_dsd on 2019-08-24 */
public class SquareImageView extends android.support.v7.widget.AppCompatImageView {
public SquareImageView(Context context) {
super(context);
}
public SquareImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SquareImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void layout(int l, int t, int r, int b) {
// 使用寬高的最大值設置邊長
int width = r - l;
int height = b - t;
int size = Math.max(width, height);
super.layout(l, t, l + size, t + size);
}
}
複製代碼
代碼很簡單,獲取寬與高的最大值用於設置正方形 View 的邊長。再看一下佈局文件的設置
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" tools:context=".MainActivity">
<com.example.dsd.demo.ui.custom.measure.SquareImageView android:background="@color/colorAccent" android:layout_width="200dp" android:layout_height="300dp"/>
<View android:background="@android:color/holo_blue_bright" android:layout_width="200dp" android:layout_height="200dp"/>
</LinearLayout>
複製代碼
經過佈局文件的描述若是是普通的 View 顯示的狀態應該是這樣的
而咱們期待的狀態應該是這樣的:SquareImageView 的寬高均爲 300dp。
可是最終的結果倒是下圖,雖然咱們使用了 LinearLayout 可是咱們經過layout()
方法改變了 SquareImageView 的大小,對於這個變化LinearLayout 並不知道,因此會發生布局重疊的問題。可見通常狀況下不要使用 layout()
方法。
onMeasure
方法更改尺寸。@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure 中已經完成了 View 的測量
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 獲取測量的結果比較後得出最大值
int height = getMeasuredHeight();
int width = getMeasuredWidth();
int size = Math.max(width, height);
// 將結果設置回去
setMeasuredDimension(size, size);
}
複製代碼
總結
簡單來講,更改已有 View 的尺寸主要分爲如下步驟
onMeasure()
getMeasureWidth
和 getMeasureHeight()
獲取測量尺寸setMeasuredDimension(width, height)
把結果保存此處用繪製圓形的 CircleView 作一個例子。對於這個 View 的指望是:View 的大小有內部的圓決定。
首先畫一個圓形看看
/** * 自定義 View 簡單測量 * Created by im_dsd on 2019-08-15 */
public class CircleView extends View {
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/** * 爲了方便簡單,固定尺寸 */
private static final float PADDING = DisplayUtils.dp2px(20);
private static final float RADIUS = DisplayUtils.dp2px(80);
public CircleView(Context context) {
super(context);
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(Color.RED);
canvas.drawCircle(PADDING + RADIUS, PADDING + RADIUS, RADIUS, mPaint);
}
}
複製代碼
<com.example.dsd.demo.ui.custom.layout.CircleView
android:background="@android:color/background_dark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
複製代碼
此時將大小設置爲 wrap_content 包裹布局,結果會是怎麼樣的呢?
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 沒有必要再讓 view 本身測量一遍了,浪費資源
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 計算指望的 size
int size = (int) ((PADDING + RADIUS) * 2);
// 獲取父 View 傳遞來的可用大小
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 開始計算
int result = 0;
switch (widthMode) {
// 不超過
case MeasureSpec.AT_MOST:
// 在 AT_MOST 模式下,取兩者的最小值
if (widthSize < size) {
result = widthSize;
} else {
result = size;
}
break;
// 精準的
case MeasureSpec.EXACTLY:
// 父 View 給多少用多少
result = widthSize;
break;
// 無限大,沒有指定大小
case MeasureSpec.UNSPECIFIED:
// 使用計算出的大小
result = size;
break;
default:
result = 0;
break;
}
// 設置大小
setMeasuredDimension(result, result);
}
複製代碼
上面的代碼就是 onMeasure(int,int)
的模板代碼了,要注意一點的是須要註釋 super.onMeasure
方法,此處面試的時候廣泛會問。
// 沒有必要再讓 view 本身測量一遍了,浪費資源
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
複製代碼
這段模版代碼其實 Android SDK 裏面早就有了很好的封裝 : resolveSize(int size, int measureSpec)
和 resolveSizeAndState(int size, int measureSpec, int childMeasuredState)
,兩行代碼直接搞定。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 沒有必要再讓 view 本身測量一遍了,浪費資源
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 計算指望的 size
int size = (int) ((PADDING + RADIUS) * 2);
// 指按期望的 size
int width = resolveSize(size, widthMeasureSpec);
int height = resolveSize(size, heightMeasureSpec);
// 設置大小
setMeasuredDimension(width, height);
}
複製代碼
使用的時候徹底能夠這樣作,可是很是建議你們都本身手寫幾遍理解其中的含義,由於面試會問到其中的細節。
還有一點很遺憾,就是 resolveSizeAndState(int, int, int)
很差用。很差用的緣由不是方法有問題,而是不少自定義 View 包括原生的 View 都沒有使用 resolveSizeAndState(int, int, int)
方法,或者沒用指定 sate (state 傳遞父 View 對於子 View 的指望,相比resolveSize(int, in)
方法對於子 View 的控制更好)因此就算設置了,也不會起做用。
總結
徹底自定義 View 的尺寸主要分爲如下步驟:
onMeasure()
resolveSize()
或者 resolveSizeAndState()
修正結果setMeasuredDimension(width, height)
保存結果以 TagLayout 爲例一步一步實現一個自定義 Layout。具體指望的效果以下圖:
onLayout()
在繼承 ViewGroup 的時候 onLayout()
是必需要實現的,這意味着子 View 的位置擺放的規則,所有交由開發者定義。
/** * 自定義 Layout Demo * * Created by im_dsd on 2019-08-11 */
public class TagLayout extends ViewGroup {
public TagLayout(Context context) {
super(context);
}
public TagLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TagLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 此時全部的子 View 都和 TagLayout 同樣大
child.layout(l, t, r, b);
}
}
}
複製代碼
實驗一下是否和指望的效果同樣呢
<?xml version="1.0" encoding="utf-8"?>
<com.example.dsd.demo.ui.custom.layout.TagLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dp" android:padding="5dp" android:background="#ffee00" android:textSize="16sp" android:textStyle="bold" android:text="音樂" />
</com.example.dsd.demo.ui.custom.layout.TagLayout>
複製代碼
的確和指望一致。若是想要 TextView 顯示爲 TagLayout 的四分之一呢?
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); // 子 View 顯示爲 TagLayout 的 1/4 child.layout(l, t, r / 4, b / 4); } } 複製代碼
效果達成!!!很明顯onLayout
能夠很是靈活的控制 View 的位置
再嘗試讓兩個 View 呈對角線佈局呢?
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (i == 0 ){ child.layout(0, 0, (r - l) / 2, (b - t) / 2); } else { child.layout((r - l) / 2, (b - t) / 2, (r - l), (b - t)); } } } 複製代碼
onLayout
的方法仍是很簡單的,可是在真正佈局中怎麼獲取 View 的位置纔是難點!如何獲取呢,這時候就須要 onMeasure
的幫助了!
在寫具體的代碼以前,先來搭建大致的框架。主要的思路就是在 onMeasure()
方法中計算好子 View 的尺寸和位置信息包括 TagLayout 的具體尺寸,而後在onLayout()
方法中擺放子 View。
在計算過程當中涉及到三個難點,具體請看註釋
private List<Rect> mChildRectList = new ArrayList<>();
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 沒有必要讓 View 本身算了,浪費資源。
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 難點1: 計算出對於每一個子 View 的尺寸
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
// 難點2:計算出每個子 View 的位置並保存。
Rect rect = new Rect(?, ?, ?, ?);
mChildRectList.add(rect);
}
// 難點3:根據全部子 View 的尺寸計算出 TagLayout 的尺寸
int measureWidth = ?;
int measureHeight = ?;
setMeasuredDimension(measureWidth, measureHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mChildRectList.size() == 0) {
return;
}
for (int i = 0; i < getChildCount(); i++) {
if (mChildRectList.size() <= i) {
return;
}
View child = getChildAt(i);
// 經過保存好的位置,設置子 View
Rect rect = mChildRectList.get(i);
child.layout(rect.left, rect.top, rect.right, rect.bottom);
}
}
複製代碼
主要涉及兩點:開發者對於子 View 的尺寸設置和父 View 的具體可用空間。獲取開發者對於子 View 尺寸的設置就比較簡單了:
// 獲取開發者對於子 View 尺寸的設置
LayoutParams layoutParams = child.getLayoutParams();
int width = layoutParams.width;
int height = layoutParams.height;
複製代碼
獲取父 View (TagLayout) 的可用空間要結合兩點:
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// TagLayout 已經使用過的空間,此處的計算是個難點,此處不是本例子重點,一下子討論
int widthUseSize = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 獲取開發者對於子 View 尺寸的設置
LayoutParams layoutParams = child.getLayoutParams();
int childWidthMode;
int childWidthSize;
// 獲取父 View 具體的可用空間
switch (layoutParams.width) {
// 若是說子 View 被開發者設置爲 match_parent
case LayoutParams.MATCH_PARENT:
switch (widthMode) {
case MeasureSpec.EXACTLY:
// TagLayout 爲 EXACTLY 模式下,子 View 能夠填充的部位就是 TagLayout 的可用空間
case MeasureSpec.AT_MOST:
// TagLayout 爲 AT_MOST 模式下有一個最大可用空間,子 View 要是想 match_parent 實際上是和
// EXACTLY 模式同樣的
childWidthMode = MeasureSpec.EXACTLY;
childWidthSize = widthSize - widthUseSize;
break;
case MeasureSpec.UNSPECIFIED:
// 當 TagLayout 爲 UNSPECIFIED 不限制尺寸的時候,意味着可用空間無限大!空間無限大還想
// match_parent 兩者徹底是悖論,因此咱們也要將子 View 的 mode 指定爲 UNSPECIFIED
childWidthMode = MeasureSpec.UNSPECIFIED;
// 此時 size 已經沒有做用了,寫 0 就能夠了
childWidthSize = 0;
break;
}
case LayoutParams.WRAP_CONTENT:
break;
default:
// 具體設置的尺寸
break;
}
// 獲取 measureSpec
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, childWidthMode);
複製代碼
補充一下何時會是 UNSPECIFIED 模式呢?好比說橫向或縱向滑動的 ScrollView,他的寬度或者高度的模式就是 UNSPECIFIED
僞代碼僅僅模擬了開發者將子 View 的 size 設置爲 match_parent 的狀況,其餘的狀況讀者要是感興趣能夠本身分析一下。筆者就不作過多的分析了!由於 Android SDK 早就爲咱們提供好了可用的 API: measureChildWithMargins(int, int, int, int)
一句話就完成了對於子 View 的測量。
有了 measureChildWithMargins
方法,對於子 View 的測量就很簡單啦。 一口氣解決難點 2 3。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int lineHeightUsed = 0;
int lineWidthUsed = 0;
int widthUsed = 0;
int heightUsed = 0;
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 測量子 View 尺寸。TagLayout 的子 view 是能夠換行的,因此設置 widthUsed 參數爲 0
// 讓子 View 的尺寸不會受到擠壓。
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
if (widthMode != MeasureSpec.UNSPECIFIED && lineWidthUsed + child.getMeasuredWidth() > widthSize) {
// 須要換行了
lineWidthUsed = 0;
heightUsed += lineHeightUsed;
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
}
Rect childBound;
if (mChildRectList.size() >= i) {
// 不存在則建立
childBound = new Rect();
mChildRectList.add(childBound);
} else {
childBound = mChildRectList.get(i);
}
// 存儲 child 位置信息
childBound.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(),
heightUsed + child.getMeasuredHeight());
// 更新位置信息
lineWidthUsed += child.getMeasuredWidth();
// 獲取一行中最大的尺寸
lineHeightUsed = Math.max(lineHeightUsed, child.getMeasuredHeight());
widthUsed = Math.max(lineWidthUsed, widthUsed);
}
// 使用的寬度和高度就是 TagLayout 的寬高啦
heightUsed += lineHeightUsed;
setMeasuredDimension(widthUsed, heightUsed);
}
複製代碼
終於寫完代碼啦,運行起來瞧瞧看。
居然奔潰了!經過日誌能夠定位到是
// 對於子 View 的測量
measureChildWithMargins(child, widthMeasureSpec, widthUsed,
heightMeasureSpec, heightUsed);
複製代碼
這一句出了問題,經過源碼得知measureChildWithMargins
方法會有一個類型轉換致使了崩潰
protected void measureChildWithMargins(int, int ,int, int) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
………………
}
複製代碼
解決辦法就是在 TagLayout 中重寫方法 generateLayoutParams(AttributeSet)
返回一個 MarginLayoutParams 就能夠解決問題了。
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
複製代碼
再次運行達到最終目標!
總結
自定義 Layout 的主要步驟分爲如下幾點:
onMeasure()
measureChildWidthMargins()
測量 View
setMeasuredDimension(width, height)
保存onLayout()
getMeasureXX 表明的是 onMeasure 方法結束後(準確的說應該是測量結束後)測量的值,而 getXX 表明的是 layout 階段 right - left、bottom - top 的真實顯示值,因此第一個不一樣點就是賦值的階段不一樣,可見 getXXX 在 layout() 以前一直爲 0, 而 getMeasureXX 可能不是最終值( onMeasure 可能會被調用屢次),可是最終的時候兩者的數值都會是相同的。使用那個還須要看具體的場景。
總結: getMeasureXX 獲取的是臨時的值,而 getXX 獲取的時候最終定稿的值,通常在繪製階段、觸摸反饋階段使用 getXXX,在 onMeasure 階段被迫使用 getMeasureXX 。
個人 Android 知識體系,歡迎 Star https://github.com/daishengda2018/AndroidKnowledgeSystem