Android自定義View實現(xiàn)開關(guān)按鈕
前言:Android自定義View對于剛?cè)腴T乃至工作幾年的程序員來說都是非??謶值模彩茿ndroid進(jìn)階學(xué)習(xí)的必經(jīng)之路,平時項目中經(jīng)常會有一些苛刻的需求,我們可以在GitHub上找到各種各樣的效果,能用則用,不能用自己花功夫改改也能草草了事。不過隨著工作經(jīng)驗和工作性質(zhì),越來越覺得自定義View是時候有必要自己花點功夫研究一下。
一、經(jīng)過這兩天的努力,自己也嘗試著寫了一個Demo,效果很簡單,就是開關(guān)按鈕的實現(xiàn)。
可能有的人會說這效果so easy,找UI切三張圖就完事了,何必大費周折自定義。你說的沒錯,不過這里只是用來學(xué)習(xí)自定義View來展示這么一個常見案例。

自定義控件
1.為什么自定義View?
Android自身帶的控件不能滿足需求, 需要根據(jù)自己的需求定義控件.
2.Android 的界面繪制流程?
onMeasure()——onLayout()——onDraw()方法都在Activity生命周期的onResume()方法之后執(zhí)行。
3.Android自定義View的方式?
集成View:View流程
onMeasure() (在這個方法里指定自己的寬高) -> onDraw() (繪制自己的內(nèi)容)
集成ViewGroup:ViewGroup流程
onMeasure() (指定自己的寬高, 所有子View的寬高)-> onLayout() (擺放所有子View) -> onDraw() (繪制內(nèi)容)
自定義View實現(xiàn)開關(guān)按鈕步驟:
寫個類繼承View,
拷貝包含包名的全路徑到xml中,
界面中找到該控件, 設(shè)置初始信息,
根據(jù)需求繪制界面內(nèi)容,
響應(yīng)用戶的觸摸事件,
創(chuàng)建一個狀態(tài)更新監(jiān)聽.
1.自定義ToggleView集成View,并且重新三個構(gòu)造方法。
注意:構(gòu)造方法為什么要重寫三個?
ToggleView(Context context)一個參數(shù)的構(gòu)造方法是用于代碼創(chuàng)建控件時調(diào)用的
ToggleView(Context context, AttributeSet attrs)用于在xml里使用, 可指定自定義屬性
ToggleView(Context context, AttributeSet attrs, int defStyle)用于在xml里使用, 可指定自定義屬性, 如果指定了樣式, 則走此構(gòu)造函數(shù)
我們在XML中定義了背景圖片、開關(guān)按鈕圖片和開關(guān)默認(rèn)狀態(tài),要獲取在XML文件定義的屬性就在包含三個參數(shù)的構(gòu)造方法里用TypedArray類來獲取。
在attrs.xml聲明節(jié)點declare-styleable
<declare-styleable name="ToggleView">
<attr name="switch_background" format="reference" />
<attr name="slide_button" format="reference" />
<attr name="switch_state" format="boolean" />
</declare-styleable>
/**
* 用于在xml里使用, 可指定自定義屬性, 如果指定了樣式, 則走此構(gòu)造函數(shù)
* @param context
* @param attrs
* @param defStyle
*/
public ToggleView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// 獲取配置的自定義屬性
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ToggleView, defStyle, 0);
int switchBackgroundResource = a.getResourceId(R.styleable.ToggleView_switch_background, -1);
int slideButtonResource = a.getResourceId(R.styleable.ToggleView_slide_button, -1);
mSwitchState = a.getBoolean(R.styleable.ToggleView_switch_state, false);
//獲取背景圖片和開關(guān)圖片后設(shè)置圖片,便于在onMeasure()方法中設(shè)置View寬和高,防止Null
setSwitchBackgroundResource(switchBackgroundResource);
setSlideButtonResource(slideButtonResource);
init();
}
2.自定義ToggleView集成View后,在XML文件里不要忘記添加命名空間
“xmlns:cb=”http://schemas.android.com/apk/res-auto””
然后將自定義View的完整路徑粘貼到XML中,這點類似于Android v4包下的ViewPager控件
以下便是demo中XML文件代碼:
設(shè)置開關(guān)背景圖片
- cb:switch_background=”@drawable/switch_background”
設(shè)置開關(guān)按鈕圖片
- cb:slide_button=”@drawable/slide_button”
設(shè)置開關(guān)默認(rèn)狀態(tài)
- cb:switch_state=”false”

3.界面中找到該控件, 設(shè)置初始信息
在Activity中通過findViewById方法找到自定義的View控件,和系統(tǒng)的組件操作沒區(qū)別。
private ToggleView toggleView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
toggleView = (ToggleView) findViewById(R.id.toggleView);
// toggleView.setSwitchBackgroundResource(R.drawable.switch_background);
// toggleView.setSlideButtonResource(R.drawable.slide_button);
// toggleView.setSwitchState(true);
//
// 設(shè)置開關(guān)更新監(jiān)聽
toggleView.setOnSwitchStateUpdateListener(new ToggleView.OnSwitchStateUpdateListener(){
@Override
public void onStateUpdate(boolean state) {
Toast.makeText(getApplicationContext(), "state: " + state, Toast.LENGTH_SHORT).show();
}
});
}
4.根據(jù)需求繪制界面內(nèi)容
已經(jīng)通過onMeasure()方法設(shè)置了View的寬度和高度,下面開始繪制的操作就全部在onDraw()方法中進(jìn)行,onDraw(Canvas canvas) 方法中canvas參數(shù):畫布, 畫板. 在上邊繪制的內(nèi)容都會顯示到界面上.
// 根據(jù)開關(guān)狀態(tài)boolean, 直接設(shè)置圖片位置
if(mSwitchState){// 開
int newLeft = switchBackgroupBitmap.getWidth() - slideButtonBitmap.getWidth();
canvas.drawBitmap(slideButtonBitmap, newLeft, 0, paint);
}else {// 關(guān)
canvas.drawBitmap(slideButtonBitmap, 0, 0, paint);
}
開關(guān)打開時,開關(guān)按鈕的位置在開關(guān)背景中的位置計算:
int newLeft = switchBackgroupBitmap.getWidth() -
slideButtonBitmap.getWidth(); 背景的寬度-按鈕的寬度就是當(dāng)前開關(guān)按鈕所在的X軸上的位置點
開關(guān)關(guān)閉時,當(dāng)前開關(guān)按鈕所在的X軸上的位置點=0
5.響應(yīng)用戶的觸摸事件
在完成以上3步操作后,你會發(fā)現(xiàn),只有在第一次進(jìn)入后XML初始化默認(rèn)開關(guān)狀態(tài)的boolean值才會有變化,此后點擊是沒有任何效果的,這個時候我們就要想辦法監(jiān)聽手勢事件,重寫onTouchEvent(MotionEvent event)方法,相信大多數(shù)朋友對這個方法并不陌生。
MotionEvent有三種狀態(tài):
MotionEvent.ACTION_DOWN: //按下屏幕
MotionEvent.ACTION_MOVE: //手指在屏幕上移動
MotionEvent.ACTION_UP //離開屏幕
當(dāng)前需要考慮的問題是:
當(dāng)手指按下屏幕后MotionEvent.ACTION_DOWN(在當(dāng)前開關(guān)背景View中)開關(guān)的X軸位置應(yīng)該移動到手指按下的位置;
當(dāng)手指在屏幕上移動MotionEvent.ACTION_MOVE(在當(dāng)前開關(guān)背景View中)開關(guān)按鈕X軸應(yīng)該隨著手指移動的位置改變;
當(dāng)手指離開屏幕后MotionEvent.ACTION_UP(在當(dāng)前開關(guān)背景View中)開關(guān)按鈕應(yīng)該判斷手指離開的位置是否是當(dāng)前背景的一半位置,如果X軸位置大于View背景寬度的1/2、那么應(yīng)該處于打開狀態(tài),如果X軸位置小于View背景寬度的1/2,那么應(yīng)該處于關(guān)閉狀態(tài)。
如圖所示:

private OnSwitchStateUpdateListener onSwitchStateUpdateListener;
// 重寫觸摸事件, 響應(yīng)用戶的觸摸.
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isTouchMode = true;
System.out.println("event: ACTION_DOWN: " + event.getX());
currentX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
System.out.println("event: ACTION_MOVE: " + event.getX());
currentX = event.getX();
break;
case MotionEvent.ACTION_UP:
isTouchMode = false;
System.out.println("event: ACTION_UP: " + event.getX());
currentX = event.getX();
float center = switchBackgroupBitmap.getWidth() / 2.0f;
// 根據(jù)當(dāng)前按下的位置, 和控件中心的位置進(jìn)行比較.
boolean state = currentX > center;
// 如果開關(guān)狀態(tài)變化了, 通知界面. 里邊開關(guān)狀態(tài)更新了.
if(state != mSwitchState && onSwitchStateUpdateListener != null){
// 把最新的boolean, 狀態(tài)傳出去了
onSwitchStateUpdateListener.onStateUpdate(state);
}
mSwitchState = state;
break;
default:
break;
}
// 重繪界面
invalidate(); // 會引發(fā)onDraw()被調(diào)用, 里邊的變量會重新生效.界面會更新
return true; // 消費了用戶的觸摸事件, 才可以收到其他的事件.
}
注意:
以上監(jiān)聽onTouchEvent(MotionEvent
event)方法后還存在一個問題,不知道大家有沒有發(fā)現(xiàn),我們沒有設(shè)置開關(guān)按鈕的邊界值,什么意思呢?就是手指滑動的時候左邊和右邊可以畫出當(dāng)前背景之外。
所以這里需要對左右兩邊的X軸位置進(jìn)行處理:
// Canvas 畫布, 畫板. 在上邊繪制的內(nèi)容都會顯示到界面上.
@Override
protected void onDraw(Canvas canvas) {
// 1. 繪制背景
canvas.drawBitmap(switchBackgroupBitmap, 0, 0, paint);
// 2. 繪制滑塊
if(isTouchMode){
// 根據(jù)當(dāng)前用戶觸摸到的位置畫滑塊
// 讓滑塊向左移動自身一半大小的位置
float newLeft = currentX - slideButtonBitmap.getWidth() / 2.0f;
int maxLeft = switchBackgroupBitmap.getWidth() - slideButtonBitmap.getWidth();
// 限定滑塊范圍
if(newLeft < 0){
newLeft = 0; // 左邊范圍
}else if (newLeft > maxLeft) {
newLeft = maxLeft; // 右邊范圍
}
canvas.drawBitmap(slideButtonBitmap, newLeft, 0, paint);
}else {
// 根據(jù)開關(guān)狀態(tài)boolean, 直接設(shè)置圖片位置
if(mSwitchState){// 開
int newLeft = switchBackgroupBitmap.getWidth() - slideButtonBitmap.getWidth();
canvas.drawBitmap(slideButtonBitmap, newLeft, 0, paint);
}else {// 關(guān)
canvas.drawBitmap(slideButtonBitmap, 0, 0, paint);
}
}
}
6.創(chuàng)建一個狀態(tài)更新監(jiān)聽.
基本上所以工作已經(jīng)完成,這樣我們一個自定義View已經(jīng)大功告成了,當(dāng)你完成這個效果后,你可能會發(fā)現(xiàn)有點類似于CheckBox。既然類似于CheckBox,我們知道當(dāng)CheckBox點擊選中和取消選中的時候都會有ischecked()方法來獲取選中狀態(tài),所以我們這個自定義的開關(guān)按鈕自然不能少這個功能,否則我們在界面上只有效果展示,卻沒有邏輯處理的地方。
public interface OnSwitchStateUpdateListener{
// 狀態(tài)回調(diào), 把當(dāng)前狀態(tài)傳出去
void onStateUpdate(boolean state);
}
public void setOnSwitchStateUpdateListener(
OnSwitchStateUpdateListener onSwitchStateUpdateListener) {
this.onSwitchStateUpdateListener = onSwitchStateUpdateListener;
}
代碼很簡單,寫一個接口,然后定義一個回調(diào)方法返回開關(guān)狀態(tài),需要注意的是,在手指離開屏幕的時候,我們需要判斷此次操作是否改變了開關(guān)的狀態(tài),如果沒有變化我們不做操作,如果跟上次狀態(tài)不同,則通知Activity狀態(tài)更改!
case MotionEvent.ACTION_UP:
isTouchMode = false;
System.out.println("event: ACTION_UP: " + event.getX());
currentX = event.getX();
float center = switchBackgroupBitmap.getWidth() / 2.0f;
// 根據(jù)當(dāng)前按下的位置, 和控件中心的位置進(jìn)行比較.
boolean state = currentX > center;
// 如果開關(guān)狀態(tài)變化了, 通知界面. 里邊開關(guān)狀態(tài)更新了.
if(state != mSwitchState && onSwitchStateUpdateListener != null){
// 把最新的boolean, 狀態(tài)傳出去了
onSwitchStateUpdateListener.onStateUpdate(state);
}
mSwitchState = state;
break;
Activity中回調(diào)也是非常簡單的,類似于Android系統(tǒng)為我們提供的setOnClickListener回調(diào)接口,之前一直用系統(tǒng)定義的監(jiān)聽接口,這次通過簡單的一個自定義View,我們也可以給自己的View寫回調(diào)接口了。是不是覺得是件很開心的事情呢?
// 設(shè)置開關(guān)更新監(jiān)聽
toggleView.setOnSwitchStateUpdateListener(new ToggleView.OnSwitchStateUpdateListener(){
@Override
public void onStateUpdate(boolean state) {
Toast.makeText(getApplicationContext(), "state: " + state, Toast.LENGTH_SHORT).show();
}
});
以上所述是小編給大家介紹的Android自定義View實現(xiàn)開關(guān)按鈕,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關(guān)文章
Android 監(jiān)聽Notification 被清除實例代碼
本文主要介紹Android 監(jiān)聽Notification 事件,這里給大家提供實例代碼進(jìn)行參考,有需要的小伙伴可以參考下2016-07-07
Android Studio升級到3.0 Terminal 中文顯示異常解決
本篇文章主要介紹了Android Studio升級到3.0 Terminal 中文顯示異常解決,非常具有實用價值,需要的朋友可以參考下2017-10-10
Android編程實現(xiàn)ActionBar的home圖標(biāo)動畫切換效果
這篇文章主要介紹了Android編程實現(xiàn)ActionBar的home圖標(biāo)動畫切換效果,涉及Android布局、樣式、Activity及菜單相關(guān)操作技巧,需要的朋友可以參考下2017-01-01
Android 實例開發(fā)基于ArcSoft實現(xiàn)人臉識別
人臉識別,是基于人的臉部特征信息進(jìn)行身份識別的一種生物識別技術(shù)。用攝像機(jī)或攝像頭采集含有人臉的圖像或視頻流,并自動在圖像中檢測和跟蹤人臉,進(jìn)而對檢測到的人臉進(jìn)行識別的一系列相關(guān)技術(shù),通常也叫做人像識別、面部識別2021-11-11
Android開發(fā)實現(xiàn)長按返回鍵彈出關(guān)機(jī)框功能
這篇文章主要介紹了Android開發(fā)實現(xiàn)長按返回鍵彈出關(guān)機(jī)框功能,涉及Android針對長按事件的響應(yīng)與處理相關(guān)操作技巧,需要的朋友可以參考下2017-09-09
AndroidStudio3.6.1打包jar及AndroidStudio4.0打包jar的一系列問題及用法
這篇文章主要介紹了AndroidStudio3.6.1打包jar,AndroidStudio4.0打包jar的問題及用法,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03

