本人只是
Android
小菜一個,寫技術文檔只是爲了總結本身在最近學習到的知識,歷來不敢爲人師,若是裏面有些不正確的地方請你們盡情指出,謝謝!java
在進行Android
應用開發時,能夠選擇系統提供的各式各樣的控件,但有時原生控件在功能和效果上並不能知足需求,這時就要求必須根據實際需求來定義新的控件,能夠經過繼承View
,也能夠繼承某些已經存在的原生控件,來實現自定義控件。本文將選擇直接繼承View
來實現一個最簡單的控件。android
自定義控件包含了Android
中和View
相關的不少知識,學習自定義控件也能幫組學習和理解相關知識。canvas
要想自定義出功能強大效果酷炫的控件,要求必須對View
體系有深刻的理解,在這點我還差的不少,因此本文並不能教你們怎樣去實現這樣的控件。本文只是從自定義View
的基本規範方面,跟你們探討下在自定義一個控件的過程當中,有哪些方面須要注意的,或者說有哪些功能是須要實現的,主要包括:控件屬性
、控件測量
、控件繪製
和控件交互
。app
當咱們在xml
中定義控件的時候,確定須要對控件具備的某些屬性進行設置,例如寬高
、背景顏色
、文本
等等,下面是在使用 TextView
的一個示例:ide
<TextView android:id="@+id/main_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#FF0000" android:text="Hello World!" />
複製代碼
在自定義控件的時候,爲了可以讓用戶靈活定義控件的某些特性,也須要經過屬性的方法把用戶指定的值傳入控件,而不是在控件內部使用預約義的值,這也就要求在自定義控件的時候使用到自定義屬性。函數
自定義屬性須要在res/values/attrs.xml
裏面定義,若是這個文件不存就本身建立一個。結合一個例子進行介紹:學習
<declare-styleable name="custom_view">
<attr name="default_color" format="color"></attr>
</declare-styleable>
複製代碼
declare-styleable name="custom_view"
指定了自定義屬性集合的name
信息,這個值能夠是任意值,但通常爲了方面使用都是直接使用自定義控件的名字。測試
<attr name="default_color" format="color"></attr>
指定了自定義屬性集合裏的具體屬性和該屬性對應的類型,本例中使用的是color
類型,代表這個屬性須要的是一個顏色值,可以支持的format
類型以下表:this
類型 | 含義 | 取值 |
---|---|---|
boolean |
布爾類型 | 只能是true 或false |
string |
字符串類型 | 任意字符串值 |
integer |
整數類型 | 只能是整數 |
float |
浮點數類型 | 只能是浮點和整型 |
fraction |
百分比類型 | 只能以% 結尾 |
color |
顏色類型 | 能夠是顏色值或者指向color 的資源 |
dimension |
尺寸類型 | 能夠是具體尺寸值或指向尺寸的資源 |
reference |
引用類型 | 只能是指向某一資源的ID |
enum |
枚舉類型 | 只能是定義的枚舉值 |
flag |
位標誌類型 | 只能是定義的位值 |
在這裏只定義了一個簡單的color
類型的屬性,其餘類型的屬性你們可自行定義,方法是相似的。spa
在定義了屬性後,能夠直接在xml
使用這些屬性,使用方法和原生控件屬性同樣,只需根據不一樣類型設置值便可。在上面定義一個屬性default_color
,如今就能夠在xml
裏使用了:
<com.test.androidtest.CustomView android:id="@+id/custom_view" android:layout_width="wrap_content" android:layout_height="wrap_content" app:default_color="#ffff00"/>
複製代碼
須要注意的是,在這裏使用了新的命名空間app
,其聲明是xmlns:app="http://schemas.android.com/apk/res-auto"
,若是你們使用的Android Studio
,這個命名空間是自動添加的,無須自行處理。
當xml
使用了自定義屬性後,在建立這個控件的時候,就會把這些屬性傳入控件,在控件內部就能夠獲取並使用到該屬性值了。
// 在代碼裏經過 new 方式建立控件實例時使用
public CustomView(Context context) {
super(context);
}
// 在 xml 定義控件時使用,會獲取到定義的屬性
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
// 獲取定義的屬性集合
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.custom_view);
// 獲取特定的屬性值
if (array != null) {
default_color = array.getColor(R.styleable.custom_view_default_color, -1);
}
}
複製代碼
上述代碼演示瞭如何在控件內部獲取自定義屬性,在成功獲取到屬性值後就能夠利用該值進行後續的控件繪製工做了。
須要注意的是在自定義控件是須要實現兩個不一樣的構造函數,分別對應於在
java
和xml
的使用場景。
在前面已經講了如何定義和在控件內部獲取屬性,可是咱們知道有時控件屬性是須要根據不一樣的場景進行修改的,而在xml
只能指定屬性的初始值,沒法進行不斷的修改。這就要求必須針對有些屬性提供取值器
和設值器
,也就是常說的getter
和setter
,這裏之因此說是「有些屬性」,是由於並非全部屬性都須要支持動態修改的。
仍是針對前面定義的default_color
屬性,如今對其設置取值器
和設值器
:
public int getColor() {
return default_color;
}
public void setColor(int color) {
default_color = color;
// 調用 onDraw,從新刷新控件.
invalidate();
}
複製代碼
取值器
比較簡單,只要返回當前屬性值就能夠了。設值器
除了要更新當前屬性值外,更重要的是,在更新完當前屬性值外,要對當前的控件進行第二次的繪製,以更新控件狀態,這裏直接調用invalidate()
,它會把當前view
標誌爲DIRTY
,在下一幀繪製時調用控件的onDraw()
方法完成對控件的更新。設置了屬性的getter
和setter
後,就能夠在使用控件的時候,動態獲取和修改屬性值了。
測量的目的是要肯定控件在顯示的時候具體的顯示尺寸,你們可能會奇怪:不是在xml
已經指定了控件大小了嗎?爲何還要再測量一次呢?這是由於在xml
指定控件大小的時候有不一樣的方式,每種方式最終致使分配給控件的尺寸也不同。
指定尺寸方式 | 含義 |
---|---|
wrap_content |
根據控件具體內容分配尺寸 |
match_parent |
根據父控件剩餘大小給控件分配尺寸 |
具體數值 |
根據給定的數值進行分配控件尺寸 |
爲了可以測了控件,須要實現onMeasure(int widthMeasureSpec, int heightMeasureSpec)
方法,先看下View
中該方法聲明:
/** * Measure the view and its content to determine the measured width and the * measured height. This method is invoked by {@link #measure(int, int)} and * should be overridden by subclasses to provide accurate and efficient * measurement of their contents. */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製代碼
這裏提到:onMeasure
是用來決定控件的寬高信息的,爲了可以提供更準確和高效的控件測量,子類最好要重寫這個方法,因此自定義控件最好也要實現這個方法。
這裏的參數widthMeasureSpec
和heightMeasureSpec
表明是什麼意思?是否是就是控件的寬高呢?固然不是,若是它們表示的就是控件寬高就不須要咱們繼續測量了。widthMeasureSpec
和heightMeasureSpec
裏面都包含了兩個信息:size
和mode
,其中size
表示的是父控件告訴子控件的建議寬高,mode
表示當前的測量模式,具體有AT_MOST
,EXACTLY
和UNSPECIFIED
,其含義以下:
測量模式 | 尺寸模式 | 含義 |
---|---|---|
AT_MOST |
wrap_content |
父控件提供一個最大值,子控件不要超過父控件提供的尺寸大小。 |
EXACTLY |
match_parent 或者具體值 |
父控件提供一個確切值,子控件能夠直接使用這個尺寸來設置大小。 |
UNSPECIFIED |
暫無 |
父控件不提供,子控件能夠任意設置大小。 |
從上面的表格能夠看到:UNSPECIFIED
通常是遇不到的,而AT_MOST
和EXACTLY
都會提供一個建議值,能夠根據這個值和測試模式來肯定子控件大小。
本文中的自定義控件的onMeasure
以下:
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 使用寬高中的最小值把寬高設置爲等值,由於控件的最終目的是畫一個圓。
int dimension = Math.min(getSize(widthMeasureSpec), getSize(heightMeasureSpec));
// 設置最終的寬高信息,若是少了這步,獲得的寬高將沒法應用到控件中。
setMeasuredDimension(dimension, dimension);
}
private int getSize(int measureSpec) {
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
// EXACTLY 和 AT_MOST 直接使用父控件提供的寬高信息。
switch (mode) {
case MeasureSpec.EXACTLY:
case MeasureSpec.AT_MOST:
return size;
default:
// UNSPECIFIED 返回預約義的寬高信息,通常不會遇到。
return mMeasureWidthHeight;
}
}
複製代碼
在本文的自定義控件中,最終的目的是要顯示一個圓形,在onMeasure
裏設置了等值寬高,而在獲取寬高時針對AT_MOST
和EXACTLY
兩種狀況都直接使用了父控制傳遞過來的尺寸。固然這只是一種最簡單的狀況,當要自定義高能複雜的控件時,寬高的肯定須要結合的因素會更多,計算也會更復雜。
測量控件後就能夠知道控件的最終寬高信息,這時須要作的就是進行實際的繪製,只有經過繪製,控件才能真正地顯示出來。繪製控件須要實現onDraw(Canvas canvas)
方法,和onMeasure
同樣,先看下在View
中的聲明:
/** * Implement this to do your drawing. * * @param canvas the canvas on which the background will be drawn */
protected void onDraw(Canvas canvas) {
}
複製代碼
能夠發現:View
並無實現onDraw
,這是由於View 是全部控件的父類,但其自己並非一個能夠直接顯示的控件
,這就要求全部須要顯示的控件都必須實現這個方法,它的參數是Canvas
類,就是常說的畫布
。爲了顯示控件,咱們須要作的就是用Paint
在Canvas
上把須要顯示的圖像畫出來,正如咱們在電腦上常常在畫圖軟件上畫圖同樣。
如今看下本例中自定義控件的onDraw(Canvas canvas)
的實現:
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 初始化畫筆,這個對象須要在控件初始化時初始化,這裏正常不會走到。
if (mPaint == null) {
mPaint = new Paint();
}
// 設置畫筆的顏色。
mPaint.setColor(default_color);
// 在畫布上畫出一個圓形。
int radius = getMeasuredWidth() / 2;
canvas.drawCircle(getLeft() + radius, getTop() +radius, radius, mPaint);
}
複製代碼
上面的示例代碼只是實現一個根據用戶傳入的顏色來進行畫圓功能,其效果以下:
Canvas
除了畫圓,還能夠畫出更多更復雜的圖形,Paint
也能夠有更多的控制,其你們自行查閱相關API
。
經過上面的幾個過程,已經能在界面上顯示自定義控件了,但顯示不是最終的目的,真正的目的仍是但願能與控件進行交互,最重要的是可以響應touch
事件,接下來就經過實現一個簡單的隨手指移動
功能:
private int mLastX;
private int mLastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - mLastX;
int offsetY = y - mLastY;
//從新放置新的位置
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
break;
default:
break;
}
return true;
}
複製代碼
此次對onTouchEvent
的重寫能夠實現讓控件隨着手指會移動,固然這裏只是一個簡單演示,還存在一些問題,好比控件會被移出屏幕以外,這是由於在移動時並無判斷當前控件的位置,把這個條件加上就能夠保證控件只在界面以內移動。
本文經過一個簡單的自定義圓形的例子,大體講解了自定義View的基本規範
,其中包括屬性、測量、繪製、交互
,你們能夠把它當作自定義控件的入門知識,但相信在瞭解了這些基本規範
後,再加上勤奮的練習,之後也能定義出功能複雜效果炫酷的控件,一塊兒加油!