måndag 9 april 2012

Android : Multiple Selection ListView with Custom Layout

This is one solution for the troublesome ListView when not using the internal layouts. This blogpost helped me alot as toggling the checkbox in the adapter or in the ListView's setOnItemClickListener() gave me strange results, based on the visibility of the row. He implemented the Checkable interface in the layout container, i use the same extended RelativeLayout.

Lets begin with the CheckableRelativeLayout.
src/CheckableRelativeLayout.java
import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Checkable;
import android.widget.RelativeLayout;

/**
 * Extension of a relative layout to provide a checkable behaviour
 *
 * @author marvinlabs
 */
public class CheckableRelativeLayout extends RelativeLayout implements
        Checkable {

    private boolean isChecked;
    private List<Checkable> checkableViews;

    public CheckableRelativeLayout(Context context, AttributeSet attrs,
                                   int defStyle) {
        super(context, attrs, defStyle);
        initialise(attrs);
    }

    public CheckableRelativeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialise(attrs);
    }

    public CheckableRelativeLayout(Context context, int checkableId) {
        super(context);
        initialise(null);
    }

    /*
      * @see android.widget.Checkable#isChecked()
      */
    public boolean isChecked() {
        return isChecked;
    }

    /*
      * @see android.widget.Checkable#setChecked(boolean)
      */
    public void setChecked(boolean isChecked) {
        this.isChecked = isChecked;
        for (Checkable c : checkableViews) {
            c.setChecked(isChecked);
        }
    }

    /*
      * @see android.widget.Checkable#toggle()
      */
    public void toggle() {
        this.isChecked = !this.isChecked;
        for (Checkable c : checkableViews) {
            c.toggle();
        }
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        final int childCount = this.getChildCount();
        for (int i = 0; i < childCount; ++i) {
            findCheckableChildren(this.getChildAt(i));
        }
    }

    /**
     * Read the custom XML attributes
     */
    private void initialise(AttributeSet attrs) {
        this.isChecked = false;
        this.checkableViews = new ArrayList<Checkable>(5);
    }

    /**
     * Add to our checkable list all the children of the view that implement the
     * interface Checkable
     */
    private void findCheckableChildren(View v) {
        if (v instanceof Checkable) {
            this.checkableViews.add((Checkable) v);
        }

        if (v instanceof ViewGroup) {
            final ViewGroup vg = (ViewGroup) v;
            final int childCount = vg.getChildCount();
            for (int i = 0; i < childCount; ++i) {
                findCheckableChildren(vg.getChildAt(i));
            }
        }
    }
}

After that just use it in your XML layout files, make it part of your package and use syntax <org.mypackage.myapp.CheckableRelativeLayout>

Our testclass Team, which we want to populate the ListView with.
src/Team.java
public class Team {
    private String teamName;
    private int teamWins;
    public  Team(String name, int wins)
    {
        teamName = name;
        teamWins = wins;
    }

    public String getTeamName() {
        return teamName;
    }

    public int getTeamWins() {
        return teamWins;
    }
}


Simple class for testing purposes, next up we build our ListView layout, what each row will look like.
I decided on a CheckBox (the new one as the old one felt big), and two TextViews to display the teamname & team wins. (Remember to look at the CheckableRelativeLayout class above as this isn't working with the normal containers.)

layout/row_team_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<se.adanware.listviewexample.CheckableRelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

   <!-- We dont want to be able to click the CheckBox -
        android:clickable="false" added.
        CheckableRelativeLayout takes care of the toggle when clicking the row -->
    <CheckBox
              android:id="@+id/myCheckBox"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:focusable="false"
              android:clickable="false"
              android:layout_alignParentLeft="true"
              android:background="@drawable/customcheckbox_background"
              android:button="@drawable/customcheckbox"
            />
    <TextView
            android:id="@+id/listview_TeamDescription"
            android:focusable="false"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#FFFFFF"
            android:textSize="16sp"
            android:layout_toRightOf="@id/myCheckBox"
            android:layout_centerVertical="true"
            />
    <TextView
            android:id="@+id/listview_TeamWins"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#FFFFFF"
            android:textSize="16sp"
            android:paddingRight="5dp"
            android:layout_centerVertical="true"
            android:focusable="false"
            android:layout_alignParentRight="true"
            />
</se.adanware.listviewexample.CheckableRelativeLayout>

I'm still targeting  the earlier Android version so i've taken the new checkbox from the SDK.

drawable-hdpi/customcheckbox.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="false"
          android:drawable="@drawable/btn_check_off_holo_dark" />
    <item android:state_checked="true"
          android:drawable="@drawable/btn_check_on_holo_dark" />
</selector>
drawable-hdp/customcheckbox_background.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/btn_check_label_background" />
</selector>
btn_check_label_background.9.png    


btn_check_on_holo_dark.png
btn_check_off_holo_dark.png
Hopefully the images can be seen, i'll change the background color later, now it's turn for our custom adapter that we'll populate the list with.
src/TeamListViewAdapter.java
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.ArrayList;

public class TeamListViewAdapter extends ArrayAdapter<Team>
        {
        View row;
        ArrayList<Team> myTeams;
        int resLayout;
        Context context;

        public TeamListViewAdapter(Context context, int textViewResourceId, ArrayList<Team> myTeams) {
            super(context, textViewResourceId, myTeams);
            this.myTeams = myTeams;
            resLayout = textViewResourceId;
            this.context = context;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent)
        {
            row = convertView;
            if(row == null)
            {   // inflate our custom layout. resLayout == R.layout.row_team_layout.xml
                LayoutInflater ll = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                row = ll.inflate(resLayout, parent, false);
            }

            Team item = myTeams.get(position); // Produce a row for each Team.

            if(item != null)
            {   // Find our widgets and populate them with the Team data.
                TextView myTeamDescription = (TextView) row.findViewById(R.id.listview_TeamDe                                                                       scription);
                TextView myTeamWins = (TextView) row.findViewById(R.id.listview_TeamWins);
                if(myTeamDescription != null)
                    myTeamDescription.setText(item.getTeamName());
                if(myTeamWins != null)
                    myTeamWins.setText("Wins: " + String.valueOf(item.getTeamWins()));
            }
            return row;
        }
}

Okay! Now we just need to make the root layout and our start activity! First a simple layout:

res/main.xml
<?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"
    >
<TextView  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="Checkable Listview Example"
    />
    <Button android:id="@+id/buttonStart"
            android:layout_height="wrap_content"
            android:layout_width="fill_parent"
            android:text="Start tournament with selected teams"
            />
<ListView android:id="@+id/myListView"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent"
          android:choiceMode="multipleChoice"/>
</LinearLayout>

Nothing unusual here, android:choiceMode="multipleChoice" cant be omitted.
It can also be set from code with myListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
There's also android:choiceMode="singleChoice" if you just want a single item checked.
If not set the ListView.getCheckedItemPositions() method will return a null SparseBooleanArray.

Finally we come to our startup activity!


src/ListViewExample.java
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.View;
import android.widget.*;
import java.util.ArrayList;

public class ListViewExample extends Activity
{
    ArrayList<Team> myTeams;
    TeamListViewAdapter myAdapter;
    ListView myListView;
    Button myButton;
    
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        myTeams = new ArrayList<Team>();
        // Add a few teams to display.
        myTeams.add(new Team("Winners", 10));
        myTeams.add(new Team("Philidelphia Flyers", 5));
        myTeams.add(new Team("Detroit Red Wings", 1));
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        myListView = (ListView) findViewById(R.id.myListView);
        myButton = (Button) findViewById(R.id.buttonStart);
        // Construct our adapter, using our own layout and myTeams
        myAdapter = new TeamListViewAdapter(this, R.layout.row_team_layout, myTeams );
        myListView.setAdapter(myAdapter);
        myListView.setItemsCanFocus(false);

        myButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ArrayList<Team> selectedTeams = new ArrayList<Team>();
                final SparseBooleanArray checkedItems = myListView.getCheckedItemPositions();
                int checkedItemsCount = checkedItems.size();
                for (int i = 0; i < checkedItemsCount; ++i) {
                   // Item position in adapter
                   int position = checkedItems.keyAt(i);
                   // Add team if item is checked == TRUE!
                   if(checkedItems.valueAt(i))
                      selectedTeams.add(myAdapter.getItem(position));
                }
                if(selectedTeams.size() < 2)
                  Toast.makeText(getBaseContext(), "Need to select two or more teams.", Toast                                                       .LENGTH_SHORT).show();
                else
                {
                   // Just logging the output.
                   for(Team t : selectedTeams)
                      Log.d("SELECTED TEAMS: ", t.getTeamName());
                }
            }
        });
    }
}


Thanks goes to http://www.marvinlabs.com/2010/10/custom-listview-ability-check-items/ for the elegant solution!



... Family time, nightie!


11 kommentarer:

  1. Hi Your Tutorial is perfect but i had a small problem whenever i click on the list item nothing is happening how to do that and you mentioned that Checkable interface and CheckableRelativeLayout i did not get what are those so COULD YOU PLEASE SEND THE SOURCE CODE OF ABOVE EXAMPLE to my mail id LAXMANAMAIL@GMAIL.COM waiting for your replay Thanx

    SvaraRadera
  2. same problem...unable to load checkableRelativeLayout..please if you could send me the code on snehalpoyrekar@gmail.com...I would really appreciate it..thanks

    SvaraRadera
    Svar
    1. Updated the blogpost, looks like the page hosting the CheckableRelativeLayout class is doing some rework.

      Class is included at the top of the blogpost, should work after that.
      Good luck!

      Radera
  3. For the android new comers like me...

    Do not forget to set the package in the CheckableRelativeLayout !

    SvaraRadera
  4. hi
    i am using a custom row including a Spinner and a Checkbox,
    i don't know how , but the spinner seems to intercept the touch and the RelativeLayout above don't do the work
    could anyone help plz

    SvaraRadera
    Svar
    1. Don't really understand what you're trying to do. You have a spinner in each row to select 'something', then a checkbox to select the #s of spinner you want ?

      Radera
    2. Yes it's like you say ...

      Radera
    3. Tried it and it's like you said. Google solved it for me. Set tag android:descendantFocusability="blocksDescendants" in your RelativeLayout. You will not be able to focus the spinner with keyboard but clicking the spinner opens it, clicking the row activates the checkbox.

      Radera
  5. The blogpost is currently available in 'The MarvinLabs Blog':
    http://blog.marvinlabs.com/2010/10/29/custom-listview-ability-check-items/

    SvaraRadera
  6. Congrats. Nice tutorial

    I have used instructions, and I'm in position to retrieve my selected values, but my checkboxes are not selected, can someone help me please, I ve spent hours on.

    SvaraRadera
  7. Top 10 casino games, ranked: how to play, tips, bonuses
    Casino games 양산 출장샵 online are 경산 출장안마 a part of the world of casino gaming. Casino games are 경기도 출장샵 some of the most 충주 출장안마 played 김천 출장샵 casino games in the world

    SvaraRadera