2012年2月8日

有關帶 Checkbox 的 ListView (二)

之前一篇說到要寫一個帶 Checkbox 的 ListView,過了一段時間,最後還是自己寫一個 CheckableSimpleAdapter 算了,這個方法應該算是靈活而又簡化多了,因為它 extends SimpleAdapter,所以用起來跟 SimpleAdapter 差不多。

這個CheckableSimpleAdapter是伸廷 SimpleAdapter 的一個類,自帶一個 OnCheckedStateChangeListener 介面,只要 Layout 中帶有 CheckBox 並且在建立 CheckableSimpleAdapter 時表示那個 Resource ID 是用作 Row Select 便可以。

要注意 data 這個 List,每列的 Map 裡會被加進一個 Key-value pair 值,用以表示這一行被選取的狀態。

代碼如下:
/**
 * An extended SimpleAdapter for handling Checkable row/item.
 * 
 * Note that extra entity of checked state will be saved to each row data (i.e.
 * in each Map object). The key value is {@link#KEY_ROW_SEL_CHECKBOX}
 * 
 * TODO: Isolate the checked state to separated storage but keep the data
 * synchronized.
 * 
 * http://www.badbuta.com
 * 
 * @author David Hon
 * 
 */
public class CheckableSimpleAdapter extends SimpleAdapter implements
  OnClickListener {
 private final static String LOG_TAG = CheckableSimpleAdapter.class
   .getSimpleName();

 public final static String KEY_ROW_SEL_CHECKBOX = "_rowSelectCbox";

 private final int rowSelCheckBoxResource;

 // Backup the list
 private final List<? extends Map<String, ?>> data;

 private OnCheckedStateChangeListener onCheckedStateChangeListener = null;

 /**
  * Interface definition for a callback to be invoked when this Checkable
  * item state is changed.
  * 
  * @author David Hon
  * 
  */
 public interface OnCheckedStateChangeListener {
  /**
   * Called when the checkable item check state is changed.
   * 
   * @param position
   *            the changed item position
   * @param checkedState
   *            a state presents the checkable item
   */
  public void onCheckableStateChange(int position, boolean checkedState);
 }

 /**
  * The constructor, which is similar to SimpleAdapter constructor. However
  * it requires additional resource id for the CheckBox for row selection.
  * Note that there is no checking for invalid CheckBox ID. Please make sure
  * given ID is valid. Otherwise getView() may throw NullPointerException
  * during runtime.
  * 
  * 
  * @param context
  * @param data
  * @param resource
  * @param from
  * @param to
  * @param rowSelCheckBoxResource
  *            Additional resource id for the CheckBox for row selection.
  */
 public CheckableSimpleAdapter(Context context,
   List<? extends Map<String, ?>> data, int resource, String[] from,
   int[] to, int rowSelCheckBoxResource) {
  super(context, data, resource, from, to);
  this.rowSelCheckBoxResource = rowSelCheckBoxResource;
  this.data = data;
 }

 /**
  * Clear all checked rows (UI and Data)
  */
 @SuppressWarnings("unchecked")
 public void clearCheckedRows() {
  int rowCount = getCount();
  for (int i = 0; i < rowCount; i++) {
   Map<String, Object> o = (Map<String, Object>) data.get(i);

   if (o.containsKey(KEY_ROW_SEL_CHECKBOX)) {
    o.put(KEY_ROW_SEL_CHECKBOX, Boolean.FALSE);
   }
  }
  notifyDataSetChanged();
 }

 /**
  * Check if there is ANY checked data
  * 
  * @return
  */
 public boolean hasCheckedData() {
  int rowCount = getCount();
  for (int i = 0; i < rowCount; i++) {
   Map<String, ?> o = data.get(i);
   if (o.containsKey(KEY_ROW_SEL_CHECKBOX)) {
    boolean val = (Boolean) o.get(KEY_ROW_SEL_CHECKBOX);
    if (val) {
     return true;
    }
   }
  }
  return false;
 }

 /**
  * Get the (un)checked row data
  * 
  * @param invertSelection
  *            FALSE for checked data, TRUE for unchecked data
  * @return A List which store the result.
  */
 public List<Map<String, ?>> getCheckedData(boolean invertSelection) {
  int rowCount = getCount();
  List<Map<String, ?>> result = new ArrayList<Map<String, ?>>(rowCount);
  for (int i = 0; i < rowCount; i++) {
   Map<String, ?> o = data.get(i);
   if (o.containsKey(KEY_ROW_SEL_CHECKBOX)) {
    boolean val = (Boolean) o.get(KEY_ROW_SEL_CHECKBOX);
    if (val != invertSelection) {
     result.add(o);
    }
   }
  }
  return result;
 }

 /**
  * Get the checked state which stored in the data
  * 
  * @return SparseBooleanArray which store the result.
  */
 public SparseBooleanArray getCheckedStates() {
  int rowCount = getCount();
  SparseBooleanArray result = new SparseBooleanArray();
  for (int i = 0; i < rowCount; i++) {
   Map<String, ?> o = data.get(i);
   // If the check value has not yet set, it should be unchecked
   // (unseen), so return the value as FALSE
   boolean val = (o.containsKey(KEY_ROW_SEL_CHECKBOX)) ? (Boolean) o
     .get(KEY_ROW_SEL_CHECKBOX) : false;
   result.append(i, val);
  }
  return result;
 }

 /**
  * Setterfor OnCheckedStateChangeListener
  * 
  * @param listener
  *            the OnCheckedStateChangeListener
  */
 public void setOnCheckedStateChangeListener(
   OnCheckedStateChangeListener listener) {
  this.onCheckedStateChangeListener = listener;
 }

 /*
  * (non-Javadoc)
  * 
  * @see android.widget.SimpleAdapter#getView(int, android.view.View,
  * android.view.ViewGroup)
  */
 @Override
 public View getView(int position, View convertView, ViewGroup parent) {
  // Save the convertView state before creation from super class.
  boolean emptyConvertView = (convertView == null);

  View v = super.getView(position, convertView, parent);

  // expected the returned view should not be null.

  // No null-checking, Checkbox may be come from recycle pool.
  CheckBox cb = (CheckBox) v.findViewById(rowSelCheckBoxResource);

  // Save the position to tag anyway.
  cb.setTag(position);

  // Create the OnClickListener and set it as necessary.
  // i.e. Avoid re-creation of OnClickListener, save resources.
  if (emptyConvertView) {
   cb.setOnClickListener(this);
  }

  @SuppressWarnings("unchecked")
  Map<String, Object> o = (Map<String, Object>) getItem(position);
  if (!o.containsKey(KEY_ROW_SEL_CHECKBOX)) {
   // Create key-value for row selection state if it has not yet
   // created. (Lazy creation)
   o.put(KEY_ROW_SEL_CHECKBOX, Boolean.FALSE);
  }

  // Get the row selection from data
  boolean bVal = (Boolean) o.get(KEY_ROW_SEL_CHECKBOX);

  // Set the checkbox check state according to that value:
  if (cb.isChecked() != bVal) {
   cb.setChecked(bVal);
  }

  // Done!
  return v;
 }

 /*
  * (non-Javadoc)
  * 
  * @see android.view.View.OnClickListener#onClick(android.view.View)
  */
 @Override
 public void onClick(View v) {
  if (v instanceof CheckBox) {
   // It should be a Checkbox
   CheckBox cb = (CheckBox) v;

   // Retrieve the data position from tag:
   Object tag = cb.getTag();
   if (tag != null && (tag instanceof Integer)) {
    int pos = (Integer) cb.getTag();

    @SuppressWarnings("unchecked")
    Map<String, Object> o = (Map<String, Object>) data.get(pos);
    boolean cbIsChecked = cb.isChecked();

    // Save the checkbox check state (i.e. row
    // selection) to data:

    o.put(KEY_ROW_SEL_CHECKBOX, Boolean.valueOf(cbIsChecked));

    // notify the state changed.
    if (onCheckedStateChangeListener != null) {
     onCheckedStateChangeListener.onCheckableStateChange(pos,
       cbIsChecked);
    }
   } else {
    // It should be programming error, anyway log it.
    Log.e(LOG_TAG, "CheckBox tag for data position is invalid!");
   }
  }
 }
}

NOTE: imports, package 說自己加回去吧

沒有留言:

發佈留言