在 Android 系統中,界面中全部能看到的元素都是 View。默認狀況下,Android 系統爲開發者提供了不少 View,好比用於展現文本信息的 TextView,用於展現圖片的 ImageView 等等。但有時,這並不能知足開發者的需求,例如,開發者想要用一個餅狀圖來展現一組數據,這時若是用系統提供的 View 就不能實現了,只能經過自定義 View 來實現。那到底什麼是自定義 View 呢?android
自定義 View 就是經過繼承 View 或者 View 的子類,並在新的類裏面實現相應的處理邏輯(重寫相應的方法),以達到本身想要的效果。面試
Android 中的全部 UI 元素都是 View 的子類:canvas
PS:因爲涉及的類太多,若是將全部涉及到的類所有加到類圖裏面,類圖將十分大,因此此處只列出了 View 的直接子類。設計模式
Android View 體系以下:bash
仔細觀察,你會發現,Android View 的體系結構和設計模式中的組合模式的結構一模一樣:app
Android View 體系結構中的 ViewGroup 對應於組合模式中抽象構件(Component 和 Composite),Android View 體系結構中的 View 對應於組合模式中的葉子構件(Leaf):ide
Android View 構件 | Composite Pattern 構件 |
---|---|
ViewGroup | Component、Composite |
View | Leaf |
大多數狀況下,開發者經常會由於下面四個緣由去自定義 View:函數
默認狀況下,Android 系統爲開發者提供了不少控件,但有時,這並不能知足開發者的需求。例如,開發者想要用一個餅狀圖來展現一組數據,這時若是用系統提供的 View 就不能實現了,只能經過自定義 View 來實現。佈局
If none of the prebuilt widgets or layouts meets your needs, you can create your own View subclass.post
默認狀況下,Android 系統爲開發者提供的控件都有屬於它們本身的特定的交互方式,但有時,控件的默認交互方式並不能知足開發者的需求。例如,開發者想要縮放 ImageView 中的圖片內容,這時若是用系統提供的 ImageView 就不能實現了,只能經過自定義 ImageView 來實現。
有時,有些佈局若是用系統提供的控件實現起來至關複雜,須要各類嵌套,雖然最終也能實現了想要的效果,但性能極差,此時就能夠經過自定義 View 來減小嵌套層級、優化佈局。
有些控件可能在多個地方使用,如大多數 App 裏面的底部 Tab,像這樣的常常被用到的控件就能夠經過自定義 View 將它們封裝起來,以便在多個地方使用。
在說「如何自定義 View?」以前,咱們須要知道「自定義 View 都包括哪些內容」?
自定義 View 包括三部份內容:
佈局階段:肯定 View 的位置和尺寸。
繪製階段:繪製 View 的內容。
觸摸反饋:肯定用戶點擊了哪裏。
其中佈局階段包括測量(measure)和佈局(layout)兩個過程,另外,佈局階段是爲繪製和觸摸反饋階段作支持的,它並無什麼直接做用。正是由於在佈局階段肯定了 View 的尺寸和位置,繪製階段才知道往哪裏繪製,觸摸反饋階段才知道用戶點的是哪裏。
另外,因爲觸摸反饋是一個大的話題,限於篇幅,就不在這裏講解了,後面有機會的話,我會再補上一篇關於觸摸反饋的文章。
在自定義 View 和自定義 ViewGroup 中,佈局和繪製流程雖然總體上都是同樣的,但在細節方面,自定義 View 和自定義 ViewGroup 仍是不同的,因此,接下來分兩類進行討論:
「自定義 View 佈局、繪製」主要包括三個階段:
在 View 的測量階段會執行兩個方法(在測量階段,View 的父 View 會經過調用 View 的 measure() 方法將父 View 對 View 尺寸要求傳進來。緊接着 View 的 measure() 方法會作一些前置和優化工做,而後調用 View 的 onMeasure() 方法,並經過 onMeasure() 方法將父 View 對 View 的尺寸要求傳入。在自定義 View 中,只有須要修改 View 的尺寸的時候才須要重寫 onMeasure() 方法。在 onMeasure() 方法中根據業務需求進行相應的邏輯處理,並在最後經過調用 setMeasuredDimension() 方法告知父 View 本身的指望尺寸):
measure() : 調度方法,主要作一些前置和優化工做,並最終會調用 onMeasure() 方法執行實際的測量工做;
onMeasure() : 實際執行測量任務的方法,主要用與測量 View 尺寸和位置。在自定義 View 的 onMeasure() 方法中,View 根據本身的特性和父 View 對本身的尺寸要求算出本身的指望尺寸,並經過 setMeasuredDimension() 方法告知父 View 本身的指望尺寸。
onMeasure() 計算 View 指望尺寸方法以下:
參考父 View 的對 View 的尺寸要求和實際業務需求計算出 View 的指望尺寸:
經過 setMeasuredDimension() 保存 View 的指望尺寸(其實是經過 setMeasuredDimension() 告知父 View 本身的指望尺寸);
注意:
多數狀況下,這裏的指望尺寸就是 View 的最終尺寸。不過最終 View 的指望尺寸和實際尺寸是否是同樣還要看它的父 View 會不會贊成。View 的父 View 最終會經過調用 View 的 layout() 方法告知 View 的實際尺寸,而且在 layout() 方法中 View 須要將這個實際尺寸保存下來,以便繪製階段和觸摸反饋階段使用,這也是 View 須要在 layout() 方法中保存本身實際尺寸的緣由——由於繪製階段和觸摸反饋階段要使用啊!
在 View 的佈局階段會執行兩個方法(在佈局階段,View 的父 View 會經過調用 View 的 layout() 方法將 View 的實際尺寸(父 View 根據 View 的指望尺寸肯定的 View 的實際尺寸)傳給 View,View 須要在 layout() 方法中將本身的實際尺寸保存(經過調用 View 的 setFrame() 方法保存,在 setFrame() 方法中,又會經過調用 onSizeChanged() 方法告知開發者 View 的尺寸修改了)以便在繪製和觸摸反饋階段使用。保存 View 的實際尺寸以後,View 的 layout() 方法又會調用 View 的 onLayout() 方法,不過 View 的 onLayout() 方法是一個空實現,由於它沒有子 View):
layout() : 保存 View 的實際尺寸。調用 setFrame() 方法保存 View 的實際尺寸,調用 onSizeChanged() 通知開發者 View 的尺寸更改了,並最終會調用 onLayout() 方法讓子 View 佈局(若是有子 View 的話。由於自定義 View 中沒有子 View,因此自定義 View 的 onLayout() 方法是一個空實現);
onLayout() : 空實現,什麼也不作,由於它沒有子 View。若是是 ViewGroup 的話,在 onLayout() 方法中須要調用子 View 的 layout() 方法,將子 View 的實際尺寸傳給它們,讓子 View 保存本身的實際尺寸。所以,在自定義 View 中,不需重寫此方法,在自定義 ViewGroup 中,需重寫此方法。
注意:
layout() & onLayout() 並非「調度」與「實際作事」的關係,layout() 和 onLayout() 均作事,只不過職責不一樣。
在 View 的繪製階段會執行一個方法——draw(),draw() 是繪製階段的總調度方法,在其中會調用繪製背景的方法 drawBackground()、繪製主體的方法 onDraw()、繪製子 View 的方法 dispatchDraw() 和 繪製前景的方法 onDrawForeground():
draw() : 繪製階段的總調度方法,在其中會調用繪製背景的方法 drawBackground()、繪製主體的方法 onDraw()、繪製子 View 的方法 dispatchDraw() 和 繪製前景的方法 onDrawForeground();
drawBackground() : 繪製背景的方法,不能重寫,只能經過 xml 佈局文件或者 setBackground() 來設置或修改背景;
onDraw() : 繪製 View 主體內容的方法,一般狀況下,在自定義 View 的時候,只用實現該方法便可;
dispatchDraw() : 繪製子 View 的方法。同 onLayout() 方法同樣,在自定義 View 中它是空實現,什麼也不作。但在自定義 ViewGroup 中,它會調用 ViewGroup.drawChild() 方法,在 ViewGroup.drawChild() 方法中又會調用每個子 View 的 View.draw() 讓子 View 進行自我繪製;
onDrawForeground() : 繪製 View 前景的方法,也就是說,想要在主體內容之上繪製東西的時候就能夠在該方法中實現。
注意:
Android 裏面的繪製都是按順序的,先繪製的內容會被後繪製的蓋住。如,你在重疊的位置「先畫圓再畫方」和「先畫方再畫圓」所呈現出來的結果是不一樣的,具體表現爲下表:
「自定義 ViewGroup 佈局、繪製」主要包括三個階段:
同自定義 View 同樣,在自定義 ViewGroup 的測量階段會執行兩個方法:
measure() : 調度方法,主要作一些前置和優化工做,並最終會調用 onMeasure() 方法執行實際的測量工做;
onMeasure() : 實際執行測量任務的方法,與自定義 View 不一樣,在自定義 ViewGroup 的 onMeasure() 方法中,ViewGroup 會遞歸調用子 View 的 measure() 方法,並經過 measure() 將 ViewGroup 對子 View 的尺寸要求(ViewGroup 會根據開發者對子 View 的尺寸要求、本身的父 View(ViewGroup 的父 View) 對本身的尺寸要求和本身的可用空間計算出本身對子 View 的尺寸要求)傳入,對子 View 進行測量,並把測量結果臨時保存,以便在佈局階段使用。測量出子 View 的實際尺寸以後,ViewGroup 會根據子 View 的實際尺寸計算出本身的指望尺寸,並經過 setMeasuredDimension() 方法告知父 View(ViewGroup 的父 View) 本身的指望尺寸。
具體流程以下:
同自定義 View 同樣,在自定義 ViewGroup 的佈局階段會執行兩個方法:
layout() : 保存 ViewGroup 的實際尺寸。調用 setFrame() 方法保存 ViewGroup 的實際尺寸,調用 onSizeChanged() 通知開發者 ViewGroup 的尺寸更改了,並最終會調用 onLayout() 方法讓子 View 佈局;
onLayout() : ViewGroup 會遞歸調用每一個子 View 的 layout() 方法,把測量階段計算出的子 View 的實際尺寸和位置傳給子 View,讓子 View 保存本身的實際尺寸和位置。
同自定義 View 同樣,在自定義 ViewGroup 的繪製階段會執行一個方法——draw()。draw() 是繪製階段的總調度方法,在其中會調用繪製背景的方法 drawBackground()、繪製主體的方法 onDraw()、繪製子 View 的方法 dispatchDraw() 和 繪製前景的方法 onDrawForeground():
draw() : 繪製階段的總調度方法,在其中會調用繪製背景的方法 drawBackground()、繪製主體的方法 onDraw()、繪製子 View 的方法 dispatchDraw() 和 繪製前景的方法 onDrawForeground();
在 ViewGroup 中,你也能夠重寫繪製主體的方法 onDraw()、繪製子 View 的方法 dispatchDraw() 和 繪製前景的方法 onDrawForeground()。但大多數狀況下,自定義 ViewGroup 是不須要重寫任何繪製方法的。由於一般狀況下,ViewGroup 的角色是容器,一個透明的容器,它只是用來盛放子 View 的。
自定義 View,它的內容是「三個半徑不一樣、顏色不一樣的同心圓」,效果圖以下:
//1.1 在 xml 中自定義 View 屬性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--CircleView-->
<declare-styleable name="CircleView">
<attr name="circle_radius" format="dimension" />
<attr name="outer_circle_color" format="reference|color" />
<attr name="middle_circle_color" format="reference|color" />
<attr name="inner_circle_color" format="reference|color" />
</declare-styleable>
</resources>
//1.2 在 View 構造函數中獲取自定義 View 屬性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mRadius = typedArray.getDimension(R.styleable.CircleView_circle_radius, getResources().getDimension(R.dimen.avatar_size));
mOuterCircleColor = typedArray.getColor(R.styleable.CircleView_outer_circle_color, getResources().getColor(R.color.purple_500));
mMiddleCircleColor = typedArray.getColor(R.styleable.CircleView_middle_circle_color, getResources().getColor(R.color.purple_500));
mInnerCircleColor = typedArray.getColor(R.styleable.CircleView_inner_circle_color, getResources().getColor(R.color.purple_500));
typedArray.recycle();
複製代碼
因爲不須要自定義 View 的尺寸,因此,不用重寫該方法。
因爲沒有子 View 須要佈局,因此,不用重寫該方法。
//4. 重寫 onDraw() 方法,自定義 View 內容
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(mOuterCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
mPaint.setColor(mMiddleCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius * 2/3, mPaint);
mPaint.setColor(mInnerCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius/3, mPaint);
}
複製代碼
因爲 View 不須要和用戶交互,因此,不用重寫該方法。
ViewGroup 的方法。
完整代碼以下:
//1. 自定義屬性的聲明
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--CircleView-->
<declare-styleable name="CircleView">
<attr name="circle_radius" format="dimension" />
<attr name="outer_circle_color" format="reference|color" />
<attr name="middle_circle_color" format="reference|color" />
<attr name="inner_circle_color" format="reference|color" />
</declare-styleable>
</resources>
//2. CircleView
public class CircleView extends View {
private float mRadius;
private int mOuterCircleColor, mMiddleCircleColor, mInnerCircleColor;
private Paint mPaint;
public CircleView(Context context) {
this(context, null);
}
public CircleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initData(context, attrs);
}
private void initData(Context context, AttributeSet attrs) {
//1. 自定義屬性的聲明與獲取
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mRadius = typedArray.getDimension(R.styleable.CircleView_circle_radius, getResources().getDimension(R.dimen.avatar_size));
mOuterCircleColor = typedArray.getColor(R.styleable.CircleView_outer_circle_color, getResources().getColor(R.color.purple_500));
mMiddleCircleColor = typedArray.getColor(R.styleable.CircleView_middle_circle_color, getResources().getColor(R.color.purple_500));
mInnerCircleColor = typedArray.getColor(R.styleable.CircleView_inner_circle_color, getResources().getColor(R.color.purple_500));
typedArray.recycle();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mOuterCircleColor);
}
//2. 重寫測量階段相關方法(onMeasure());
//因爲不須要自定義 View 的尺寸,因此不用重寫該方法
// @Override
// protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// }
//3. 重寫佈局階段相關方法(onLayout()(僅 ViewGroup 須要重寫));
//因爲沒有子 View 須要佈局,因此不用重寫該方法
// @Override
// protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// super.onLayout(changed, left, top, right, bottom);
// }
//4. 重寫繪製階段相關方法(onDraw() 繪製主體、dispatchDraw() 繪製子 View 和 onDrawForeground() 繪製前景);
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(mOuterCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
mPaint.setColor(mMiddleCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius * 2/3, mPaint);
mPaint.setColor(mInnerCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius/3, mPaint);
}
}
//3. 在 xml 中應用 CircleView
<?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:gravity="center"
tools:context=".custom_view_only_draw.CustomViewOnlyDrawActivity">
<com.smart.a03_view_custom_view_example.custom_view_only_draw.CircleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:circle_radius="@dimen/padding_ninety_six"
app:inner_circle_color="@color/yellow_500"
app:middle_circle_color="@color/cyan_500"
app:outer_circle_color="@color/green_500" />
</LinearLayout>
複製代碼
最終效果以下:
此時,即便你在 xml 中將 CircleView 的寬、高聲明爲「match_parent」,你會發現最終的顯示效果都是同樣的。
主要緣由是:默認狀況下,View 的 onMeasure() 方法在經過 setMeasuredDimension() 告知父 View 本身的指望尺寸時,會調用 getDefaultSize() 方法。在 getDefaultSize() 方法中,又會調用 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 獲取建議的最小寬度和最小高度,並根據最小尺寸和父 View 對本身的尺寸要求進行修正。最主要的是,在 getDefaultSize() 方法中修正的時候,會將 MeasureSpec.AT_MOST 和 MeasureSpec.EXACTLY 一視同仁,直接返回父 View 對 View 的尺寸要求:
//1. 默認 onMeasure 的處理
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
//2. getSuggestedMinimumWidth()
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
//3. getSuggestedMinimumHeight()
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
//4. getDefaultSize()
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:
case MeasureSpec.EXACTLY:
//MeasureSpec.AT_MOST、MeasureSpec.EXACTLY 一視同仁
result = specSize;
break;
}
return result;
}
複製代碼
正是由於在 getDefaultSize() 方法中處理的時候,將 MeasureSpec.AT_MOST 和 MeasureSpec.EXACTLY 一視同仁,因此纔有了上面「在 xml 中應用 CircleView 的時候,不管將 CircleView 的尺寸設置爲 match_parent 仍是 wrap_content 效果都同樣」的現象。
具體分析以下:
開發者對 View 的尺寸要求 | View 的父 View 對 View 的尺寸要求 | View 的指望尺寸 |
---|---|---|
android:layout_width="wrap_content" android:layout_height="wrap_content" |
MeasureSpec.AT_MOST specSize |
specSize |
android:layout_width="match_parent" android:layout_height="match_parent" |
MeasureSpec.EXACTLY specSize |
specSize |
注:
上表中,「View 的父 View 對 View 的尺寸要求」是 View 的父 View 根據「開發者對子 View 的尺寸要求」、「本身的父 View(View 的父 View 的父 View) 對本身的尺寸要求」和「本身的可用空間」計算出本身對子 View 的尺寸要求。
另外,由執行結果可知,上表中的 specSize 實際上等於 View 的尺寸:
2019-08-13 17:28:26.855 16024-16024/com.smart.a03_view_custom_view_example E/TAG: Width(getWidth()): 1080 Height(getHeight()): 1584
複製代碼
自定義 View,它的內容是「三個半徑不一樣、顏色不一樣的同心圓」,效果圖以下:
//1.1 在 xml 中自定義 View 屬性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--CircleView-->
<declare-styleable name="CircleView">
<attr name="circle_radius" format="dimension" />
<attr name="outer_circle_color" format="reference|color" />
<attr name="middle_circle_color" format="reference|color" />
<attr name="inner_circle_color" format="reference|color" />
</declare-styleable>
</resources>
//1.2 在 View 構造函數中獲取自定義 View 屬性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mRadius = typedArray.getDimension(R.styleable.CircleView_circle_radius, getResources().getDimension(R.dimen.avatar_size));
mOuterCircleColor = typedArray.getColor(R.styleable.CircleView_outer_circle_color, getResources().getColor(R.color.purple_500));
mMiddleCircleColor = typedArray.getColor(R.styleable.CircleView_middle_circle_color, getResources().getColor(R.color.purple_500));
mInnerCircleColor = typedArray.getColor(R.styleable.CircleView_inner_circle_color, getResources().getColor(R.color.purple_500));
typedArray.recycle();
複製代碼
//2. onMeasure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//2.1 根據 View 特色或業務需求計算出 View 的尺寸
mWidth = (int)(mRadius * 2);
mHeight = (int)(mRadius * 2);
//2.2 經過 resolveSize() 方法修正結果
mWidth = resolveSize(mWidth, widthMeasureSpec);
mHeight = resolveSize(mHeight, heightMeasureSpec);
//2.3 經過 setMeasuredDimension() 保存 View 的指望尺寸(經過 setMeasuredDimension() 告知父 View 的指望尺寸)
setMeasuredDimension(mWidth, mHeight);
}
複製代碼
因爲沒有子 View 須要佈局,因此,不用重寫該方法。
//4. 重寫 onDraw() 方法,自定義 View 內容
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(mOuterCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
mPaint.setColor(mMiddleCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius * 2/3, mPaint);
mPaint.setColor(mInnerCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius/3, mPaint);
}
複製代碼
因爲 View 不須要和用戶交互,因此,不用重寫該方法。
ViewGroup 的方法。
完整代碼以下:
//1. 自定義屬性的聲明
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--CircleView-->
<declare-styleable name="CircleView">
<attr name="circle_radius" format="dimension" />
<attr name="outer_circle_color" format="reference|color" />
<attr name="middle_circle_color" format="reference|color" />
<attr name="inner_circle_color" format="reference|color" />
</declare-styleable>
</resources>
//2. MeasuredCircleView
public class MeasuredCircleView extends View {
private int mWidth, mHeight;
private float mRadius;
private int mOuterCircleColor, mMiddleCircleColor, mInnerCircleColor;
private Paint mPaint;
public MeasuredCircleView(Context context) {
this(context, null);
}
public MeasuredCircleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MeasuredCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initData(context, attrs);
}
private void initData(Context context, AttributeSet attrs) {
//1. 自定義屬性的聲明與獲取
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mRadius = typedArray.getDimension(R.styleable.CircleView_circle_radius, getResources().getDimension(R.dimen.avatar_size));
mOuterCircleColor = typedArray.getColor(R.styleable.CircleView_outer_circle_color, getResources().getColor(R.color.purple_500));
mMiddleCircleColor = typedArray.getColor(R.styleable.CircleView_middle_circle_color, getResources().getColor(R.color.purple_500));
mInnerCircleColor = typedArray.getColor(R.styleable.CircleView_inner_circle_color, getResources().getColor(R.color.purple_500));
typedArray.recycle();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mOuterCircleColor);
}
//2. 重寫測量階段相關方法(onMeasure());
//因爲不須要自定義 View 的尺寸,因此不用重寫該方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//2.1 根據 View 特色或業務需求計算出 View 的尺寸
mWidth = (int)(mRadius * 2);
mHeight = (int)(mRadius * 2);
//2.2 經過 resolveSize() 方法修正結果
mWidth = resolveSize(mWidth, widthMeasureSpec);
mHeight = resolveSize(mHeight, heightMeasureSpec);
//2.3 經過 setMeasuredDimension() 保存 View 的指望尺寸(經過 setMeasuredDimension() 告知父 View 的指望尺寸)
setMeasuredDimension(mWidth, mHeight);
}
//3. 重寫佈局階段相關方法(onLayout()(僅 ViewGroup 須要重寫));
//因爲沒有子 View 須要佈局,因此不用重寫該方法
// @Override
// protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// super.onLayout(changed, left, top, right, bottom);
// }
//4. 重寫繪製階段相關方法(onDraw() 繪製主體、dispatchDraw() 繪製子 View 和 onDrawForeground() 繪製前景);
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(mOuterCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
mPaint.setColor(mMiddleCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius * 2/3, mPaint);
mPaint.setColor(mInnerCircleColor);
canvas.drawCircle(mRadius, mRadius, mRadius/3, mPaint);
}
}
//3. 在 xml 中應用 MeasuredCircleView
<?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:gravity="center"
android:orientation="vertical"
tools:context=".custom_view_measure_draw.CustomViewMeasureDrawActivity">
<com.smart.a03_view_custom_view_example.custom_view_measure_draw.MeasuredCircleView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:circle_radius="@dimen/padding_ninety_six"
app:inner_circle_color="@color/yellow_500"
app:middle_circle_color="@color/cyan_500"
app:outer_circle_color="@color/green_500" />
</LinearLayout>
複製代碼
最終效果以下:
當在 xml 中將 MeasuredCircleView 的寬、高聲明爲「match_parent」時,顯示效果跟 CircleView 顯示效果同樣。
開發者對 View 的尺寸要求 | View 的父 View 對 View 的尺寸要求 | View 的指望尺寸 |
---|---|---|
android:layout_width="match_parent" android:layout_height="match_parent" |
MeasureSpec.EXACTLY specSize |
specSize |
可是,當在 xml 中將 MeasuredCircleView 的寬、高聲明爲「wrap_content」時,顯示效果是下面這個樣子:
其實,也很好理解:
開發者對 View 的尺寸要求 | View 的父 View 對 View 的尺寸要求 | View 的指望尺寸 |
---|---|---|
android:layout_width="wrap_content" android:layout_height="wrap_content" |
MeasureSpec.AT_MOST specSize |
if(childSize < specSize) childSize if(childSize > specSize) specSize |
自定義 ViewGroup,標籤佈局,效果圖以下:
不管是自定義 View 仍是自定義 ViewGroup,大體的流程都是同樣的:
只不過,大多數狀況下,ViewGroup 不須要「自定義屬性」和「重寫繪製階段相關方法」,但有些時候仍是須要的,如,開發者想在 ViewGroup 的全部子 View 上方繪製一些內容,就能夠經過重寫 ViewGroup 的 onDrawForeground() 來實現。
在自定義 ViewGroup 中「自定義屬性的聲明與獲取」的方法與在自定義 View 中「自定義屬性的聲明與獲取」的方法同樣,且由於大多數狀況下,在自定義 ViewGroup 中是不須要自定義屬性的,因此,在這裏就不自定義屬性了。
//2. 重寫測量階段相關方法(onMeasure());
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//2.1 解析 ViewGroup 的父 View 對 ViewGroup 的尺寸要求
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(widthMeasureSpec);
//2.2 ViewGroup 根據「開發者在 xml 中寫的對 ViewGroup 子 View 的尺寸要求」、「本身的父 View(ViewGroup 的父 View)對本身的尺寸要求」和
//「本身的可用空間」計算出本身對子 View 的尺寸要求,並將該尺寸要求經過子 View 的 measure() 方法傳給子 View,讓子 View 測量本身(View)的指望尺寸
int widthUsed = 0;
int heightUsed = getPaddingTop();
int lineHeight = 0;
int lineWidthUsed = getPaddingLeft();
int maxRight = widthSize - getPaddingRight();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
//是否須要換行
if(widthMode != MeasureSpec.UNSPECIFIED && (lineWidthUsed + child.getMeasuredWidth() > maxRight)){
lineWidthUsed = getPaddingLeft();
heightUsed += lineHeight + mRowSpace;
lineHeight = 0;
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
}
//2.3 ViewGroup 暫時保存子 View 的尺寸,以便佈局階段和繪製階段使用
Rect childBound;
if(mChildrenBounds.size() <= i){
childBound = new Rect();
mChildrenBounds.add(childBound);
}else{
childBound = mChildrenBounds.get(i);
}
//此處不能用 child.getxxx() 獲取子 View 的尺寸值,由於子 View 只是量了尺寸,尚未佈局,這些值都是 0
// childBound.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
childBound.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(), heightUsed + child.getMeasuredHeight());
lineWidthUsed += child.getMeasuredWidth() + mItemSpace;
widthUsed = Math.max(lineWidthUsed, widthUsed);
lineHeight = Math.max(lineHeight, child.getMeasuredHeight());
}
//2.4 ViewGroup 將「根據子 View 的實際尺寸計算出的本身(ViewGroup)的尺寸」結合「本身父 View 對本身的尺寸要求」進行修正,並通
//過 setMeasuredDimension() 方法告知父 View 本身的指望尺寸
int measuredWidth = resolveSize(widthUsed, widthMeasureSpec);
int measuredHeight = resolveSize((heightUsed + lineHeight + getPaddingBottom()), heightMeasureSpec);
setMeasuredDimension(measuredWidth, measuredHeight);
}
//重寫generateLayoutParams()
//2.2.1 在自定義 ViewGroup 中調用 measureChildWithMargins() 方法計算 ViewGroup 對子 View 的尺寸要求時,
//必須在 ViewGroup 中重寫 generateLayoutParams() 方法,由於 measureChildWithMargins() 方法中用到了 MarginLayoutParams,
//若是不重寫 generateLayoutParams() 方法,那調用 measureChildWithMargins() 方法時,MarginLayoutParams 就爲 null,
//因此在自定義 ViewGroup 中調用 measureChildWithMargins() 方法時,必須重寫 generateLayoutParams() 方法。
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
複製代碼
//3. 重寫佈局階段相關方法(onLayout()(僅 ViewGroup 須要重寫));
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
//應用測量階段計算出的子 View 的尺寸值佈局子 View
View child = getChildAt(i);
Rect childBound = mChildrenBounds.get(i);
child.layout(childBound.left, childBound.top, childBound.right, childBound.bottom);
}
}
複製代碼
默認狀況下,自定義 ViewGroup 時是不須要重寫任何繪製階段的方法的,由於 ViewGroup 的角色是容器,一個透明的容器,它只是用來盛放子 View 的。
注意:
因爲 ViewGroup 不須要和用戶交互,因此,不用重寫該方法。
因爲 ViewGroup 不須要和用戶交互且 ViewGroup 不須要攔截子 View 的 MotionEvent,因此,不用重寫該方法。
完整代碼以下:
//1. TabLayout
public class TabLayout extends ViewGroup {
private ArrayList<Rect> mChildrenBounds;
private int mItemSpace;
private int mRowSpace;
public TabLayout(Context context) {
this(context, null);
}
public TabLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initData();
}
private void initData(){
mChildrenBounds = new ArrayList<>();
mItemSpace = (int)getResources().getDimension(R.dimen.padding_small);
mRowSpace = (int)getResources().getDimension(R.dimen.padding_small);
}
//2. 重寫測量階段相關方法(onMeasure());
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//2.1 解析 ViewGroup 的父 View 對 ViewGroup 的尺寸要求
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(widthMeasureSpec);
//2.2 ViewGroup 根據「開發者在 xml 中寫的對 ViewGroup 子 View 的尺寸要求」、「本身的父 View(ViewGroup 的父 View)對本身的尺寸要求」和
//「本身的可用空間」計算出本身對子 View 的尺寸要求,並將該尺寸要求經過子 View 的 measure() 方法傳給子 View,讓子 View 測量本身(View)的指望尺寸
int widthUsed = 0;
int heightUsed = getPaddingTop();
int lineHeight = 0;
int lineWidthUsed = getPaddingLeft();
int maxRight = widthSize - getPaddingRight();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
//是否須要換行
if(widthMode != MeasureSpec.UNSPECIFIED && (lineWidthUsed + child.getMeasuredWidth() > maxRight)){
lineWidthUsed = getPaddingLeft();
heightUsed += lineHeight + mRowSpace;
lineHeight = 0;
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
}
//2.3 ViewGroup 暫時保存子 View 的尺寸,以便佈局階段和繪製階段使用
Rect childBound;
if(mChildrenBounds.size() <= i){
childBound = new Rect();
mChildrenBounds.add(childBound);
}else{
childBound = mChildrenBounds.get(i);
}
//此處不能用 child.getxxx() 獲取子 View 的尺寸值,由於子 View 只是量了尺寸,尚未佈局,這些值都是 0
// childBound.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
childBound.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(), heightUsed + child.getMeasuredHeight());
lineWidthUsed += child.getMeasuredWidth() + mItemSpace;
widthUsed = Math.max(lineWidthUsed, widthUsed);
lineHeight = Math.max(lineHeight, child.getMeasuredHeight());
}
//2.4 ViewGroup 將「根據子 View 的實際尺寸計算出的本身(ViewGroup)的尺寸」結合「本身父 View 對本身的尺寸要求」進行修正,並通
//過 setMeasuredDimension() 方法告知父 View 本身的指望尺寸
int measuredWidth = resolveSize(widthUsed, widthMeasureSpec);
int measuredHeight = resolveSize((heightUsed + lineHeight + getPaddingBottom()), heightMeasureSpec);
setMeasuredDimension(measuredWidth, measuredHeight);
}
//2.2.1 在自定義 ViewGroup 中調用 measureChildWithMargins() 方法計算 ViewGroup 對子 View 的尺寸要求時,
//必須在 ViewGroup 中重寫 generateLayoutParams() 方法,由於 measureChildWithMargins() 方法中用到了 MarginLayoutParams,
//若是不重寫 generateLayoutParams() 方法,那調用 measureChildWithMargins() 方法時,MarginLayoutParams 就爲 null,
//因此在自定義 ViewGroup 中調用 measureChildWithMargins() 方法時,必須重寫 generateLayoutParams() 方法。
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
//3. 重寫佈局階段相關方法(onLayout()(僅 ViewGroup 須要重寫));
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
//應用測量階段計算出的子 View 的尺寸值佈局子 View
View child = getChildAt(i);
Rect childBound = mChildrenBounds.get(i);
child.layout(childBound.left, childBound.top, childBound.right, childBound.bottom);
}
}
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
return super.onInterceptHoverEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
}
//2. 在 xml 中應用 TabLayout
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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:scrollbars="none"
tools:context=".MainActivity">
<com.smart.a04_view_custom_viewgroup_example.custom_layout.TabLayout
android:id="@+id/tag_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/grey_400"
android:padding="@dimen/padding_small">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/common_bg"
android:text="@string/spending_clothes" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/common_bg"
android:text="@string/spending_others" />
...
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/common_bg"
android:text="@string/november" />
</com.smart.a04_view_custom_viewgroup_example.custom_layout.TabLayout>
</ScrollView>
複製代碼
最終效果以下:
自定義 View 包括三部份內容:
其中佈局階段肯定了 View 的位置和尺寸,該階段主要是爲了後面的繪製和觸摸反饋作支持;繪製階段主要用於繪製 View 的內容(大多數狀況下,只用實現 OnDraw 方法(Where)方法、按照指定順序調用相關 API(How)便可實現自定義繪製(What));觸摸反饋階段肯定了用戶點擊了哪裏,三者相輔相成,缺一不可。