2012年2月4日

有關帶 Checkbox 的 ListView

近日正忙著編寫一個有 CRUD (Create-Read-Update-Delete) 功能的 ListActvity,其中因為想有多選功能,所以想把一個 CheckBox 放到每一行的一旁,感覺就跟 CheckBoxPreference 差不多。

我花了很多時間想著使用 Custom List Adaptor 或 extend SimpleAdaptor,最終當然會成功,不過就是代碼太煩瑣,設計不好。

幸好從網上找到一編教學,它解釋怎樣製作 widget (即 custom Relative Layout 跟 custom CheckBox),用後我覺得十分有用,既簡潔又易用,大家不防參考一下:

Custom ListView with ability to check items


[Update: 2012-02-05]
這個方法有一個大問題 (迫使我放棄它) - 這個方法不適用於 List 數據操作

  1. 因為 ListView 裡 Recycling 的機制,假如刪除一行  List  數據,然後呼叫 notifyDataSetChanged(),ListView 會確實的移除了那一行 ,不過有時候那個勾勾會出現在另一行 (這行之前並沒有點選喔)。因為我使用SimpleAdapter,每次調用 notifyDataSetChanged() 時,View(如 TextView) 中的資料會跟據 List 數據改變,但因勾選並沒有關聯到List 數據,它只保留在 custom Relative Layout/CheckBox 中,那 Adapter 調用 getView 時,原來的 Check 狀態便保留著,因 List 數據位移了,  Check 狀態便一起位移,形成錯誤。
  2. 避免這個問題的方法,我目前只想到呼叫 getListView().clearChoices(),把所有的勾勾取消。
  3. 對於單向操作 (即只用作取得勾選的項目),它應該是一個很不錯的方案。一但須要操作 List 原數據時便不可以了。
這樣又回歸原方法了,那我把我用的方法寫出來吧:

  1. Item/Row 的 Layout 放一個 CheckBox,並設為 android:focusable="false"
  2. 新增一個 boolean 的值並綁到數據去
    1. 如使用 SimpleAdapter,可以在數據中加一個 Key-value 值,Key 為 "rowSelected", 值為 Boolean。
    2. 如數據使用自訂的List;,可以做一個 Wrapper Class/Extended Class,把原數據加一個 row select 的property,那便可自訂一個 List Adapter (extends BaseAdapter),把 List data 在這個 Class 中管理
    3. 自行寫一個 getView 和 row holder class,重點在於,當 create / inflate convertView 時把 OnClickListener() 加到 CheckBox 去 ,因借用  CheckBox.setTag 把 position 值寄存起,當 onClick() 時便可以將 position 從  CheckBox.getTag 取出,再跟據相對的 List data 位置取出數據,並把數據中的 "rowSelected" 值設為 CheckBox 的 isChecked() 值。
    4. 不要忘記使用 convertView.setTag / getTag 把  row holder class 寄存起來。
    5. 在 getView 的最後,就是把 holder 中的每一個 View (包括 CheckBox) 按數據值設好做成。
    6. 以下是我的 getView代碼 片段給大家參考:

      @Override
       public View getView(int position, View convertView, ViewGroup parent) {
        final SelectablePhoneNumber rowData = getItem(position);
      
        final ListItemViewHolder holder;
        if (convertView == null) {
         convertView = mInflater.inflate(
           R.layout.recipient_editor_row_layout, null);
         holder = new ListItemViewHolder();
         holder.rowSelect = (CheckBox) convertView
           .findViewById(R.id.rowSelect);
      
         holder.rowSelect.setOnClickListener(new OnClickListener() {
          @Override
          public void onClick(View v) {
           CheckBox cb = (CheckBox) v;
           int pos = (Integer) cb.getTag();
           SelectablePhoneNumber row = getItem(pos);
           row.setSelected(cb.isChecked());
           // Pretent the data changed
           notifyDataSetChanged();
           // Use Invalidate?
          }
         });
      
         holder.recpName = (TextView) convertView.findViewById(R.id.title);
      
         holder.recpPhoneNum = (TextView) convertView
           .findViewById(R.id.description);
         convertView.setTag(holder);
        } else {
         holder = (ListItemViewHolder) convertView.getTag();
        }
      
        // Set the value
      
        holder.rowSelect.setChecked(rowData.isSelected());
        holder.rowSelect.setTag(position);
        holder.recpName.setText(rowData.getName());
        holder.recpPhoneNum.setText(rowData.getPhoneNumber());
        return convertView;
      
       }
      
       static class ListItemViewHolder {
        CheckBox rowSelect;
        TextView recpName;
        TextView recpPhoneNum;
       }
      

      [Update: 2012-02-05]
      還有一件事,當看清楚 SimpleAdapter 裡的 constructor 時,其中 data 這個輸入參數為 List<? extends Map<String, ?>> data,它用了 ? extends,這表示 SimpleAdapter 裡不希望對 data 進行 put 動作。雖然可以強行對它修改,但這種設計不好呢。

      在網上找到一編文章:
      http://stackoverflow.com/questions/4543592/help-with-java-generics-cannot-use-object-as-argument-for-extends-object

      [Update: 20120209]
      過了一段時間,最後還是自己寫一個 CheckableSimpleAdapter 算了,這個方法應該算是靈活而又簡化多了,代碼在這裡:
      http://www.badbuta.com/2012/02/checkbox-listview_08.html

沒有留言:

發佈留言