Android:初識自定義控件

AdaWootton 8年前發布 | 25K 次閱讀 Android Android開發 移動開發

一、什么是自定義控件

1、概念

簡單算來學習Android已經有一年時間了,從最初覺得別人寫的軟件好厲害到這么厲害的軟件我也能寫。但是現在還是會被有些軟件的UI和動畫所驚艷。一開始以為UI上的控件都是畫出來的,后來才知道這些控件都有一個共同的名字——自定義控件。

Android:初識自定義控件

自定義控件

為什么要自定義控件呢?當然不是為了簡單的好看。我們知道Android官方自帶的控件種類很多,基本能夠滿足日常的開發需求。但是一件產品的開發不僅僅需要功能上的完善,更要追求用戶體驗。所以單從用戶體驗上來說,官方的控件是遠遠談不上體驗的。所以越來越多的APP使用了自定義控件,一方面美觀好看,另一方面極大的提高了用戶體驗,何樂而不為呢?

而隨著Android技術越來越成熟,基本的控件有時已經滿足不了簡單的開發需求了,這個時候就需要我們自定義出滿足功能需求的控件來實現APP的一些需求。

2、實現方式

一般實現自定義控件會有三種方式:

  • 繼承已有的控件實現
  • 組合已有的控件實現
  • 完全自定義控件

第一種方式其實也就相當于擴展已有控件的功能,這種實現方式比較簡單;第二種組合方式目的是通過多種控件的組合來完成一種控件的需求,也就是通過這種方式自定義出來的控件具有多種基本控件的功能,更加強大,較第一種而言這種實現方式比較復雜;而第三種完全自定義控件這就更加復雜了,這需要我們新建一種控件繼承View/ViewGroup,并實現一些其中的屬性或方法。

總的來說,按照需求我們采取不同的方式。這里我們先說一下完全自定義控件的方式。

二、完全自定義控件

下面我就分享我最近學習黑馬教程中的一個自定義開關的過程。

Android:初識自定義控件

自定義開關

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:初識自定義控件

    界面繪制流程

    從圖中我們可以看出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>

定義好相關參數及變量后,我們需要知道開關位置的參數規定,直接上圖吧。

Android:初識自定義控件

開關參數規定

下面我們就需要重寫 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 ,也就是自定義的屬性文件。所以我們還需要重寫這個構造函數。

/**

 本文由用戶 AdaWootton 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!