文章目錄
1 View的框架
2 View的整體繪製流程
View的繪製都是由ViewRoot(對應 ViewRootImpl
)來完成,Window和DecorView通過ViewRootImpl關聯。
Activity對象被創建 -> DecorView被設置到Window中,創建ViewRootImpl關聯DecorView(間接關聯Window)-> 執行ViewRoot的 performTraversals()
:
-
performMeasure()
->measure()
先繪製DecorView,再執行onMeasure()
遍歷繪製ViewGroup和子View -
performLayout()
->layout()
先繪製DecorView,再執行onLayout()
遍歷繪製ViewGroup和子View -
performDraw()
->dispatchDraw()
先繪製DecorView,再執行onDraw()
遍歷繪製ViewGroup和子View
3 MeasureSpec
3.1 獲取測量大小和測量模式
在佈局過程中,經常會使用到 MeasureSpec
根據 onMeasure(int widthMeasureSpec, int heightMeasureSpec)
提供的兩個參數 widthMeasureSpec
和 heightMeasureSpec
獲取測量的寬高和測量模式,或者生成 widthMeasureSpec
和 heightMeasureSpec
。
其中,widthMeasureSpec
和 heightMeasureSpec
是一個32位的int值,高2位是測量模式specMode,低30位是測量大小specSize。
寬高和測量模式使用如下方法獲取和設置:
-
MeasureSpec.getMode(widthMeasureSpec/heightMeasureSpec):獲取測量模式specMode
-
MeasureSpec.getSize(widthMeasureSpec/heightMeasureSpec):獲取測量大小specSize
-
MeasureSpec.makeMeasureSpec(int size, int mode):根據自己設置的測量大小和測量模式生成一個
widthMeasureSpec
或heightMeasureSpec
3.2 三種測量模式
-
EXACTLY:精確值模式,對應將控件的
layout_width
屬性或layout_height
屬性指定爲具體數值時,比如android:layout_width=”100dp”
,或者指定爲match_parent
(View類的onMeasure()
默認的模式,所以需要指定wrap_content
就要重寫onMeasure()
) -
AT_MOST:最大值模式,對應將控件的
layout_width
屬性或layout_height
屬性指定爲wrap_content
時,此時控件的最大尺寸只要不超過父控件允許的最大尺寸即可 -
UNSPECIFIED:不指定寬高,要多大就多大,一般用在系統內部的measure,不會經常使用
4 佈局過程
4.1 佈局過程的含義
佈局過程,就是程序在運行時利用佈局文件的代碼來計算出實際尺寸的過程。
4.2 佈局過程的工作內容
兩個階段:測量階段和佈局階段。
-
測量階段:從上到下遞歸地調用每個View或者ViewGroup的
measure()
方法,測量它們的尺寸並計算它們的位置 -
佈局階段:從上到下調用每個View或者ViewGroup的
layout()
方法,把測得的它們的尺寸和位置賦值給它們
4.3 View或ViewGroup的佈局過程
-
測量階段:
measure()
方法被父View調用,在measure()
中做一些準備和優化工作後,調用onMeasure()
來進行實際的自我測量。onMeasure()
做的事,View和ViewGroup不一樣:-
View:View在
onMeasure()
中會計算出自己的尺寸然後保存 -
ViewGroup:ViewGroup在
onMeasure()
中會調用所有子View的measure()
讓它們進行自我測量,並根據子View計算出期望尺寸來計算出它們的實際尺寸和位置(實際上99%的父View都會使用子View繪製出的期望尺寸來作爲實際尺寸)然後保存。同時,它也會根據子View的尺寸和位置來計算出自己的尺寸然後保存
-
-
佈局階段:
layout()
方法被父View調用,在layout()
中它會保存父View傳進來的自己的位置和尺寸,並且調用onLayout()
來進行實際的內部佈局。onLayout()
做的事,View和ViewGroup也不一樣:-
View:由於沒有子View,所以View的
onLayout()
什麼也不做 -
ViewGroup:ViewGroup在
onLayout()
中會調用自己的所有子View的layout()
方法,把它們的尺寸和位置傳給它們,讓它們完成自我的內部佈局
-
4.4 佈局過程自定義的方式
有三種自定義方式:
-
重寫
onMeasure()
來修改已有的View的尺寸 -
重寫
onMeasure()
來全新定製自定義View的尺寸 -
重寫
onMeasure()
和onLayout()
來全新定製自定義ViewGroup的內部佈局
4.4.1 重寫onMeasure修改已有的View的尺寸
-
重寫
onMeasure()
方法,並在裏面調用super.onMeasure()
,觸發原有的自我測量 -
在
super.onMeasure()
的下面用getMeasureWidth()
和getMeasureHeight()
來獲取之前的測量結果,並使用自己的算法,根據測量結果計算出新的結果 -
調用
setMeasureDimension()
來保存新的結果
public class SquareImageView extends ImageView {
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 先執行原測量算法
// super.onMeasure()也是調用了setMeasureDimension()
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 獲取原先的測量結果
int measureWidth = getMeasureWidth();
int measureHeight = getMeasureHeight();
// 利用原先的測量結果計算出尺寸
int size = Math.min(measureWidth, measureHeight);
// 保存計算後的結果
setMeasureDimension(size, size);
}
// 如果重寫layout方法,你發現其實也是可以實現效果的,但是我們爲什麼不在這裏寫,要在onMeasure()呢?
// 因爲在onMeasure()的時候保存的測量尺寸在父View是可知的,父View會根據你測量的尺寸來佈局你的位置
// 但是在layout的時候父View就是不可知的了,這會導致當父View內有多個子View,你在layout又重寫了自己的大小
// 而父View仍舊按照你在onMeasure()時測量的尺寸來擺放你的位置,就會出現佈局錯誤
// @Override
// public void layout(int l, int t, int r, int b) {
// int width = r - l;
// int height = b - t;
// int size = Math.min(width, height);
// super.layout(l, t, l + size, t + size);
// }
}
4.4.2 重寫onMeasure()來全新定製自定義View的尺寸
-
重寫
onMeasure()
把尺寸計算出來 -
計算出自己的尺寸
-
用
resolveSize()
或者resolveSizeAndState()
修正結果 -
使用
setMeasuredDimension()
保存結果
public class CircleView extends View {
private static final float RADIUS = Utils.dpToPx(80);
private static final float PADDING = Utils.dpToPx(30);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
// onMeasure()方法的widthMeasureSpec和heightMeasureSpec有特殊的含義
// 從上面我們知道,子View的測量是父View發起measure()然後讓子View從上到下遞歸自測量
// 而widthMeasureSpec和heightMeasureSpec就是父View提供給子View的一個測量寬高最大限度
// widthMeasureSpec和heightMeasureSpec對於子View來說,就是處理自己在xml佈局設置的layout_width和layout_height
// 自定義子View最主要的就是處理wrap_content也就是MeasureSpec.AT_MOST的情況
// 在AT_MOST情況將自己的想要的大小和父view限定給我們的大小int size = MeasureSpec.getSize(measureSpec)對比符合預期
// AT_MOST自適應大小,如果想要的大小超過父view的大小就使用限定大小size,否則用我們自己的大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 自己定義的view寬高
int size (int) (PADDING + RADIUS) * 2);
// 將子View測量的寬高大小過濾一遍,讓寬高符合父View的要求
// resolveSize() sdk已經提供該方法
int width = resolveSize(measureWidth, widthMeasureSpec);
int height = resolveSize(measureHeight, heightMeasureSpec);
setMeasureDimension(width , height );
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.RED);
canvas.drawCircle(PADDING + RADIUS, PADDING + RADIUS, RADIUS, paint);
}
// 注:下面這段代碼已經簡化過,不是resoveSize()的源碼
// public static int resolveSize(int size, int measureSpec) {
// final int specMode = MeasureSpec.getMode(measureSpec);
// final int specSize = MeasureSpec.getSize(measureSpec);
// switch(specMode) {
// 父View提供的specMode爲不限定寬高,返回子View子測量寬高
// case MeasureSpec.UNSPECIFIED:
// return size;
// 父View提供的specMode爲自適應,需要特殊處理
// case MeasureSpec.AT_MOST:
// 如果子View測量寬高比父View提供最大限度的寬高要小,使用子View測量的寬高
// if (size <= specSize) {
// return size;
// }
// 否則使用父View提供的最大限度寬高
// else {
// return specSize;
// }
// 父View提供的specMode爲精確值,直接返回父View提供的寬高
// case MeasureSpec.EXACTLY:
// return specSize;
// default:
// return size;
// }
// }
//}
問題:getWidth()
、getHeight()
和 getMeasureWidth()
、getMeasureHeight()
的區別?
getWidth()
和 getHeight()
是確定了View的最終寬高,形成於layout過程;getMeasureWidth()
和 getMeasureHeight()
是確定了View的測量寬高,形成於measure過程。
一般情況下最終寬高和測量寬高几乎相同。除非重寫了layout方法導致最終寬高與測量寬高不同。
5 定製Layout的內部佈局
步驟:
-
重寫
onMeasure()
-
遍歷每個子View,用
measureChildWidthMargins()
測量子View-
MarginLayoutParams
和generateLayoutParams()
-
有些子View可能需要重新測量
-
測量完畢後,得出子View的實際位置和尺寸,並暫時保存
-
-
測量出所有子View的位置和尺寸後,計算出自己的尺寸,並用
setMeasureDimension()
保存
-
-
重寫
onLayout()
- 遍歷每個子View,調用它們的
layout()
來將位置和尺寸傳給它們
- 遍歷每個子View,調用它們的
public class TagLayout extends ViewGroup {
private List<Rect> childRectBounds = new ArrayList<>();
public TagLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthUsed = 0; // 已使用父View的寬度,按最大的子View寬度來就行了
int heightUsed = 0; // 已使用父View的高度,每一行最大子View高度的疊加和
int lineWidthUsed = 0; // 當前行的已用寬度
int lineHeight = 0; // 當前行最大子View的高度
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 根據父View的MeasureSpec限定大小以及子View的LayoutParams共同決定去測量子View的大小
// 這句代碼的具體實現要對照下表
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
// 父View的MeasureSpec不能是UNSPECIFIED無限大,當前已用寬度+下一個測量子View的寬度 > 父View的限定寬度,換行
if (widthMode != MeasureSpec.UNSPECIFIED && ((lineWidthUsed + child.getMeasuredWidth()) > widthSize)) {
// 當前行的已用寬度置0
lineWidthUsed = 0;
// 已使用父View的高度增加爲當前行的最大子View的高度
heightUsed += lineHeight;
// 重新計算
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
}
// 測量後保存子View的擺放位置,每一行因子View標籤文本不同而擺放數量不同
Rect childRect;
if (childRectBounds.size() <= i) {
childRect = new Rect();
childRectBounds.add(childRect);
} else {
childRect = childRectBounds.get(i);
}
// left:添加一個子View後,上一個子View的寬度就是下一個子View的起始位置
// top:其實就是每一行最大的View高度作爲下一行的起始高度
// right:left+當前View的寬度
// bottom:top+當前View的高度
childRect.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(),
heightUsed + child.getMeasuredHeight());
lineWidthUsed += child.getMeasuredWidth(); // 該行添加子View的寬度方便下一個子View的位置擺放計算
widthUsed = Math.max(lineWidthUsed, widthUsed);
lineHeight = Math.max(lineHeight, child.getMeasuredHeight()); // 該行最大子View高度,換行時使用
}
int measureWidth = widthUsed;
heightUsed += lineHeight;
int measureHeight = heightUsed;
setMeasuredDimension(measureWidth, measureHeight);
}
// 使用使用measureChildWithMargins()時,內部會獲取到childView的LayoutParams並將它強轉爲MarginLayoutParams
// 需要使用該方法轉換,否則拋出ClassCastException
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 只需要根據測量的結果循環擺放子View的位置即可
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
Rect rect = childRectBounds.get(i);
child.layout(rect.left, rect.top, rect.right, rect.bottom);
}
}
}
上面的計算方式可以通過一個表格來說明:
自定義ViewGroup的MeasureSpec與父容器的MeasureSpec和自身的LayoutParams決定:
-
子View是dp/dx(即固定寬高),不管父容器的MeasureSpec是什麼,View的MeasureSpec都是
EXACTLY
,大小遵循LayoutParams中的大小 -
子View是
match_parent
(即寬高爲match_parent
),如果父容器是EXACTLY
,子View也是EXACTLY
,如果父容器是AT_MOST
,子View也是AT_MOST
;大小是父容器的剩餘空間 -
子View是
wrap_content
(即寬高爲wrap_content
),不管父容器的MeasureSpec是什麼,子View的MeasureSpec都是AT_MOST
,大小不能超過父容器的剩餘空間
上面的規則其實就是通過一個方法 measureChildWidthMargins()
完成。
關於保存子View位置的兩點說明:
-
不是所有的Layout都需要保存子View的位置(因爲有的Layout可以在佈局階段實時推導出子View的位置,例如LinearLayout)
-
有時候對某些子View需要重複測量兩次或多次才能得到正確的尺寸和位置
6 自定義View注意事項
-
讓View支持wrap_content:如果是直接繼承View或ViewGroup,且自定義View的寬高有wrap_content,需要重寫onMeasure()進行處理
-
如果有必要,讓自定義View支持padding:如果是直接繼承View,要在
onDraw()
中處理padding(margin在父容器會處理),否則無法起作用;如果是直接繼承ViewGroup,要在onMeasure()
和onLayout()
中處理padding和margin,否則無法起作用;getPaddingLeft()
、getPaddingTop()
、getPaddingRight()
、getPaddingBottom()
獲取padding -
儘量不要在自定義View中使用Handler,沒必要:View內部提供了post方法可以替代Handler的作用,除非明確需要Handler發送消息
-
自定義View中如果有線程或動畫,要及時停止:如果自定義View中有線程或動畫,在
onDetachedFromWindow()
停止線程或動畫(Activity退出或當前View被remove時調用,onAttachedToWindow()相反);不及時處理容易造成內存泄漏 -
自定義View帶有滑動嵌套情形時,要處理滑動衝突