Android:初識自定義控件
一、什么是自定義控件
1、概念
簡單算來學習Android已經有一年時間了,從最初覺得別人寫的軟件好厲害到這么厲害的軟件我也能寫。但是現在還是會被有些軟件的UI和動畫所驚艷。一開始以為UI上的控件都是畫出來的,后來才知道這些控件都有一個共同的名字——自定義控件。
自定義控件
為什么要自定義控件呢?當然不是為了簡單的好看。我們知道Android官方自帶的控件種類很多,基本能夠滿足日常的開發需求。但是一件產品的開發不僅僅需要功能上的完善,更要追求用戶體驗。所以單從用戶體驗上來說,官方的控件是遠遠談不上體驗的。所以越來越多的APP使用了自定義控件,一方面美觀好看,另一方面極大的提高了用戶體驗,何樂而不為呢?
而隨著Android技術越來越成熟,基本的控件有時已經滿足不了簡單的開發需求了,這個時候就需要我們自定義出滿足功能需求的控件來實現APP的一些需求。
2、實現方式
一般實現自定義控件會有三種方式:
- 繼承已有的控件實現
- 組合已有的控件實現
- 完全自定義控件
第一種方式其實也就相當于擴展已有控件的功能,這種實現方式比較簡單;第二種組合方式目的是通過多種控件的組合來完成一種控件的需求,也就是通過這種方式自定義出來的控件具有多種基本控件的功能,更加強大,較第一種而言這種實現方式比較復雜;而第三種完全自定義控件這就更加復雜了,這需要我們新建一種控件繼承View/ViewGroup,并實現一些其中的屬性或方法。
總的來說,按照需求我們采取不同的方式。這里我們先說一下完全自定義控件的方式。
二、完全自定義控件
下面我就分享我最近學習黑馬教程中的一個自定義開關的過程。
自定義開關
1、確定需求
從圖中我們可以看出,這個開關是由兩部分組成,第一部分是背景圖也就是顯示“開/關”的圖片,第二部分是前景圖也就是開關小滑塊。那么第一步我們肯定是需要將兩者組合在一起,成為一個一個全新的控件。所以說這一步中,我們需要繪制出控件的基本形狀。
因為這個開關是一個滑動開關,需要用戶手動觸摸才能改變狀態,那么我們肯定需要實現這個控件的觸摸事件,通過觸摸事件來改變開關的狀態。
開關的狀態既然需要改變,那么如何知道狀態發生改變呢?沒錯,就是事件監聽。我們還需要將這個控件綁定事件監聽器,來實時監聽開關狀態的改變。
一個控件有了形狀,有了觸摸事件和狀態監聽器,就已經能實現一些基本的功能需求了。所以總的來說,我們需要做三件事情:
- 繪制控件
- 觸摸事件監聽
- 狀態事件監聽
下面我就按照順序來實現相關的功能。
2、繪制控件
首先我們新建一個 CustomSwitchView 類,類直接繼承于View。繼承類過后我們需要實現類的幾種構造方法。在這里如果用Eclipse新建類的話,我們可以直接勾選 Constructors from superclass 選項。用Android Studio新建類的話,我們可以在類建立過后利用快捷鍵 Alt + Enter 來實現構造方法。
/**
- @ClassName: CustomSwitchView
- @Description:自定義控件 繼承View
- @author: iamxiarui@foxmail.com
@date: 2016年5月5日 下午6:51:49
*/
public class CustomSwitchView extends View {
/**
@Description:用于代碼創建控件
*/
public CustomSwitchView(Context context) {
super(context);
}
/**
@Description:用于在XML中使用,可以指定自定義屬性
*/
public CustomSwitchView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
@Description:用于在XML中使用,可以指定自定義屬性,并指定樣式
*/
public CustomSwitchView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
- @Description:用于在XML中使用,可以指定自定義屬性,并指定樣式及其資源
*/
public CustomSwitchView(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}</code></pre>
當構造函數實現之后,我們就需要實現控件的一些屬性。這里我們先不用自定義屬性,而用自定義的方法來設置相關屬性。先定義如下變量,后面我們需要用到:
//定義背景圖
private Bitmap switchBackgroupBitmap;
//定義前景圖
private Bitmap switchForegroupBitmap;</code></pre>
變量定義好之后我們需要自定義兩個方法,分別設置前景圖和背景圖,而兩個方法的參數都是一個 int 類型的資源ID,然后通過BitmapFactory對象來將資源ID對應的圖片資源添加到控件上:
/**
- @Title: setBackgroundPic
- @Description:設置背景圖
@return: void
*/
public void setBackgroundPic(int switchBackground) {
switchBackgroupBitmap = BitmapFactory.decodeResource(getResources(), switchBackground);
}
/**
- @Title: setForegroundPic
- @Description:設置前景圖
- @return: void
*/
public void setForegroundPic(int switchForeground) {
switchForegroupBitmap = BitmapFactory.decodeResource(getResources(), switchForeground);
}</code></pre>
注意這個時候不是說我們設置上圖片就能顯示出來,因為我們是自定義控件,所以我們必須將控件繪制在View中,這就涉及到一個非常重要知識——Android界面繪制流程。

界面繪制流程
從圖中我們可以看出Android界面繪制流程分為三個部分,第一部分是測量(Measure),在這部分里面View會先做一次測量,計算出自己需要占用多大的面積,我們可以重寫 onMeasure() 方法來重新定義View的寬高。第二部分是布局(Layout),這個部分我們需要做的事情就是將整個View中所有的子View大小寬高設置好,可以通過復寫 onLayout() 方法來實現,當然如果你的自定義View中沒有子View,那就不需要設計這一部分了。第三部分是繪制(Draw),這個很好理解,就是在創建的畫布(Canvas)上繪制出我們所需要的View樣式,同樣可以通過復寫 onDraw() 方法來實現。
由于我們現在所要做的就是一個簡單開關,只需要直接繼承View,并將開關的兩張圖設置成控件的背景即可,所以我們只要重寫 onMeasure() 和 onDraw() 這兩個方法。
/**
- @Title: onMeasure
- @Description:測量出自定義控件的長寬
@return: void
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(switchBackgroupBitmap.getWidth(), switchBackgroupBitmap.getHeight());
}
/**
- @Title: onDraw
- @Description:繪制控件
- @return: void
*/
@Override
protected void onDraw(Canvas canvas) {
// 先繪制背景
canvas.drawBitmap(switchBackgroupBitmap, 0, 0, paint);
//再繪制前景
canvas.drawBitmap(switchForegroupBitmap, 0, 0, paint);
}</code></pre>
當上面的方法重寫完畢后,我們就可以在Activity中設置圖片并顯示控件了:
buttonCSView = (CustomSwitchView) findViewById(R.id.csv_button);
// 設置背景圖
buttonCSView.setBackgroundPic(R.drawable.switch_background);
// 設置前景圖
buttonCSView.setForegroundPic(R.drawable.switch_foreground);
當然在此之前,我們需要一個畫筆工具,因為每一個畫筆都需要在創建的時候使用,所以我將畫筆工具的創建放在單獨的方法中,而且在每一個構造函數中,調用這個方法,也就相當于只要創建了自定義控件,那么就自動創建了一個畫筆工具。
/**
- @Title: initView
- @Description:初始化View
- @return: void
*/
private void initView() {
paint = new Paint();
}</code></pre>
除此之外呢,還需要在布局文件中定義出控件,注意一定要寫View所在類的完整包名,在這里我的包名是xr.customswitch.view.CustomSwitchView
<xr.customswitch.view.CustomSwitchView
android:id="@+id/csv_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />;
好了進行到這一步的話,我們的自定義控件就算繪制出來了。但是一個控件繪制出來還不算一個完整的控件,所以我們還需要添加一些事件監聽。
3、觸摸事件
在寫觸摸事件之前,我們需要聲明一些參數。首先開關在開或者關的時候一定有個狀態(isSwitchState),我們必須要根據這個狀態來處理一些邏輯問題,所以這個狀態我們必須要明確。其次由于是觸摸事件,所以我們還需要一個觸摸狀態(isTouchState),根據觸摸狀態我們處理觸摸事件邏輯。而如何知道開關狀態和觸摸狀態呢,當然是根據前景圖中的開關滑塊相對于背景圖的位置來確定,而這個開關一定是處于背景圖中的,不能超過背景圖的范圍,所以我們必須明確當前開關位置(currentPosition)和這個開關能滑動的最大位置(maxPosition)。
private boolean isSwitchState = true; //開關狀態
private boolean isTouchState = false; //觸摸狀態
private float currentPosition; // 當前開關位置
private int maxPosition; // 開關滑動最大位置</code></pre>
定義好相關參數及變量后,我們需要知道開關位置的參數規定,直接上圖吧。

開關參數規定
下面我們就需要重寫 onTouchEvent( )與 onDraw() 方法了,由于本文主要講的是自定義控件的步驟,所以具體邏輯上的處理就看注釋吧,寫的很詳細:
/**
- @Title: onTouchEvent
- @Description:觸摸事件
@return: void
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 處于觸摸狀態
isTouchState = true;
// 得到位置坐標
currentPosition = event.getX();
break;
case MotionEvent.ACTION_MOVE:
currentPosition = event.getX();
break;
case MotionEvent.ACTION_UP:
// 觸摸狀態結束
isTouchState = false;
currentPosition = event.getX();
// 中間標志位置
float centerPosition = switchBackgroupBitmap.getWidth() / 2.0f;
// 如果開關當前位置大于背景位置的一半 顯示關 否則顯示開
boolean currentState = currentPosition > centerPosition;
// 當前狀態置為開關狀態
isSwitchState = currentState;
break;
}
// 重新調用onDraw方法,不斷重繪界面
invalidate();
return true;
}
/**
- @Title: onDraw
- @Description:繪制控件
@return: void
*/
@Override
protected void onDraw(Canvas canvas) {
// 先繪制背景
canvas.drawBitmap(switchBackgroupBitmap, 0, 0, paint);
// 如果處于觸摸狀態
if (isTouchState) {
// 觸摸位置在開關的中間位置
float movePosition = currentPosition - switchForegroupBitmap.getWidth() / 2.0f;
maxPosition = switchBackgroupBitmap.getWidth() - switchForegroupBitmap.getWidth();
// 限定開關滑動范圍 只能在 0 - maxPosition范圍內
if (movePosition < 0) {
movePosition = 0;
} else if (movePosition > maxPosition) {
movePosition = maxPosition;
}
// 繪制開關
canvas.drawBitmap(switchForegroupBitmap, movePosition, 0, paint);
}
// 直接繪制開關
else {
// 如果是真,直接將開關滑塊置為開啟狀態
if (isSwitchState) {
maxPosition = switchBackgroupBitmap.getWidth() - switchForegroupBitmap.getWidth();
canvas.drawBitmap(switchForegroupBitmap, maxPosition, 0, paint);
} else {
// 否則將開關置為關閉狀態
canvas.drawBitmap(switchForegroupBitmap, 0, 0, paint);
}
}
}</code></pre>
這里有幾個需要注意的問題:
第一觸摸事件的返回值一定要返回 true ,目的是讓觸摸事件一直生效。也就是Touch中的事件消費。
第二就是在觸摸事件返回之前,我們需要重新繪制控件,這個時候我們沒辦法直接調用 onDraw() 方法,Android給我們提供了一個方法 invalidate() ,這個方法的目的就是重新調用一次 onDraw() 方法,十分方便。
第三就是開關位置的判定,因為我們只有兩個狀態,那么如果開關已經劃過了背景寬度的一半,那么我們就判定開關位置已經變化。當然也要注意滑塊的位置范圍在0~maxPosition之間。
到這里我們的觸摸事件就算全部搞定了,但是我們知道,開關滑動后需要完成相關邏輯處理。這個時候就需要一個事件監聽者,來實時監聽開關狀態的變化。
4、事件監聽者
事件監聽者我們不會很陌生,經常使用到的是 onClickListener() ,我們就仿照這個類來實現開關狀態的監聽。
首先我們需要聲明一個狀態監聽接口對象,并添加監聽方法用來Acitivity中的控件來調用。
/**
- @ClassName: OnSwitchStateUpdateListener
- @Description:添加事件狀態監聽接口對象
- @author: iamxiarui@foxmail.com
@date: 2016年5月5日 下午9:33:35
*/
public interface OnSwitchStateUpdateListener {
// 狀態回調, 把當前狀態傳出去
void onStateUpdate(boolean state);
}
/**
- @Title: setOnSwitchStateUpdateListener
- @Description:狀態監聽方法
- @return: void
*/
public void setOnSwitchStateUpdateListener(OnSwitchStateUpdateListener
onSwitchStateUpdateListener) {
this.onSwitchStateUpdateListener = onSwitchStateUpdateListener;
}</code></pre>
然后需要在合適的位置來處理監聽的相關邏輯,在這個控件中,我們最好在觸摸事件中的 MotionEvent.ACTION_UP 監聽,因為開關的變化一定是觸摸點抬起后開始變化,所以我們需要在判斷開關位置與改變開關狀態之間執行監聽方法:
/**
- @Title: onTouchEvent
- @Description:觸摸事件
@return: void
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
...
// 如果開關當前位置大于背景位置的一半 顯示關 否則顯示開
boolean currentState = currentPosition > centerPosition;
// 如果當然狀態不相同且綁定了監聽對象 則執行監聽方法
if (currentState != isSwitchState && onSwitchStateUpdateListener != null) {
onSwitchStateUpdateListener.onStateUpdate(currentState);
}
// 當前狀態置為開關狀態
isSwitchState = currentState;
break;
}
// 重新調用onDraw方法,不斷重繪界面
invalidate();
return true;
}</code></pre>
這個時候我們再在Activity中給控件綁定監聽事件,并處理相關邏輯:
// 綁定監聽事件
buttonCSView.setOnSwitchStateUpdateListener(new OnSwitchStateUpdateListener() {
@Override
public void onStateUpdate(boolean state) {
if (state) {
Toast.makeText(MainActivity.this, "打開", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "關閉", Toast.LENGTH_SHORT).show();
}
}
});</code></pre>
好了,至此可以說一個自定義控件的基本工作已經完成,現在這個控件已經能夠正常使用了。簡單回顧一下,還是比較復雜的。因為我們添加復寫了很多方法,而一些方法的功能其實就是簡單的設置一些圖片,并沒有一些復雜的功能。我們知道Android中控件的一些屬性可以直接在XML文件中定義,那么我們是否可以自定義一些屬性并直接在XML文件引用呢?答案是肯定的,也就是接下來要說的自定義屬性。
三、自定義屬性
在說自定義屬性之前,我們先明確幾個概念。每次我們創建XML布局文件的時候都會有這樣一句代碼:
xmlns:android="http://schemas.android.com/apk/res/android"
由于是自動創建的,所以我們很少注意到這句話。其實這行代碼的意思是指定命名空間,用于在一個XML文檔中提供名字唯一的元素和屬性。也就是指定了一個命名空間叫做 android ,然后后面跟上空間的地址。這樣我們才能夠使用一些比如 android : id 這樣的屬性。
其次自定義屬性一般是在 values 文件夾下的 attrs.xml 文件中定義好的。格式如下:
<declare-styleable name = "名稱">
<attr name = "屬性名稱" format = "屬性類型" />
</declare-styleable></code></pre>
其中屬性的類型一般分為以下幾種:
- reference:某一資源ID。
- color:顏色值。
- boolean:布爾值。
- dimension:尺寸值。注意,這里如果是dp那就會做像素轉換。
- float:浮點值。
- integer:整型值。
- string:字符串
- fraction:百分數。
- enum:枚舉值。
- flag:自定義,里面對應了自定義的屬性值。
- reference|color:顏色的資源文件。
- reference|boolean:布爾值的資源文件
而在本例中,我們只需要設置前景圖、背景圖和初識開關狀態即可,所以我們在文件中這樣定義:
<resources>
<!-- 自定義屬性 -->
<declare-styleable name="CustomSwitchView">
<attr name="switch_background" format="reference" />
<attr name="switch_foreground" format="reference" />
<attr name="switch_state" format="boolean" />
</declare-styleable>
</resources></code></pre>
定義好之后,我們就可以在XML文件中增加命名空間與這些屬性了,注意命名空間一定要是自己的包名,至于空間名稱當然是自己隨便寫。
xmlns:customswitch="
customswitch:switch_background="@drawable/switch_background"
customswitch:switch_foreground="@drawable/switch_foreground"
customswitch:switch_state="true"</code></pre>
但是注意,我們雖然可以增加這些屬性,但是現在還不能運行。還記得之前四個構造函數么?其中第二個構造函數中有一個參數就是 AttributeSet ,也就是自定義的屬性文件。所以我們還需要重寫這個構造函數。
/**
@Description:用于在XML中使用,可以指定自定義屬性
*/
public CustomSwitchView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
// 設置命名空間
String namespace = "
// 通過命名空間 和 屬性名稱 找到對應的資源對象
int switchBackgroundResource =
attrs.getAttributeResourceValue(namespace, "switch_background", -1);
int switchForegroundResource =
attrs.getAttributeResourceValue(namespace, "switch_foreground", -1);
isSwitchState = attrs.getAttributeBooleanValue(namespace, "switch_state", false);
// 將資源對象設置到對應位置
setBackgroundPic(switchBackgroundResource);
setForegroundPic(switchForegroundResource);
}</code></pre>
這個時候,當自定義控件被創建的時候會自動調用這個構造函數,而在布局文件中設置的屬性就能夠正常使用了。
四、總結
好了,通過這么長篇幅的講解,完全自定義控件應該已經全部說明白了。現在來總結一下細節上的注意事項吧。
- 新建的自定義控件類按照需求選擇繼承View/ViewGroup或者已有的控件。
- 新建控件類根據需求實現相應的構造函數。
- 新建類最好放在指定包下,比如view包。
- XML文件一定要寫出自定義控件的全路徑。
- 添加事件監聽器后要在合適的位置執行狀態監聽。
- 自定義屬性后需要復寫構造方法中帶有自定義屬性參數的方法。
學的時候很多細節不知道歸納,寫出來才能讓自己印象深刻。由于作者還是在校學生一枚,水平有限,只是想把自己所學用文字分享出來,如有錯誤或者不同意見請多加指教,謝謝。
項目源碼:
via: iamxiarui