自定義控件系列:
秒懂OnMeasure
秒懂OnLayout
讓自定義ViewGroup裏的子控件支持Margin
讓自定義ViewGroup支持Padding
自定義ViewGroup的一個綜合實踐 FlowLayout
onDraw
最簡單的自定義View:SwitchView
我感覺之所以寫不好自定義view,是因爲我們瞭解的自定義View的基礎知識知道的太少,但是在瞭解自定義view的基礎知識的過程中,又很容易被源碼帶跑偏,找不到重點,結果是看了很多源碼,雲裏霧裏等於沒看。
很多時候,源碼是很重要,但是不懂適可而止的看源碼,你就陷入了汪洋大海。
例如:初中幾何里老師講了“兩點之間、直線最短”這個公理後,我們就可以做很多幾何題目了,做的過程中還很爽,但是老師沒講“兩點之間、直線最短”這個公理的源碼是什麼,爲什麼“兩點之間、直線最短”,你要想證明這個公理,對於初中生甚至大學生都是不可能解答的,但是這絲毫不影響一個初中生做幾何題目(當然,我記得老師說過,公理不需要證明)
所以這個系列博客採用知識點+應用的模式,有重點,有舉例
當這些結論性的知識點積累到足夠多,很多自定義view,不過就是多個結論的綜合應用+小小邏輯算法,我們怕的不是小小邏輯算法,再繞的算法,多試驗就出來了,但是不懂基本的結論性知識點,就很茫然了
知識點
關於MeasureSpec是什麼,不懂的朋友請先搜索一下,這裏對這個不做解釋。
-
如果你的自定義view的寬高只支持MeasureSpec.EXACTLY(即:match_parent和具體的數值),那麼onMeasure方法不需要重寫,因爲View這個基類已經默認實現了
-
如果你想支持MeasureSpec.AT_MOST(即:wrap_content),必須重寫onMeasure方法,不然你寫wrap_content和match_parent效果是一樣的(即系統默認返回一個父容器所能給予你的最大尺寸)。
想想爲什麼,View這個基類,不幫我們實現MeasureSpec.AT_MOST模式呢?
因爲不同的view,對於自己的MeasureSpec.AT_MOST(包裹內容)有自己特有的計算方式,例如:ImageView的MeasureSpec.AT_MOST,ImageView會根據你設置的圖片,來計算在wrap_content時候的寬高。
TextView的MeasureSpec.AT_MOST,TextView會根據你設置的文字內容多少,(因爲內容多了可能換行)和你設置的TextSize(字體大了,自然需要更大的寬高)來計算Textview在wrap_content時候的寬高
FrameLayout在MeasureSpec.AT_MOST模式下,寬度就是所有子view裏面最大的那個View的寬度,高度就是所有子view裏的最大的那個View的高度
LinearLayout在MeasureSpec.AT_MOST模式下(假設是豎向佈局),寬度是所有View裏最大的那個View的寬度,高度是所有View的高度的總和
所以View天生支持MeasureSpec.EXACTLY,但是他對於MeasureSpec.AT_MOST是無能爲力的,需要具體的View自己具體實現 -
最重要就是這個方法,計算出控件在各種模式下的寬高,通過這個方法設置進去,就好了
setMeasuredDimension(width, height);
以下就是View的onMeasure的默認實現(源碼)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST://關鍵在這裏系統對AT_MOST和EXACTLY的處理是一樣的,
//都是返回父容器的最大尺寸,不信你可以自己打印出來看看
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
應用
1. 給出支持MeasureSpec.AT_MOST的模板代碼
其實這個代碼是套路代碼,結構是不變的
當寫wrap_content,意思是父佈局不傳給你確定的尺寸,需要這個view自己確定個默認的尺寸,這個尺寸是你自己根據自己的情況計算出來的。
public class CustomView extends View {
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 首先計算一下在AtMost模式下,這個自定義view的寬高,
// 這裏把計算出來的寬高封裝在了一個Point裏,x爲寬,y爲高
// 這是模板代碼,你只需要按照自己的實現caculateAtMostSize()的具體邏輯,其餘的不用變
Point point = caculateAtMostSize(widthMeasureSpec,heightMeasureSpec);
// 根據 默認寬高、AtMost下的寬高、MeasureSpec測量規格,計算出最終這個view的寬高
int width = measureSize(0, point.x, widthMeasureSpec);
int height = measureSize(0, point.y, heightMeasureSpec);
// 把上面計算出來的寬高作爲參數設置給setMeasuredDimension就ok了
setMeasuredDimension(width, height);
}
/**
* 通過widthMeasureSpec計算出這個View最終的寬高
*
* @param defalut 這個view的默認值,僅僅是爲了支持下UNSPECIFIED模式,但是這個模式其實用不到
* @param atMostSize AT_MOST下的尺寸
* @param measureSpec 測量規格(包含了模式+尺寸)
* @return
*/
private int measureSize(int defalut, int atMostSize, int measureSpec) {
int result = defalut;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = defalut;
break;
case MeasureSpec.AT_MOST:
//在AT_MOST模式下,系統傳來的specSize是一個父容器所能容納的最大值,你這個自定義view計算的尺寸不能大於這個值
result = Math.min(atMostSize, specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
/**
* 計算本View在AtMost模式下的寬高
* 其他代碼都是不用動的,在這裏寫下你特有的邏輯就可以
* 我這裏只是簡單的返回寬高都是 200
*
* @return
*/
private Point caculateAtMostSize(int widthMeasureSpec, int heightMeasureSpec) {
//一般情況,寫的自定義view是不需要特別計算這個值的,我會直接給一個默認值
//但是你是自定義ViewGroup的話,這裏你必須好好寫了
int width = 200;
int height = 200;
return new Point(width, height);
}
2. 一個自定義ViewGroup支持MeasureSpec.AT_MOST的例子:
效果如圖:
對應的佈局文件時這樣的
注意:這裏僅僅寫支持AT_MOST的代碼,還沒寫onLayout,所以代碼運行是看不到效果的,可以打印log,來看下這個view的寬高是不是正確的
/**
* 計算本View在AtMost模式下的寬高
* 其他代碼都是不用動的,在這裏寫下你特有的邏輯就可以
*
* @param widthMeasureSpec
* @param heightMeasureSpec
* @return
*/
private Point caculateAtMostSize(int widthMeasureSpec, int heightMeasureSpec) {
int width = 0;
int height = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
// 測量一下子控件的寬高
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 獲得子控件的寬高
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 因爲我們的自定義View模擬的是豎向的LinearLayout,所以:
// 控件的寬度爲所有子控件裏,寬度最大的那個view的寬度,
// 控件高度是所有子空間的高度之和
width = Math.max(childWidth, width);
height += childHeight;
}
return new Point(width, height);
}
參考資料:
《Android羣英傳》