ListView row buttons: How do I create a custom Adapter that connects a View.OnClickListener to a button on each row of a ListView?

14,167

Solution 1

Here is how to create the custom Adapter, connecting View.OnClickListener to a ListView with a button per row...

1. Create a layout for a typical row

In this case, the row is composed of three view components:

  • name (EditText)
  • value (EditText:inputType="numberDecimal")
  • delete (Button)

Xml

pay_list_item.xml layout is as follows:

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

    <EditText
        android:id="@+id/pay_name"
        android:layout_width="0dp"
        android:layout_height="fill_parent"
        android:layout_weight="2"
        android:hint="Name" />

    <EditText
        android:id="@+id/pay_value"
        android:layout_width="0dp"
        android:layout_height="fill_parent"
        android:layout_weight="1"
        android:inputType="numberDecimal"
        android:text="0.0" />

    <Button
        android:id="@+id/pay_removePay"
        android:layout_width="100dp"
        android:layout_height="fill_parent"
        android:text="Remove Pay"
        android:onClick="removePayOnClickHandler" />

</LinearLayout>

Note: the button has onClick handler defined in xml layout file, because we want to refer its action to a specific list item.

Doing this means that the handler will be implemented in Activity file and each button will know which list item it belongs to.

2. Create list item adapter

This is the java class that is the controller for pay_list_item.xml.

It keeps references for all of its views, and it also puts these references in tags, extending the ArrayAdapter interface.

The Adapter:

public class PayListAdapter extends ArrayAdapter<Payment> {

    private List<Payment> items;
    private int layoutResourceId;
    private Context context;

    public PayListAdapter(Context context, int layoutResourceId, List<Payment> items) {
        super(context, layoutResourceId, items);
        this.layoutResourceId = layoutResourceId;
        this.context = context;
        this.items = items;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View row = convertView;
        PaymentHolder holder = null;

        LayoutInflater inflater = ((Activity) context).getLayoutInflater();
        row = inflater.inflate(layoutResourceId, parent, false);

        holder = new PaymentHolder();
        holder.Payment = items.get(position);
        holder.removePaymentButton = (ImageButton)row.findViewById(R.id.pay_removePay);
        holder.removePaymentButton.setTag(holder.Payment);

        holder.name = (TextView)row.findViewById(R.id.pay_name);
        holder.value = (TextView)row.findViewById(R.id.pay_value);

        row.setTag(holder);

        setupItem(holder);
        return row;
    }

    private void setupItem(PaymentHolder holder) {
        holder.name.setText(holder.Payment.getName());
        holder.value.setText(String.valueOf(holder.Payment.getValue()));
    }

    public static class PaymentHolder {
        Payment Payment;
        TextView name;
        TextView value;
        ImageButton removePaymentButton;
    }
}

Here we list the Payment class items.

There are three most important elements here:

  • PayListAdapter constructor: sets some private fields and calls superclass constructor. It also gets the List of Payment objects. Its implementation is obligatory.
  • PaymentHolder: static class that holds references to all views that I have to set in this list item. I also keep the Payment object that references to this particular item in list. I set it as tag for ImageButton, that will help me to find the Payment item on list, that user wanted to remove
  • Overriden getView method: called by superclass. Its goal is to return the single List row. We create its fields and setup their values and store them in static holder. Holder then is put in row’s tag element. Note that there is a performance issue, as the row is being recreated each time it is displayed. I used to add some flag in holder like isCreated, and set it to true after row was already created. then you can add if statement and read tag’s holder instead of creating it from scratch.

Payment.java is quite simple as for now and it looks a bit like BasicNameValuePair:

public class Payment implements Serializable {
    private String name = "";
    private double value = 0;

    public Payment(String name, double value) {
        this.setName(name);
        this.setValue(value);
    }
...
}

There are additional gets and sets for each private field not shown.

3. Add ListView to the activity layout xml file

In its simpliest form, it will be enough to add this view to activity layout:

<ListView 
    android:id="@+id/EnterPays_PaysList"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">
</ListView>

4. Set up adapter to this list view in Activity Java code

In order to display items in ListView you need to set up its adapter and map it to some other ArrayList of Payment objects (as I am extending an Array adapter here). Here is code that is responsible for binding adapter to editPersonData.getPayments() ArrayList:

PayListAdapter adapter = new PayListAdapter(AddNewPerson.this, R.layout.pay_list_item, editPersonData.getPayments());
ListView PaysListView = (ListView)findViewById(R.id.EnterPays_PaysList);
PaysListView.setAdapter(adapter);

5. Adding / removing items to ListView (and its adapter)

Adapter is handled just like any other ArrayList, so adding new element to it is as simple as:

Payment testPayment = new Payment("Test", 13);
adapter.add(testPayment);
adapter.remove(testPayment);

6. Handle Remove Payment button click event

In an activity’s code, where ListView is displayed, add public method that will handle remove button click action. The method name has to be exactly the same as it was in pay_list_item.xml:

android:onClick="removePayOnClickHandler"
The method body is as follows:

public void removePayOnClickHandler(View v) {
    Payment itemToRemove = (Payment)v.getTag();
    adapter.remove(itemToRemove);
}

The Payment object was stored in ImageButton’s Tag element. Now it is enough to read it from Tag, and remove this item from the adapter.

7. Incorporate remove confirmation dialog window

Probably you need also make sure that user intentionally pressed the remove button by asking him additional question in confirmation dialog.

Dialogue

a) Create dialog’s id constant

This is simply dialog’s ID. it should be unique among any other dialog window that is handled by current activity. I set it like that:

protected static final int DIALOG_REMOVE_CALC = 1;
protected static final int DIALOG_REMOVE_PERSON = 2;

b) Build dialog

I use this method to build dialog window:

private Dialog createDialogRemoveConfirm(final int dialogRemove) {
    return new AlertDialog.Builder(getApplicationContext())
    .setIcon(R.drawable.trashbin_icon)
    .setTitle(R.string.calculation_dialog_remove_text)
    .setPositiveButton(R.string.calculation_dialog_button_ok, new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int whichButton) {
            handleRemoveConfirm(dialogRemove);
        }
    })
    .setNegativeButton(R.string.calculation_dialog_button_cancel, null)
    .create();
}

AlertDialog builder pattern is utilized here. I do not handle NegativeButton click action – by default the dialog is just being hidden. If dialog’s confirm button is clicked, my handleRemoveConfirm callback is called and action is performed based on dialog’s ID:

protected void handleRemoveConfirm(int dialogType) {
    if(dialogType == DIALOG_REMOVE_PERSON){
        calc.removePerson();
    }else if(dialogType == DIALOG_REMOVE_CALC){
        removeCalc();
    }
}

c) Show Dialog

I show dialog after my remove button click. The showDialog(int) is Android’s Activity’s method:

OnClickListener removeCalcButtonClickListener = new OnClickListener() {
    public void onClick(View v) {
        showDialog(DIALOG_REMOVE_CALC);
    }
};

the showDialog(int) method calls onCreateDialog (also defined in Activity’s class). Override it and tell your app what to do if the showDialog was requested:

@Override
protected Dialog onCreateDialog(int id) {
    switch (id) {
    case DIALOG_REMOVE_CALC:
        return createDialogRemoveConfirm(DIALOG_REMOVE_CALC);
    case DIALOG_REMOVE_PERSON:
        return createDialogRemoveConfirm(DIALOG_REMOVE_PERSON);
    }
}

Solution 2

Take a look at this blog post I wrote on exactly this matter:

Create custom ArrayAdapter

There are comments that explain every action I make in the adapter. Here is the explanation in short:

So lets for example take a row where you want to place a CheckBox, ImageView and a TextView while all of them are clickable. Meaning that you can click the row it self for going to another Actvity for more details on the row, check its CheckBox or press the ImageView to perform another operation.

So what you should do is:

1. First create an XML layout file for your ListView row:

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<CheckBox
    android:id="@+id/cbCheckListItem"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="10dp" />
<TextView
    android:id="@+id/tvItemTitle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="item string" />
<ImageView
    android:id="@+id/iStatus"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:clickable="true"
    android:contentDescription="@drawable/ic_launcher"
    android:src="@drawable/ic_launcher" /> 
  </LinearLayout>

2. Second in your java code define a ViewHolder, a ViewHolder is designed to hold the row views and that way operating more quickly:

static class ViewHolder 
{
TextView title;
CheckBox checked;
ImageView changeRowStatus;
}

3. Now we have to define CustomArrayAdapter, using the array adapter we can define precisely what is the desired output for each row based on the content of this row or it’s position. We can do so by overriding the getView method:

private class CustomArrayAdapter extends ArrayAdapter<RowData>
{   
private ArrayList<RowData> list;

//this custom adapter receives an ArrayList of RowData objects.
//RowData is my class that represents the data for a single row and could be anything.
public CustomArrayAdapter(Context context, int textViewResourceId, ArrayList<RowData> rowDataList) 
{
    //populate the local list with data.
    super(context, textViewResourceId, rowDataList);
    this.list = new ArrayList<RowData>();
    this.list.addAll(rowDataList);
}

public View getView(final int position, View convertView, ViewGroup parent)
{
    //creating the ViewHolder we defined earlier.
    ViewHolder holder = new ViewHolder();) 

    //creating LayoutInflator for inflating the row layout.
    LayoutInflater inflator = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);

    //inflating the row layout we defined earlier.
    convertView = inflator.inflate(R.layout.row_item_layout, null);

    //setting the views into the ViewHolder.
    holder.title = (TextView) convertView.findViewById(R.id.tvItemTitle);
    holder.changeRowStatus = (ImageView) convertView.findViewById(R.id.iStatus);
    holder.changeRowStatus.setTag(position);

    //define an onClickListener for the ImageView.
    holder.changeRowStatus.setOnClickListener(new OnClickListener() 
    {           
        @Override
        public void onClick(View v) 
        {
            Toast.makeText(activity, "Image from row " + position + " was pressed", Toast.LENGTH_LONG).show();
        }
    });
    holder.checked = (CheckBox) convertView.findViewById(R.id.cbCheckListItem);
    holder.checked.setTag(position);

    //define an onClickListener for the CheckBox.
    holder.checked.setOnClickListener(new OnClickListener() 
    {       
        @Override
        public void onClick(View v)
        {
            //assign check-box state to the corresponding object in list.    
            CheckBox checkbox = (CheckBox) v;
            rowDataList.get(position).setChecked(checkbox.isChecked());
            Toast.makeText(activity, "CheckBox from row " + position + " was checked", Toast.LENGTH_LONG).show();    
        }
    });

    //setting data into the the ViewHolder.
    holder.title.setText(RowData.getName());
    holder.checked.setChecked(RowData.isChecked());

    //return the row view.
    return convertView;
}
}

4. Now you need to set this adapter, as the adapter of your ListView. this ListView can be created in java or using an XML file, in this case I’m using a list that was defined in the XML file using the “list” id:

public void onCreate(Bundle savedInstanceState) 
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_layout); 
ListView list = (ListView)findViewById(R.id.list);
CustomArrayAdapter dataAdapter = new CustomArrayAdapter(this, R.id.tvItemTitle, rowDataList);
list.setAdapter(dataAdapter);
}

5. Finally if we want to be able to press the row it self and not only a certain view in it we should assign an onItemClickListener to the ListView:

list.setOnItemClickListener(new OnItemClickListener() 
{
public void onItemClick(AdapterView<?> parent, View view,int position, long id) 
{
    Toast.makeText(activity, "row " + position + " was pressed", Toast.LENGTH_LONG).show();
}
});
Share:
14,167
WonderWorker
Author by

WonderWorker

I like to spend my time writing my own software, creating cartoons, recording music, swimming and spending time with my lovely wife and dog. Inexplicably and unashamedly, I adore old processors and my favourites are MOS 6502, Zilog Z80 and Motorola 680x0. My goal is to successfully combine curriculum learning with video games with an aim to create something better than both, and not worse.

Updated on June 05, 2022

Comments

  • WonderWorker
    WonderWorker almost 2 years

    I want my ListView to contain buttons, but setting the button's xml property, onClick="myFunction" and then placing a public void myFunction(android.view.View view) method in the activity causes an NoSuchMethodException (the stack trace is null) to be thrown, as although the onclick listener is there, it doesn't fire myFunction(...) and cause the activity to close.

    How do I create a custom Adapter that connects a View.OnClickListener to a button on each row of a ListView?

    My ListView is created as follows...

    [activity.java content..]

    public void myFunction(android.view.View view)
    {
        //Do stuff
    }
    

    [activity.xml content..]

    <LinearLayout xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".FrmCustomerDetails" >
        <ListView android:id="@+id/LstCustomerDetailsList" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_weight="1" android:clickable="true" android:clipChildren="true" android:divider="@null" android:dividerHeight="0dp" android:fastScrollEnabled="true" android:footerDividersEnabled="false" android:headerDividersEnabled="false" android:requiresFadingEdge="vertical" android:smoothScrollbar="true" />
    </LinearLayout>
    

    [activity_row_item.xml content..]

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/Llt" android:layout_width="match_parent" android:layout_height="match_parent" >
        <Button android:id="@+id/Btn" android:text="Click me" android:onClick="myFunction" />
    </LinearLayout>
    
    • Duncan Jones
      Duncan Jones about 11 years
      Please post your current code (edited down to the vital information) plus your stacktrace.
    • WonderWorker
      WonderWorker about 11 years
      The stack trace is null
  • Jacek Milewski
    Jacek Milewski almost 11 years
    since this is exact copy-paste from my blog post: looksok.wordpress.com/2012/11/03/… could you please edit your answer and add a note/url about it or just link to url?
  • Alioo
    Alioo over 10 years
    Okay what the heck is getPayments? You never made that.