Saturday, May 5, 2012

List Activity using checkboxes - an issue

One common List Activity using android is the one which uses check boxes. On may note, however, every this list is created using the CheckBox component, one cannot have an item selected. The only thing one can do is simply to check or uncheck the Check Box. One cannot, for instance, select an item or do any operations regarding this very selection. For instance, one cannot open a menu for a desired item.

The first thought one might have is to customize several features of the Activity List itself. However, this would take quite some time and would be a huge effort in order to accomplish a simple thing: select an item.

Another solution, which is quite a good one, is to, instead of adding a CheckBox component, add an Image component for a check box. This image would hold two figures: one for a selected check box and another for a unselected check box. They would be changed whenever this image was clicked. Therefore, one would have the behavior of the check box implemented and the list would behave naturally, allowing items selection.

A third solution, and the simplest one, would be to use the CheckedTextView component. This is actually a check box with text component. One simply omits the text by providing none. This component behaves exactly like a CheckBox with the advantage of allowing the Activity List to behave naturally, allowing items selection. Thus, a substitution of a CheckBox component for a CheckedTextView would be straightforward.

When one simply add the CheckedTextView into the xml layout file, it may not appear correctly or not appear at all, because some layout definitions are required. One that works is displayed below:

<CheckedTextView android:id="@+id/ckb"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                   android:textAppearance="?android:attr/textAppearanceLarge"
                android:layout_gravity="right"
                android:checked="false"
                android:clickable="false"
                android:checkMark="?android:attr/listChoiceIndicatorMultiple"
            />

Now a full example using CheckedTextView is displayed below. The first box shows the layout of xml of the main Activity. The second one depicts the layout xml of the list row. Finally, the third one displays the code of the activity.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/vw1"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">   


    <ImageView android:id="@+id/img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="6dip"/>


    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_weight="4" >


        <TextView android:id="@+id/text1"
            android:textSize="12sp"
            android:textStyle="bold"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:paddingLeft="10dip"
            android:paddingTop="6dip"/>


        <TextView android:id="@+id/text2"
            android:textSize="12sp"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:paddingLeft="10dip"/>
    </LinearLayout>
   
    <LinearLayout
        android:id="@+id/cbxLayout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="right"
        >
       
        <CheckedTextView android:id="@+id/ckb"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:layout_gravity="right"
            android:checked="false"
            android:clickable="false"
            android:checkMark="?android:attr/listChoiceIndicatorMultiple"
        />
    </LinearLayout>
</LinearLayout>


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">   


    <ListView
         android:id="@id/android:list"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="1"
         android:drawSelectorOnTop="false"
     />
    <Button
        android:id="@+id/button"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/button_label"
        android:gravity="center_horizontal"       
        />


</LinearLayout>

package com.checkboxlist;


import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


import android.app.ListActivity;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.CheckedTextView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import android.widget.Toast;


public class CheckBoxList extends ListActivity implements OnClickListener {
    private ArrayList<Integer> selectedItems = new ArrayList<Integer>();
    private final String SELECTED_ITEM_KEY = "selected_items";
    public final String TEXT_KEY_1 = "title";
    public final String TEXT_KEY_2 = "description";
    public final String ITEM_ID = "id";
    public final String IMG_KEY = "img";


    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.listviewmult);


        findViewById(R.id.button).setOnClickListener(this);


        // list data
        List<Map<String, Object>> resourceNames = new ArrayList<Map<String, Object>>();
        generateData(resourceNames);


        MyAdapter notes = new MyAdapter(this, resourceNames,
                R.layout.listrowmult, new String[] { TEXT_KEY_1, TEXT_KEY_2,
                        IMG_KEY, ITEM_ID }, new int[] { R.id.text1, R.id.text2,
                        R.id.img }, selectedItems);


        setListAdapter(notes);
    }


    private void generateData(List<Map<String, Object>> resourceNames) {
        // TODO here you will fill resourceNames with your own data


        Map<String, Object> data;
        int NUM_ITEMS = 50;


        for (int i = 0; i <= NUM_ITEMS; i++) {
            data = new HashMap<String, Object>();
            data.put(ITEM_ID, i);
            data.put(TEXT_KEY_1,
                    getString(R.string.list_item) + " " + Integer.toString(i));
            data.put(TEXT_KEY_2, getString(R.string.description));
            data.put(IMG_KEY, R.drawable.listicon);
            resourceNames.add(data);
        }
    }


    /*
     * Restores list selection
     */
    @Override
    protected void onRestoreInstanceState(Bundle state) {
        super.onRestoreInstanceState(state);
        selectedItems.addAll(state.getIntegerArrayList(SELECTED_ITEM_KEY));
    }


    /*
     * When the device is rotated, this activity is killed This method is called
     * when activity is about to be killed and saves the current list selection
     */
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putIntegerArrayList(SELECTED_ITEM_KEY, selectedItems);
    }


    /*
     * Prints on screen the currently selected items
     */
    public void onClick(View target) {
        // TODO execute your action here


        StringBuilder strText = new StringBuilder();
        strText.append(getString(R.string.selected));


        Collections.sort(selectedItems);


        boolean first = true;
        for (Integer cur : selectedItems) {
            if (first) {
                strText.append(cur);
                first = false;
            } else {
                strText.append(", " + cur);
            }
        }


        Toast.makeText(getApplicationContext(), strText.toString(),
                Toast.LENGTH_LONG).show();
    }


    class MyAdapter extends SimpleAdapter {
        List<? extends Map<String, ?>> resourceNames;
        OnItemClickListener listener = null;
        ArrayList<Integer> selectedItems;
        String[] strKeys;
        int[] ids;


        public MyAdapter(Context context, List<? extends Map<String, ?>> data,
                int resource, String[] from, int[] to,
                ArrayList<Integer> selectedItems) {
            super(context, data, resource, from, to);
            this.selectedItems = selectedItems;
            resourceNames = data;
            strKeys = from;
            ids = to;
        }


        /*
         * Returns a view to be added on the list When we scroll the list, some
         * items leave the screen becoming invisible to the user. Since creating
         * views is an expensive task, we'd rather recycle these not visible
         * views, that are referenced by convertView, updating its fields
         * values.
         */
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {


            // used to improve performance, since we call findViewById
            // only once for each created, but not recycled, view
            ViewHolder holder;


            if (listener == null)
                listener = new OnItemClickListener(selectedItems);


            // view to be recycled
            if (convertView == null) {
                holder = new ViewHolder();
                convertView = LayoutInflater.from(parent.getContext()).inflate(
                        R.layout.listrowmult, null);


                holder.tv1 = (TextView) convertView.findViewById(R.id.text1);
                holder.tv2 = (TextView) convertView.findViewById(R.id.text2);
                holder.img = (ImageView) convertView.findViewById(R.id.img);
                holder.ckb = (CheckedTextView) convertView.findViewById(R.id.ckb);
                holder.cbxLayout = (LinearLayout) convertView.findViewById(R.id.cbxLayout);


                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            Map<String, ?> currentData = resourceNames.get(position);


            // updates list items values
            holder.tv1.setText(currentData.get(strKeys[0]).toString());
            holder.tv2.setText(currentData.get(strKeys[1]).toString());
            holder.img.setImageResource((Integer) currentData.get(strKeys[2]));
            holder.ckb.setChecked(selectedItems.contains((Integer) currentData
                    .get(strKeys[3])));


            holder.cbxLayout.setId((Integer) currentData.get(strKeys[3]));
            holder.cbxLayout.setOnClickListener(listener);


            return convertView;
        }
    }


    /*
     * Holds references to list items
     */
    class ViewHolder {
        TextView tv1, tv2;
        ImageView img;
        CheckedTextView ckb;
        LinearLayout cbxLayout;
    }


    /*
     * Called when a list item is clicked
     */
    class OnItemClickListener implements OnClickListener {
        ArrayList<Integer> selectedItems;


        public OnItemClickListener(ArrayList<Integer> selectedItems) {
            this.selectedItems = selectedItems;
        }


        public void onClick(View v) {
            // handles list item click
            CheckedTextView  ckb = (CheckedTextView ) v.findViewById(R.id.ckb);
            boolean checked = ckb.isChecked();
            // updates selected list
            if (checked) {
                selectedItems.remove(new Integer(v.getId()));
            } else {
                selectedItems.add(v.getId());
            }
            // update check box value
            ckb.setChecked(!checked);
        }
    }
}


Now, the most importat details of the code will be explained.

private void generateData(List<Map<String, Object>> resourceNames) {
        // TODO here you will fill resourceNames with your own data

        Map<String, Object> data;
        int NUM_ITEMS = 50;

        for (int i = 0; i <= NUM_ITEMS; i++) {
            data = new HashMap<String, Object>();
            data.put(ITEM_ID, i);
            data.put(TEXT_KEY_1,
                    getString(R.string.list_item) + " " + Integer.toString(i));
            data.put(TEXT_KEY_2, getString(R.string.description));
            data.put(IMG_KEY, R.drawable.listicon);
            resourceNames.add(data);
        }
    }


The code above shows where the data is generated. A dumny collection of Map is created. This method should be replaced by the actual data retrieving of ones implementation.

    /*
     * Prints on screen the currently selected items
     */
    public void onClick(View target) {
        // TODO execute your action here

        StringBuilder strText = new StringBuilder();
        strText.append(getString(R.string.selected));

        Collections.sort(selectedItems);

        boolean first = true;
        for (Integer cur : selectedItems) {
            if (first) {
                strText.append(cur);
                first = false;
            } else {
                strText.append(", " + cur);
            }
        }

        Toast.makeText(getApplicationContext(), strText.toString(),
                Toast.LENGTH_LONG).show();
    }


 The code above display the selected items. Note that is uses the list selectedItems which stores the the elements which were checkec by the user.

class MyAdapter extends SimpleAdapter {
        List<? extends Map<String, ?>> resourceNames;
        OnItemClickListener listener = null;
        ArrayList<Integer> selectedItems;
        String[] strKeys;
        int[] ids;

        public MyAdapter(Context context, List<? extends Map<String, ?>> data,
                int resource, String[] from, int[] to,
                ArrayList<Integer> selectedItems) {
            super(context, data, resource, from, to);
            this.selectedItems = selectedItems;
            resourceNames = data;
            strKeys = from;
            ids = to;
        }

        /*
         * Returns a view to be added on the list When we scroll the list, some
         * items leave the screen becoming invisible to the user. Since creating
         * views is an expensive task, we'd rather recycle these not visible
         * views, that are referenced by convertView, updating its fields
         * values.
         */
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {

            // used to improve performance, since we call findViewById
            // only once for each created, but not recycled, view
            ViewHolder holder;

            if (listener == null)
                listener = new OnItemClickListener(selectedItems);

            // view to be recycled
            if (convertView == null) {
                holder = new ViewHolder();
                convertView = LayoutInflater.from(parent.getContext()).inflate(
                        R.layout.listrowmult, null);

                holder.tv1 = (TextView) convertView.findViewById(R.id.text1);
                holder.tv2 = (TextView) convertView.findViewById(R.id.text2);
                holder.img = (ImageView) convertView.findViewById(R.id.img);
                holder.ckb = (CheckedTextView) convertView.findViewById(R.id.ckb);
                holder.cbxLayout = (LinearLayout) convertView.findViewById(R.id.cbxLayout);

                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            Map<String, ?> currentData = resourceNames.get(position);

            // updates list items values
            holder.tv1.setText(currentData.get(strKeys[0]).toString());
            holder.tv2.setText(currentData.get(strKeys[1]).toString());
            holder.img.setImageResource((Integer) currentData.get(strKeys[2]));
            holder.ckb.setChecked(selectedItems.contains((Integer) currentData
                    .get(strKeys[3])));

            holder.cbxLayout.setId((Integer) currentData.get(strKeys[3]));
            holder.cbxLayout.setOnClickListener(listener);

            return convertView;
        }
    }


The code above is the custom implementation of the Base Adapter. Basically, for every row displayed it sets the appropriate layout, by inflating it, and set the content values and listeners appropriated to make the list work. Take a time to understand it. Note that the this Adapter was bound in the onCreate(Bundle) event. Also, oberve how it saves data in a Value Object structure, called ViewHolder. It does that to improve performance.

Concluding, studying the posted code one can accomplish to develop a List Activity enabling selection, using Check Boxes by the use of CheckedTextView component.