2014年12月9日 星期二

【Android】CheckBox的「帽子把戲」(如何自製「單選選項」)

這篇算是白癡教學。只要把基礎讀完的人應該都可以順利看完。

如果(讀者)要準備製作的是一張從頭到尾都手工打造、每個選項都有自己獨特的ID和對應資料的選單,這篇文章講的技巧沒什麼意義。

但這裡講的是「ListView這類用緩存記憶體大量複製出來的UI」,而且操作的是JSONArray這類的資料時,可能會需要的技巧(或說是心得會比較恰當)。

另外...如果不知道什麼是CheckBox的人也沒有必要繼續浪費時間看下去。



說明一下題目。

雖然Android就有預設「單選群組」給大家使用,但「萬一不夠用」,或「某些特性跟自己的需求相違背」時,該怎麼辦?

我沒辦法解釋或說明「那會是什麼樣的狀況」,那是碰到了就知道的狀況.......

一般來說CheckBox是用來製作「多選選項」、或「勾選式True/False」的元件。(有一種「滑動式切換True/False」的元件。)

如果每個CheckBox都是獨一無二的,則只要「輸出結果」時將全部的CheckBox都集合起來,用迴圈判讀它的Check狀態就好,但如果是用迴圈/緩存記憶體大量複製的,則無法這樣做,資料的操作必須要在按下「CheckBox」當下的瞬間進行修改。

再說明清楚一點:

假設功能要求這裡有個長整數(Integer)的矩陣A,每「格」矩陣對應內有二或多個以上CheckBox的CheckBox群組,CheckBox群組內的CheckBox依序對應「1」、「2」....以此類推的值,(CheckBox1被勾選時,某數值為1,CheckBox2被勾選時,某數值為2,.......某數值不可能同時又是1、又是2,所以CheckBox1和CheckBox2必須為單選項,)被選取的CheckBox則會把自己對應的數值寫入群組對應的矩陣格內。

如果矩陣A的長度永遠固定,那就表示CheckBox群組的數目也永遠固定,所以只要用固定格式的介面即可。最終要輸出答案時,只要把所有CheckBox群組用固定的順序判讀即可。

但如果矩陣A的長度會隨機變化,那狀況就很複雜了.........(先不提選項有多少種,光是題目數目不一定,這就是個「菜鳥的挑戰」了。)

以ListView為例,如果用全緩存的方式管理生成畫面,會發生「當這個選項滑出畫面外時,系統會把這個選項的介面圖形回收掉,」(不懂技術的人肯定完全不懂這是怎麼回事。意思就是說「這個欄位如果滑出畫面外,可能會被系統消除掉、以節省記憶體......,當它划回營幕時,系統再把它重新生出來。」)所以如果用「判讀介面上CheckBox選項狀態」的方式來決定答案值,會發生取不到CheckBox的狀況。

碰到這種案例時,就不能「讀取CheckBox的選項狀態」來決定數值,必須要在選項被圈選的當下就決定數值.......這就要使用介面「OnCheckedChangedListener」。

這只是狀況之一,隨機決定長度的資料難度完全不同。




首先,下面這算是個用匿名函數式去宣告的一個OnCheckedChangeListener。(怕有人沒用過,所以說明一下。CompoundButton buttonView是指設定了這個Listener的CheckBox,所以即使多個CheckBox設定同一個Listener,Listener也不會弄錯,──這是種設計函數的基本技巧。boolean isChecked則是前面這個buttonView的選取狀態,true表示被勾選,false則是無勾選、或勾選取消。)

OnCheckedChangeListener l1 = new OnCheckedChangeListener() {
  
  @Override
  public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
   // TODO Auto-generated method stub
   
  }
 };
請不要用這個方式製作接下來的OnCheckedChangeListener!

請正規的實作一個類別後再宣告。


結果大概如下!這個類別預設的是操作JSONObject。
class CheckListenr implements OnCheckedChangeListener{
  ...//省略建構子
  
  JSONObject js;
  CompoundButton lastView = null;

  @Override
  public void onCheckedChanged(CompoundButton buttonView,
    boolean isChecked) {
   // TODO Auto-generated method stub
                        //!!!!0#  將這個CheckBox對應的值取出
   int tag = (Integer) buttonView.getTag();
   if(!isChecked){//!!!!A#
    ....
    ....
                                //!!!!3#  
                                lastView = null;
   }
   else{
    if(lastView != null){
                                        //!!!!2#  將上一次選取的CheckBox取消勾選
     lastView.setOnCheckedChangeListener(null);
     lastView.setChecked(false);
     lastView.setOnCheckedChangeListener(this);
                                        //!!!!2#End
    }
                                //!!!!1#  將這次勾選的選項設為「上一次勾選的項目」
    lastView = buttonView;
    ...
                                ...
   }
  }
  
 }

假設眼前有三個CheckBox,分別是CheckA、CheckB、CheckC,則將它們對應的數值設為Tag,譬如CheckA為A、CheckB為B、CheckC為C,──可以看到「!!!!0#」下面對應的那行。(我這裡的做法是用isChecked為true時填入這個值,如果為false則填入統一的「X」。各位也可以選擇用設定兩個Tag,isChecked為true時取Tag1,false則取Tag2。這是很基本的變化技巧.......不是講給熟手聽得。)

「!!!!1#」、「!!!!2#」和「!!!!3#」就是將這些CheckBox做成單選項的關鍵。

「!!!!A#」的地方將整個邏輯切割開來,分為「勾選」和「取消勾選」......不難懂(吧)。

如果是第一次勾選這個群組(提醒:群組是指都設定了這個Listener、並且被指定要修改同一個數值的CheckBox)、或群組內的勾選被取消的狀況時,「!!!!2#」區段的程式碼並不會被執行,有CheckBox被勾選後、使用者又決定改勾選別的CheckBox時,「!!!!2#」就會被呼叫。

這地方是我稱這個技巧為「帽子把戲」的原因。注意看我將「上一次勾選的CheckBox取消勾選(設定為false)」前後,個做了一次「將Listener設為Null」和「將這個Listener設定給CheckBox」。

這有可能造成無窮迴圈,或至少會造成「無法將選項正確的反勾選」。因為如果CheckBox有設定CheckChangedListener時改變它的勾選狀態,則這個Listener會被呼叫執行,然後「取消勾選後」,也就是「!!!!A#」這個地方如果為false的區段會被執行,包含「!!!!3#」。

一般情況下這其實不會有問題!可是......我無法精確說明何時會是問題,因為「取消勾選(群組內的任何一個選項)」跟「被觸發設為false」兩者是不一樣的!(簡單來說,如果「取消勾選」時對應的動作如果在「被觸發設為false」時不可以被執行會出錯時,這就是我這裡再講的狀況。)

所以就假設「萬一出問題好了」,單純的將「上一次勾選的CheckBox」設為「false」會導致邏輯判斷出錯,解法非常簡單,再執行「被觸發設為false」前,將Listener解除就好。