Android自定義控件系列二:自定義開關按鈕(一)

這一次我們將會實現一個完整純粹的自定義控件,而不是像之前的組合控件一樣,拿系統的控件來實現;計劃分爲三部分:自定義控件的基本部分自定義控件的觸摸事件的處理自定義控件的自定義屬性

下面就開始第一部分的編寫,本次以一個定義的開關按鈕爲例,下面就開始吧:


先看看效果,一個點擊開關按鈕,實現點擊切換開關狀態:



爲了能夠講解清晰,還是來一些基本的介紹。

首先需要明確的就是自定義控件還是繼承自View這個類,Google在View這個類裏面提供了相當多的方法供我們使用,使用這些方法我們可以實現相當多的效果和功能,在這裏需要用到幾個主要的方法;


自定義控件的步驟、用到的主要方法:


1、首先需要定義一個類,繼承自View;對於繼承View的類,會需要實現至少一個構造方法;實際上這裏一共有三個構造方法:


public View (Context context)是在java代碼創建視圖的時候被調用(使用new的方式),如果是從xml填充的視圖,就不會調用這個


public View (Context context, AttributeSet attrs)這個是在xml創建但是沒有指定style的時候被調用


public View (Context context, AttributeSet attrs, int defStyle)這個是在第二個基礎上添加style的時候被調用的


所以對於這裏來說,如果不使用style, 我們重點關注第二個構造方法即可



2、對於任何一個控件來說,它需要顯示在我們的界面上,那麼肯定需要定義它的大小;

在這裏Google提供了一個方法:protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);我們去看這個方法的內部,實際上是調用了protected final void setMeasuredDimension(int measuredWidth, int measuredHeight);這個方法,其中第一個參數是view的寬,第二個參數是view的高,這樣我們就可以設置view的寬高了,但是要注意,這樣設置的單位都是像素



3、對於一個需要顯示的控件來說,我們往往還需要確定它的位置:

這就要求重寫onLayout方法;但是實際上這個方法在自定義view的時候使用的不多,原因是因爲對於位置來說,控件只有建議權而沒有決定權,決定權一般在父控件那裏。


4、對於一個控件,需要顯示,我們當然需要將它繪製出來,這裏就需要重寫onDraw方法,來將這個控件繪製出來



5、當控件狀態改變的時候,我們很可能需要刷新view的顯示狀態,這時候就需要調用invalidate()方法,這個方法實際上會重新調用onDraw方法來重繪控件



6、在定義控件的過程中,如果需要對view設置點擊事件,可以直接使用setOnClickListener方法,而不需要寫view.setOnClickListener;



7、在佈局文件中將這個自定義控件定義出來,注意名字要使用全類名;而且,由於是繼承自view控件,所以在xml文件中如果是view本身的屬性都可以直接使用,比如:android:layout_width等等



這裏比較關鍵的地方就在於這個onDraw方法,我們一起來看一下:


[java]  view plain  copy
  1. /** 
  2.      * 畫view的方法,繪製當前view的內容 
  3.      */  
  4.     @Override  
  5.     protected void onDraw(Canvas canvas) {  
  6.         // super.onDraw(canvas);  
  7.   
  8.   
  9.         Paint paint = new Paint();  
  10.         // 打開抗鋸齒  
  11.         paint.setAntiAlias(true);  
  12.   
  13.   
  14.         // 畫背景  
  15.         canvas.drawBitmap(backgroundBitmap, 00, paint);  
  16.         // 畫滑塊  
  17.         canvas.drawBitmap(slideButton, slideBtn_left, 0, paint);  
  18.     }  



onDraw方法傳入的參數是一個Canvas畫布對象,這個實際上跟Java中的差不太多,我們要在畫布上畫畫也需要一個畫筆,我們這裏也將其初始化出來Paint paint = new Paint(),同時設置了一個抗鋸齒效果paint.setAntiAlias(true),然後調用drawBitmap的方法,先後繪製了開關的背景和開關的滑塊,分別入下圖:


                          


這裏要注意的一點就是,drawBitmap(Bitmap bitmap, float left, float top, Paint paint)方法中間的兩個float類型的參數,分別代表繪製圖形的左上角的x和y的座標(原點設置在左上角),所以這裏如果我們個繪製座標都傳入0,0,那麼開關會處在一個關的狀態,這裏,我們對於滑塊使用了一個變量slideBtn_left來設置其位置,那麼對於關閉狀態,slideBtn_left的值就應該爲0,對於開啓狀態,slideBtn_left的值就應該是backgroundBitmap(背景)的寬度減去slideButton(滑塊)的寬度


那麼這樣一來,機制就比較清楚了,我們只需要在控件上設置一個點擊事件,同時設置一個boolean變量代表開關的狀態,當點擊的時候,切換這個boolean類型的變量爲true或者false,同時變化slideButton的值爲0或者backgroundBitmap.getWidth()-slideButton.getWidth(),然後再調用invalidate()方法刷新控件,就可以實現基本的開關功能了


下面來看具體的代碼,註解比較詳細:


自定義控件的類MyToggleButton.java,繼承自View:

[java]  view plain  copy
  1. package com.example.togglebutton.ui;  
  2.   
  3. import com.example.togglebutton.R;  
  4.   
  5. import android.content.Context;  
  6. import android.graphics.Bitmap;  
  7. import android.graphics.BitmapFactory;  
  8. import android.graphics.Canvas;  
  9. import android.graphics.Paint;  
  10. import android.util.AttributeSet;  
  11. import android.view.View;  
  12.   
  13. /* 
  14.  * 自定義view的幾個步驟: 
  15.  * 1、首先需要寫一個類來繼承自View 
  16.  * 2、需要得到view的對象,那麼需要重寫構造方法,其中一參的構造方法用於new,二參的構造方法用於xml佈局文件使用,三參的構造方法可以傳入一個樣式 
  17.  * 3、需要設置view的大小,那麼需要重寫onMeasure方法 
  18.  * 4、需要設置view的位置,那麼需要重寫onLayout方法,但是這個方法在自定義view的時候用的不多,原因主要在於view的位置主要是由父控件來決定 
  19.  * 5、需要繪製出所需要顯示的view,那麼需要重寫onDraw方法 
  20.  * 6、當控件狀態改變的時候,需要重繪view,那麼調用invalidate();方法,這個方法實際上會重新調用onDraw方法 
  21.  * 7、在這其中,如果需要對view設置點擊事件,可以直接調用setOnClickListener方法 
  22.  */  
  23.   
  24. public class MyToggleButton extends View {  
  25.   
  26.     /** 
  27.      * 開關按鈕的背景 
  28.      */  
  29.     private Bitmap backgroundBitmap;  
  30.     /** 
  31.      * 開關按鈕的滑動部分 
  32.      */  
  33.     private Bitmap slideButton;  
  34.     /** 
  35.      * 滑動按鈕的左邊界 
  36.      */  
  37.     private float slideBtn_left;  
  38.     /** 
  39.      * 當前開關的狀態 
  40.      */  
  41.     private boolean currentState = false;  
  42.   
  43.     /** 
  44.      * 在代碼裏面創建對象的時候,使用此構造方法 
  45.      *  
  46.      * @param context 
  47.      */  
  48.     public MyToggleButton(Context context) {  
  49.         super(context);  
  50.     }  
  51.   
  52.     /** 
  53.      * 在佈局文件中聲明的view,創建時由系統自動調用 
  54.      *  
  55.      * @param context 
  56.      * @param attrs 
  57.      */  
  58.     public MyToggleButton(Context context, AttributeSet attrs) {  
  59.         super(context, attrs);  
  60.         initView();  
  61.     }  
  62.   
  63.     /** 
  64.      * 測量尺寸時的回調方法 
  65.      */  
  66.     @Override  
  67.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  68.         // super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  69.         // 設置當前view的大小 width:view的寬,單位都是像素值 heigth:view的高,單位都是像素值  
  70.         setMeasuredDimension(backgroundBitmap.getWidth(),  
  71.                 backgroundBitmap.getHeight());  
  72.     }  
  73.   
  74.     // 這個方法對於自定義view的時候幫助不大,因爲view的位置一般由父組件來決定的  
  75.     @Override  
  76.     protected void onLayout(boolean changed, int left, int top, int right,  
  77.             int bottom) {  
  78.         super.onLayout(changed, left, top, right, bottom);  
  79.     }  
  80.   
  81.     /** 
  82.      * 畫view的方法,繪製當前view的內容 
  83.      */  
  84.     @Override  
  85.     protected void onDraw(Canvas canvas) {  
  86.         // super.onDraw(canvas);  
  87.   
  88.         Paint paint = new Paint();  
  89.         // 打開抗鋸齒  
  90.         paint.setAntiAlias(true);  
  91.   
  92.         // 畫背景  
  93.         canvas.drawBitmap(backgroundBitmap, 00, paint);  
  94.         // 畫滑塊  
  95.         canvas.drawBitmap(slideButton, slideBtn_left, 0, paint);  
  96.     }  
  97.   
  98.     /** 
  99.      * 初始化view 
  100.      */  
  101.     private void initView() {  
  102.         backgroundBitmap = BitmapFactory.decodeResource(getResources(),  
  103.                 R.drawable.switch_background);  
  104.         slideButton = BitmapFactory.decodeResource(getResources(),  
  105.                 R.drawable.slide_button);  
  106.   
  107.         /* 
  108.          * 點擊事件 
  109.          */  
  110.         setOnClickListener(new OnClickListener() {  
  111.   
  112.             @Override  
  113.             public void onClick(View v) {  
  114.   
  115.                 currentState = !currentState;  
  116.                 flushState();  
  117.                 flushView();  
  118.             }  
  119.         });  
  120.     }  
  121.   
  122.     /** 
  123.      * 刷新視圖 
  124.      */  
  125.     protected void flushView() {  
  126.         // 刷新當前view會導致ondraw方法的執行  
  127.         invalidate();  
  128.     }  
  129.   
  130.     /** 
  131.      * 刷新當前的狀態 
  132.      */  
  133.     protected void flushState() {  
  134.         if (currentState) {  
  135.             slideBtn_left = backgroundBitmap.getWidth()  
  136.                     - slideButton.getWidth();  
  137.         } else {  
  138.             slideBtn_left = 0;  
  139.         }  
  140.     }  
  141.   
  142. }  


在佈局文件中將其定義出來:


[html]  view plain  copy
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     tools:context="${relativePackage}.${activityClass}" >  
  6.   
  7.   
  8.     <com.example.togglebutton.ui.MyToggleButton  
  9.         android:id="@+id/my_toggle_btn"  
  10.         android:layout_width="wrap_content"  
  11.         android:layout_height="wrap_content"  
  12.         android:layout_centerInParent="true" />  
  13.   
  14.   
  15. </RelativeLayout>  


在這裏由於沒有寫任何點擊觸發業務的邏輯,只是一個單純的控件,所以在MainActivity裏面沒有加入多的代碼:


[java]  view plain  copy
  1. package com.example.togglebutton;  
  2.   
  3. import android.app.Activity;  
  4. import android.os.Bundle;  
  5.   
  6. public class MainActivity extends Activity {  
  7.   
  8.     @Override  
  9.     protected void onCreate(Bundle savedInstanceState) {  
  10.         super.onCreate(savedInstanceState);  
  11.         setContentView(R.layout.activity_main);  
  12.   
  13.     }  
  14. }  


至此一個自定義的開關按鈕就完成了,後面兩篇將會介紹如何在上面實現 點擊拖動開關的效果 和如何實現自定義屬性