自定義控件其實很簡單3/4

尊重原創轉載請註明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵權必究!

炮兵鎮樓

隱約雷鳴 陰霾天空 但盼風雨來 能留你在此
隱約雷鳴 陰霾天空 即使天無雨 我亦留此地

上一節我們細緻地、猥瑣地、小心翼翼地、猶如絲滑般撫摸、啊不,是講解了如何去測量一個佈局控件,再次強調,如我之前多次強調那樣,控件的測量必須要邏輯縝密嚴謹,儘量少地避免出現較大的邏輯錯誤。在整個系列撰寫的過程中,有N^N個朋友曾多次不間斷地小窗我問View是否也有生命週期,我也多次細心地、耐心地打開小窗然後默默地關掉它,不是我不願回答而是問的人太多我們乾脆就在blog中詳細闡述下,即便你是第一天學習Android,你也一定會用到Activity,用到Activity你一定會接觸到onCreate方法,然後你會從各種途徑瞭解到類似這樣的方法還有7個,我們稱之爲Activity生命週期:

  1. /** 
  2.  * 主界面 
  3.  *  
  4.  * @author Aige {@link http://blog.csdn.net/aigestudio} 
  5.  * @since 2014/11/17 
  6.  */  
  7. public class MainActivity extends Activity {  
  8.   
  9.     @Override  
  10.     public void onCreate(Bundle savedInstanceState) {  
  11.         super.onCreate(savedInstanceState);  
  12.     }  
  13.   
  14.     @Override  
  15.     protected void onStart() {  
  16.         super.onStart();  
  17.     }  
  18.   
  19.     @Override  
  20.     protected void onResume() {  
  21.         super.onResume();  
  22.     }  
  23.   
  24.     @Override  
  25.     protected void onPause() {  
  26.         super.onPause();  
  27.     }  
  28.   
  29.     @Override  
  30.     protected void onStop() {  
  31.         super.onStop();  
  32.     }  
  33.   
  34.     @Override  
  35.     protected void onRestart() {  
  36.         super.onRestart();  
  37.     }  
  38.   
  39.     @Override  
  40.     protected void onDestroy() {  
  41.         super.onDestroy();  
  42.     }  
  43. }  
Android framework在Activity的不同時期調用不同的生命週期方法並將其提供給我們以便我們能在Activity加載的不同時期根據自己的需要做不同的事,For example:我們可以在onCreate中設置我們的佈局文件或顯示的View,在onStop中處理Activity位於後臺時的操作,在onDestroy中銷燬一些不必要的強引用避免在Activity銷燬後造成泄漏等等等等,這些方法給我們控制Activity帶來了極大的便利,而在View中我們也學習了onMeasure、onLayout和onDraw這三個類似的方法,它們也是依次在View創建的不同時期被調用,那麼是否還應存在其他類似的方法呢?The answer is yes,View也提供了一下幾個類似“生命週期”的方法:


如上所示的這些方法,除了提到的“生命週期”方法外還有一些事件的回調,多說無益,我們還是來看看這些方法會在View的什麼時候被調用,老樣子我們新建一個繼承於View的子類並重寫這些方法:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/27 
  5.  *  
  6.  */  
  7. public class LifeCycleView extends View {  
  8.     private static final String TAG = "AigeStudio:LifeCycleView";  
  9.   
  10.     public LifeCycleView(Context context) {  
  11.         super(context);  
  12.         Log.d(TAG, "Construction with single parameter");  
  13.     }  
  14.   
  15.     public LifeCycleView(Context context, AttributeSet attrs) {  
  16.         super(context, attrs);  
  17.         Log.d(TAG, "Construction with two parameters");  
  18.     }  
  19.   
  20.     @Override  
  21.     protected void onFinishInflate() {  
  22.         super.onFinishInflate();  
  23.         Log.d(TAG, "onFinishInflate");  
  24.     }  
  25.   
  26.     @Override  
  27.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  28.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  29.         Log.d(TAG, "onMeasure");  
  30.     }  
  31.   
  32.     @Override  
  33.     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {  
  34.         super.onLayout(changed, left, top, right, bottom);  
  35.         Log.d(TAG, "onLayout");  
  36.     }  
  37.   
  38.     @Override  
  39.     protected void onSizeChanged(int w, int h, int oldw, int oldh) {  
  40.         super.onSizeChanged(w, h, oldw, oldh);  
  41.         Log.d(TAG, "onSizeChanged");  
  42.     }  
  43.   
  44.     @Override  
  45.     protected void onDraw(Canvas canvas) {  
  46.         super.onDraw(canvas);  
  47.         Log.d(TAG, "onDraw");  
  48.     }  
  49.   
  50.     @Override  
  51.     protected void onAttachedToWindow() {  
  52.         super.onAttachedToWindow();  
  53.         Log.d(TAG, "onAttachedToWindow");  
  54.     }  
  55.   
  56.     @Override  
  57.     protected void onDetachedFromWindow() {  
  58.         super.onDetachedFromWindow();  
  59.         Log.d(TAG, "onDetachedFromWindow");  
  60.     }  
  61.   
  62.     @Override  
  63.     protected void onWindowVisibilityChanged(int visibility) {  
  64.         super.onWindowVisibilityChanged(visibility);  
  65.         Log.d(TAG, "onWindowVisibilityChanged");  
  66.     }  
  67. }  
上面的方法中我過濾掉了事件和焦點觸發的方法,僅看View運行時被調用的方法,運行看看我們LogCat中的輸出:


首先是調用了構造方法,這是不用猜都該知道的,然後呢調用了onFinishInflate方法,這個方法當xml佈局中我們的View被解析完成後則會調用,具體的實現在LayoutInflater的rInflate方法中:

  1. public abstract class LayoutInflater {  
  2.     void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,  
  3.             boolean finishInflate) throws XmlPullParserException, IOException {  
  4.         // 省去無數代碼…………  
  5.   
  6.         if (finishInflate) parent.onFinishInflate();  
  7.     }  
  8. }  
也就是說如果我們不從xml佈局文件中解析的話,該方法就不會被調用,我們在Activity直接加載View的實例:

  1. public class MainActivity extends Activity {  
  2.     @Override  
  3.     public void onCreate(Bundle savedInstanceState) {  
  4.         super.onCreate(savedInstanceState);  
  5.         setContentView(new LifeCycleView(this));  
  6.     }  
  7. }  
這時再次運行我們的APP,就會發現不會再去調用onFinishInflate方法:


緊接着調用的是onAttachedToWindow方法,此時表示我們的View已被創建並添加到了窗口Window中,該方法後緊接着一般會調用onWindowVisibilityChanged方法,只要我們當前的Window窗口中View的可見狀態發生改變都會被觸發,這時View是被顯示了,隨後就會開始調用onMeasure方法對View進行測量,如果測量結果被確定則會先調用onSizeChanged方法通知View尺寸大小發生了改變,緊跟着便會調用onLayout方法對子元素進行定位佈局,然後再次調用onMeasure方法對View進行二次測量,如果測量值與上一次相同則不再調用onSizeChanged方法,接着再次調用onLayout方法,如果測量過程結束,則會調用onDraw方法繪製View。我們看到,onMeasure和onLayout方法被調用了兩次,很多童鞋會很糾結爲何onMeasure方法回被多次調用,其實沒必要過於糾結這個問題,onMeasure的調用取決於控件的父容器以及View Tree的結構,不同的父容器有不同的測量邏輯,比如上一節自定義控件其實很簡單2/3中,我們在SquareLayout測量子元素時就採取了二次測量,在API 19的時候Android對測量邏輯做了進一步的優化,比如在19之前只會對最後一次的測量結果進行Cache而在19開始則會對每一次測量結果都進行Cache,如果相同的代碼相同佈局相同的邏輯在19和19之前你有可能會看到不一樣的測量次數結果,所以沒必要去糾結這個問題,一般情況下只要你邏輯正確onMeasure都會得到正確的調用。

上面這些方法都很好理解,我們主要關心的是其調用流程,雖然上面我們通過LogCat的輸出大致瞭解了一下其執行順序,但是如果你好奇心足夠重,一定會想真是這樣的麼?在自定義控件其實很簡單7/12中我曾留下一個疑問:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/12 
  5.  *  
  6.  */  
  7. public class ImgView extends View {  
  8.     private Bitmap mBitmap;// 位圖對象  
  9.   
  10.     public ImgView(Context context, AttributeSet attrs) {  
  11.         super(context, attrs);  
  12.     }  
  13.   
  14.     @Override  
  15.     protected void onDraw(Canvas canvas) {  
  16.         // 繪製位圖  
  17.         canvas.drawBitmap(mBitmap, 00null);  
  18.     }  
  19.   
  20.     /** 
  21.      * 設置位圖 
  22.      *  
  23.      * @param bitmap 
  24.      *            位圖對象 
  25.      */  
  26.     public void setBitmap(Bitmap bitmap) {  
  27.         this.mBitmap = bitmap;  
  28.     }  
  29. }  
就是如上代碼片段是否有什麼問題?細心的盆友其實已經發現了,我們在onDraw中用到的mBitmap竟不爲null,按照我們上面分析的結果,如果順次調用View的各個方法,那麼此時如果我們在Activity中調用setBitmap方法爲我們的ImgView設置Bitmap:

  1. public class MainActivity extends Activity {  
  2.     private ImgView mImgView;  
  3.   
  4.     @Override  
  5.     public void onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         setContentView(R.layout.activity_main);  
  8.   
  9.         mImgView = (ImgView) findViewById(R.id.main_pv);  
  10.         Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lovestory);  
  11.         mImgView.setBitmap(bitmap);  
  12.     }  
  13. }  
的話onDraw方法獲取到的Bitmap應該爲null纔對,或者直白地說setBitmap方法應該在onDraw之後才被調用纔對。對麼?如果你這樣想,別說Android,甚至連Java基礎都不過關~~~爲什麼這麼說呢?記住上面這些除了構造方法外的onXXX方法,都是一系列的回調方法,當且僅當一定條件成立纔會被觸發,比如上面說過的onFinishInflate方法,只有在該View以及其子View從xml解析完畢纔會被調用,打個比方,如果我們編譯Android源代碼,嘗試在rInflate方法解析完xml生前View調用onFinishInflate之前調用我們的setBitmap方法,這時候你就會看到執行順序的改變。具體的原理設計太多的framework源碼,在該系列我就不在多貼一些系統的源碼了,如果你想更深地瞭解,我會在後續的《深入剖析Android GUI框架》系列中詳細闡述,這裏我僅作簡單的介紹,注意到上面我在說View的“生命週期”時使用了一個引號,雖然說到上面的一些方法會在View運行過程中依次被調用,但事實上真正稱得上View的生命週期的階段只有三個:

  • 處理控件動畫的階段
  • 處理測量的階段
  • 處理繪製的階段

Android的Animation動畫體系龐大不在本系列的講解範疇內,暫時Skip,測量和繪製的主要過程由我們之前所講的三個方法onMeasure、onLayout和onDraw所控制,這三個方法呢在framework中又主要由measure、layout、draw以及其派生方法所控制,在View中形成這樣一個體系:


再次注意:View的測量過程是由多個方法調用共同構成,measure和onMeasure僅僅代表該過程中的兩個方法而已。

如果控件繼承於ViewGroup實現的是一個佈局容器,那麼會多出一個dispatchDraw方法:


dispatchDraw方法本質上實現的是父容器對子元素的繪製分發,雖然邏輯不盡相同但是作用類似於draw,在高仿網易評論列表效果之界面生成中我們曾利用該方法在繪製子元素前繪製蓋樓背景,具體不再多說了。在我們調用setContentView方法後,如果你傳入的是一個資源文件ID,此時framework會使用LayoutInflater去解析佈局文件,當解析到我們自定義控件LifeCycleView的標籤時,通過反射獲取一個對應的LifeCycleView類實例,此時構造方法被調用,爾後開始解析LifeCycleView標籤下的各類屬性並存值,LifeCycleView標籤下的所有屬性(如果是個容器的話也會層層解析)解析完成後調用onFinishInflate方法表示當前LifeCycleView所有的(注意不是整個佈局哦僅僅是該View對應標籤)xml解析完畢,之後嘗試將View添加至當前Activity所在的Window,然後將處理UI事件的Msg壓入Message Queue開始至上而下地對整個View Tree進行測量,假設我們有如下的View Tree結構:


那麼我們的測量總是從根部RelativeLayout開始逐層往下進行調用,在Android翻頁效果原理實現之引入折線中我們曾在講滑動時對Message Queue作過一個簡單的淺析,當Msg壓入Queue並最終得到處理的這段過程並不是立即的,也就是說其中會有一定的延時,這相對於我們在setContentView後立即setBitmap來說時間要長很多很多,這也是爲什麼我們在onMeasure中獲取Bitmap不爲null的原因,具體的源碼邏輯實現會在《深入剖析Android GUI框架》深度講解,本系列除了後面要涉及到的事件分發外不會再涉及過多的源碼畢竟與基礎篇的定位不符,好了,這裏我再留一個問題,setBitmap和onMeasure、onLayout等這些回調方法之間是異步呢還是同步呢?其實答案很明顯了……OK,不說了,既然我們知道這樣直接setBitmap是不對的(即便可行)那麼我們該如何改進呢?答案很簡單,Andorid提供給我們極其簡便的方法,我們只需在設置Bitmap後調用requestLayout方法和invalidate即可:

  1. public void setBitmap(Bitmap bitmap) {  
  2.     this.mBitmap = bitmap;  
  3.     requestLayout();  
  4.     invalidate();  
  5. }  

requestLayout方法的意義在於如果你的操作有可能會讓控件的尺寸或位置發生改變那麼就可以調用該方法請求佈局,調用該方法後framework會嘗試調用measure對控件重新測量:


而invalidate方法呢我們則用的多了不再多說:

但是要注意的一點是,requestLayout方法和invalidate方法並非都必需調用的,比如我們有一個更改字體顏色的方法:

  1. public void setTextColor(int color) {  
  2.     mPaint.setColor(color);  
  3.     invalidate();  
  4. }  
這時我們僅需調用invalidate方法標識重繪視圖即可,但是,如上我們所說,如果一旦尺寸大小或位置發生了變化,那麼我們最好重新佈局並迫使視圖重繪,比如我們有個改變字體大小的方法:

  1. public void setTextSize(int size) {  
  2.     mPaint.setTextSize(size);  
  3.     requestLayout();  
  4.     invalidate();  
  5. }  
這時候我們就需要調用requestLayout請求佈局,因爲字體大小的改變有可能會影響到控件的尺寸大小和位置的改變,同樣,如果位置大小都變了,那我們是否該重新繪製呢?The answer is yes~好了,別嫌我囉嗦,最近有盆友反應說前面章節太難理解……其實之所以覺得前面的章節難是因爲涉及繪製的API大多跟一些圖像處理有關,而coder正恰恰缺乏這方面的一些知識所以不好理解,你看我前面的章節壓根就沒涉及什麼源碼,只有從測量開始才涉及了一次,此後也不再打算再過多地涉及,畢竟與該系列基礎篇的定位不符。閒話不多說了,如上以及前面兩節的內容所述,其實在應用開發使用的過程中設計測量邏輯的API並不多,也沒太多可講的,最主要的還是自己的邏輯,So,這一部分暫且爲止,在自定義控件其實很簡單7/12中我們曾定義過一個類似圖標的控件:


當時我們是直接extends View去做的,繪製了文本、繪製了Bitmap還有在此之前對其進行測量、定位等等,即便我們考慮周詳,但是也極難將一個裝載文本和圖片的控件做成一個TextView和ImageView的複合體,更難以像TextView和ImageView那樣提供儘可能多的接口方法,誒!等等!既然我們的這個圖標控件看上去就是個TextView和ImageView雜交的後代,那麼我們是否可以簡單地將這兩種控件組合起來變成一個新的控件呢?答案是肯定的撒!而且比起我們直接extends view來說簡單很多很多很多,首先我們先定義一個佈局,這個佈局裏面呢只包含一個ImageView和一個TextView,大體來說樣式跟上面我們自定義的類似:

  1. <!-- http://blog.csdn.net/aigestudio -->  
  2. <?xml version="1.0" encoding="utf-8"?>  
  3. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  4.     android:layout_width="match_parent"  
  5.     android:layout_height="match_parent"  
  6.     android:background="#FFFFFFFF"  
  7.     android:gravity="center"  
  8.     android:orientation="vertical" >  
  9.   
  10.     <ImageView  
  11.         android:id="@+id/view_complex_image_iv"  
  12.         android:layout_width="wrap_content"  
  13.         android:layout_height="wrap_content"  
  14.         android:src="@drawable/logo" />  
  15.   
  16.     <TextView  
  17.         android:textSize="40sp"  
  18.         android:textStyle="bold"  
  19.         android:id="@+id/view_complex_title_tv"  
  20.         android:layout_width="wrap_content"  
  21.         android:layout_height="wrap_content"  
  22.         android:text="AigeStudio" />  
  23.   
  24. </LinearLayout>  
ADT中直接展示的效果如下:


是不是跟我們自定義的一樣呢?如我所說,僅僅是一個ImageView和TextView的組合而已,接下來我們要做的則是將這個xml佈局文件“集成”到我們的自定義控件中去,方法也很簡單,在自定義控件的構造方法裏引入該佈局文件並將其作爲控件的佈局則可:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/2/6 
  5.  *  
  6.  */  
  7. public class ComplexView extends FrameLayout {  
  8.     private ImageView ivIcon;// 複合控件中的ImageView  
  9.     private TextView tvTitle;// 複合控件中的TextView  
  10.   
  11.     public ComplexView(Context context, AttributeSet attrs) {  
  12.         super(context, attrs);  
  13.   
  14.         // 加載佈局文件  
  15.         ((LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(  
  16.                 R.layout.view_complex, this);  
  17.   
  18.         // 獲取控件  
  19.         ivIcon = (ImageView) findViewById(R.id.view_complex_image_iv);  
  20.         tvTitle = (TextView) findViewById(R.id.view_complex_title_tv);  
  21.     }  
  22. }  
上面的代碼中我選擇繼承了FrameLayout,實際上你可以選擇繼承任何一種佈局容器類,關鍵在於我們加載xml佈局文件時以該佈局容器作爲根佈局:

  1. xxxxxxxxxxxxxxxxxxxxx.inflate(R.layout.view_complex, this);  
“this”就表示了將整個xml佈局文件作爲子元素加載至ComplexView(extends FrameLayout)下,構成如下圖所示的一個層級關係:


而後我們只需直接使用這個ComplexView符合控件即可:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:background="#ffffff"  
  4.     android:layout_height="match_parent" >  
  5.   
  6.     <com.aigestudio.customviewdemo.views.ComplexView  
  7.         android:id="@+id/main_tv"  
  8.         android:layout_width="wrap_content"  
  9.         android:layout_height="wrap_content" />  
  10.   
  11. </LinearLayout>  
這時候就可以直接在ADT中查看效果:


非常完美,不需要我們去處理測繪邏輯,所有的這些都由Android自帶的控件自行去計算,我們只是簡單地將它們組合在一起了而已,所以說,每當Android提供的控件不能滿足你的需求時,首先你應該想想是否可以在現有控件的基礎上修改一下來達到你的目的,而不是盲目地直接重寫View或ViewGroup類,你可以提供不同的接口方法來修改你複合控件中的各類元素,比如下面我們提供一個setImageIcon方法來爲複合控件中的ImageView設置圖片:

  1. public void setImageIcon(int resId) {  
  2.     ivIcon.setImageResource(resId);  
  3. }  
你甚至可以直接提供一個getter方法去獲取複合元素的引用:

  1. public TextView getTitle() {  
  2.     return tvTitle;  
  3. }  
這樣你就可以對複合控件中的TextView爲所欲爲了……如前幾節我們所說,framework對xml文件的解析是相當耗時的,如果可以,我們應當儘量避免對xml文檔的讀取,特別是元素結構複雜的xml文件,這裏我們用到的xml佈局文件還不算複雜,如果我不想從xml文檔讀取而是直接實例化類呢?我們來重新修改下我們的代碼:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/2/6 
  5.  *  
  6.  */  
  7. public class ComplexView extends LinearLayout {  
  8.     private ImageView ivIcon;// 複合控件中的ImageView  
  9.     private TextView tvTitle;// 複合控件中的TextView  
  10.   
  11.     public ComplexView(Context context, AttributeSet attrs) {  
  12.         super(context, attrs);  
  13.   
  14.         // 設置線性佈局排列方式  
  15.         setOrientation(LinearLayout.VERTICAL);  
  16.   
  17.         // 設置線性佈局子元素對齊方式  
  18.         setGravity(Gravity.CENTER);  
  19.   
  20.         // 實例化子元素  
  21.         ivIcon = new ImageView(context);  
  22.         ivIcon.setImageResource(R.drawable.logo);  
  23.   
  24.         tvTitle = new TextView(context);  
  25.         tvTitle.setText("AigeStudio");  
  26.         tvTitle.setTextSize(MeasureUtil.dp2px(context, 30));  
  27.         tvTitle.setTypeface(Typeface.DEFAULT_BOLD);  
  28.   
  29.         // 將子元素添加到複合控件  
  30.         addView(ivIcon, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,  
  31.                 LinearLayout.LayoutParams.WRAP_CONTENT));  
  32.         addView(tvTitle, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,  
  33.                 LinearLayout.LayoutParams.WRAP_CONTENT));  
  34.     }  
  35. }  
直接讓複合控件類繼承LinearLayout,在構造方法中實例化子元素並設置其屬性值然後添加至LinearLayout中搞定,複合控件在實際應用中也使用得相當廣泛,因爲很多時候使用符合控件不需要處理複雜的測繪邏輯,簡單方便高效。在Android自帶的控件中有個checkBox複選框控件:


效果單一乏味不好看,而我想要的效果很簡單也與之類似,通過不斷點擊控件往復切換控件的兩種狀態即可:


達到類似效果有多種方法,最簡單的是更改checkBox,最複雜的是繼承View自己寫一個,而上面我們瞭解過複合控件,那麼我們能不能馬上學以致用使用一個複合控件來達到該效果呢?答案是肯定的!細心觀察可以看得出上面的效果無非就是兩張不同的圖片來回顯示/隱藏地切換而已,更直白地說就是兩個ImageView不斷地顯示/隱藏切換對吧,Such easy,下面直接看全部代碼:

  1. /** 
  2.  * 自定義CheckBox 
  3.  *  
  4.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  5.  * @since 2015/2/6 
  6.  *  
  7.  */  
  8. public class CustomCheckBox extends FrameLayout {  
  9.     private ImageView ivCheckOn, ivCheckOff;// 兩種狀態的ImageView  
  10.     private CustomCheckBoxChangeListener customCheckBoxChangeListener;// 切換的監聽器  
  11.   
  12.     private boolean isCheck;// 是否被選中的標誌值  
  13.   
  14.     public CustomCheckBox(Context context) {  
  15.         this(context, null);  
  16.     }  
  17.   
  18.     public CustomCheckBox(Context context, AttributeSet attrs) {  
  19.         this(context, attrs, 0);  
  20.     }  
  21.   
  22.     public CustomCheckBox(Context context, AttributeSet attrs, int defStyleAttr) {  
  23.         super(context, attrs, defStyleAttr);  
  24.   
  25.         // 設置佈局文件  
  26.         LayoutInflater.from(context).inflate(R.layout.view_custom_check_box, this);  
  27.   
  28.         // 獲取控件元素  
  29.         ivCheckOn = (ImageView) findViewById(R.id.view_custom_check_box_on);  
  30.         ivCheckOff = (ImageView) findViewById(R.id.view_custom_check_box_off);  
  31.   
  32.         // 設置兩個ImageView的點擊事件  
  33.         ivCheckOn.setOnClickListener(new ClickListener());  
  34.         ivCheckOff.setOnClickListener(new ClickListener());  
  35.   
  36.         // 讀取xml中設置的資源屬性ID  
  37.         TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomCheckBox);  
  38.         int imageOnResId = array.getResourceId(R.styleable.CustomCheckBox_imageOn, -1);  
  39.         int imageOffResId = array.getResourceId(R.styleable.CustomCheckBox_imageOff, -1);  
  40.   
  41.         // 設置顯示資源  
  42.         setOnImage(imageOnResId);  
  43.         setOffImage(imageOffResId);  
  44.   
  45.         // 對象回收  
  46.         array.recycle();  
  47.   
  48.         // 默認顯示的是沒被選中的狀態  
  49.         setCheckOff();  
  50.     }  
  51.   
  52.     /** 
  53.      * 爲CustomCheckBox設置監聽器 
  54.      * 
  55.      * @param customCheckBoxChangeListener 
  56.      *            監聽器接口對象 
  57.      */  
  58.     public void setCustomCheckBoxChangeListener(  
  59.             CustomCheckBoxChangeListener customCheckBoxChangeListener) {  
  60.         this.customCheckBoxChangeListener = customCheckBoxChangeListener;  
  61.     }  
  62.   
  63.     /** 
  64.      * 設置開啓狀態時CustomCheckBox的圖片 
  65.      * 
  66.      * @param resId 
  67.      *            圖片資源ID 
  68.      */  
  69.     public void setOnImage(int resId) {  
  70.         ivCheckOn.setImageResource(resId);  
  71.     }  
  72.   
  73.     /** 
  74.      * 設置關閉狀態時CustomCheckBox的圖片 
  75.      * 
  76.      * @param resId 
  77.      *            圖片資源ID 
  78.      */  
  79.     public void setOffImage(int resId) {  
  80.         ivCheckOff.setImageResource(resId);  
  81.     }  
  82.   
  83.     /** 
  84.      * 設置CustomCheckBox爲關閉狀態 
  85.      */  
  86.     public void setCheckOff() {  
  87.         isCheck = false;  
  88.         ivCheckOn.setVisibility(GONE);  
  89.         ivCheckOff.setVisibility(VISIBLE);  
  90.     }  
  91.   
  92.     /** 
  93.      * 設置CustomCheckBox爲開啓狀態 
  94.      */  
  95.     public void setCheckOn() {  
  96.         isCheck = true;  
  97.         ivCheckOn.setVisibility(VISIBLE);  
  98.         ivCheckOff.setVisibility(GONE);  
  99.     }  
  100.   
  101.     /** 
  102.      * 獲取CustomCheckBox的選擇狀態 
  103.      * 
  104.      * @return true CustomCheckBox已被選擇 
  105.      * @return false CustomCheckBox未被選擇 
  106.      */  
  107.     public boolean isCheck() {  
  108.         return isCheck;  
  109.     }  
  110.   
  111.     /** 
  112.      * 狀態改變監聽接口 
  113.      */  
  114.     public interface CustomCheckBoxChangeListener {  
  115.         void customCheckBoxOn();  
  116.   
  117.         void customCheckBoxOff();  
  118.     }  
  119.   
  120.     /** 
  121.      * 自定義CustomCheckBox中控件的事件監聽器 
  122.      */  
  123.     private class ClickListener implements OnClickListener {  
  124.   
  125.         @Override  
  126.         public void onClick(View v) {  
  127.             switch (v.getId()) {  
  128.             case R.id.view_custom_check_box_on:  
  129.                 setCheckOff();  
  130.                 customCheckBoxChangeListener.customCheckBoxOff();  
  131.                 break;  
  132.             case R.id.view_custom_check_box_off:  
  133.                 setCheckOn();  
  134.                 customCheckBoxChangeListener.customCheckBoxOn();  
  135.                 break;  
  136.             }  
  137.         }  
  138.     }  
  139. }  
整個複合控件非常簡單,我們只是簡單地將兩個ImageView重疊組合了在一起並設置其點擊事件監聽,對外我們公佈了一個CustomCheckBoxChangeListener監聽接口以監聽狀態的改變並處理一些邏輯,只需在Activity中獲取CustomCheckBox控件並設置監聽對象即可:

  1. /** 
  2.  * 主界面 
  3.  *  
  4.  * @author Aige {@link http://blog.csdn.net/aigestudio} 
  5.  * @since 2014/11/17 
  6.  */  
  7. public class MainActivity extends Activity {  
  8.     private CustomCheckBox ccbTest;  
  9.   
  10.     @Override  
  11.     public void onCreate(Bundle savedInstanceState) {  
  12.         super.onCreate(savedInstanceState);  
  13.         setContentView(R.layout.activity_main);  
  14.   
  15.         ccbTest = (CustomCheckBox) findViewById(R.id.main_ccb);  
  16.         ccbTest.setCustomCheckBoxChangeListener(new CustomCheckBoxChangeListener() {  
  17.             @Override  
  18.             public void customCheckBoxOn() {  
  19.                 Toast.makeText(MainActivity.this"Check on", Toast.LENGTH_SHORT).show();  
  20.             }  
  21.   
  22.             @Override  
  23.             public void customCheckBoxOff() {  
  24.                 Toast.makeText(MainActivity.this"Check off", Toast.LENGTH_SHORT).show();  
  25.             }  
  26.         });  
  27.     }  
  28. }  
當CustomCheckBox的當前狀態爲未被選中時會觸發customCheckBoxOff方法否則觸發customCheckBoxOn方法:


CustomCheckBox中加載用到的xml佈局文件如下:

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent" >  
  5.   
  6.     <ImageView  
  7.         android:id="@+id/view_custom_check_box_on"  
  8.         android:layout_width="match_parent"  
  9.         android:scaleType="fitCenter"  
  10.         android:layout_height="match_parent" />  
  11.   
  12.     <ImageView  
  13.         android:id="@+id/view_custom_check_box_off"  
  14.         android:layout_width="match_parent"  
  15.         android:layout_height="match_parent"  
  16.         android:scaleType="fitCenter" />  
  17.   
  18. </FrameLayout>  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章