Saturday, November 21, 2009

Android: Custom List Item with nested clickable Button

This tutorial will show you how to add a button (or any other clickable item) to a customized list view item.


In the screenshot above you can see a ListView with custom built items. Each list item consists of
  • TextView for the title
  • TextView for the content
  • RelativeLayout (clickable) consisting of
    • TextView: "For more information click here"
    • ImageView for the arrow icon
We start this tutorial at a point where you should already know how to create your own customized ListViews and Adapters. If you have no clue how to do so please let me know in the comments. If there's a demand I will eventually compile another tutorial about creating custom list items and list adapters in the future.
1) Build custom adapter that extends BaseAdapter and implement all the required inherited abstract methods from the parent class:

2) Implement View getView(int position, View convertView, ViewGroup parent):
This is where you create and manipulate the view that is used for a list item. I highly recommend that you take advantage of a view holder as it is described in this API example to improve speed and efficiency.

The crucial part is now that you, after you have inflated the list item's main layout

you also need to get hold of the sub-layout (bottom part). That's the part you want to be clickable.

3) Create an onClickListener for the bottom layout and associate the data (in this case a web URL) with the current view:

While for a OnItemClickListener, which is associated to each item of a ListView, we always know which item of the list has been selected through the int position paremeter. We don't have this information for our OnClickListener that is used in the bottom layout. So how do we get the neccessary information that is associated to the current list item?

The trick is to use View.setTag() and View.getTag(). The Android documentation states: “Sets the tag associated with this view. A tag can be used to mark a view in its hierarchy and does not have to be unique within the hierarchy. Tags can also be used to store data within a view without resorting to another data structure.”

In this example we use setTag() to store an URL with each sub-layout and getTag() in the onClickListener to extract that URL whenever a sub-layout was clicked. Then we create an Intent for our WebView, pass the URL to it and start a new Activity. And Voilà! You should have a web browser now displaying the website associated to the item you clicked before.

However, there's one little detail that keeps annoying me. Even though I now can click/tap the bar at the bottom via the touch screen interface I'm not able to select it via the trackball. It always selects the whole list item (see screenshot below) and from there goes directly to the next list item ignoring the bottom bar, even though I set .setFocusable(true) and setClickable(true) for the bottom bar's RelativeLayout in getView().



If anyone knows a solution to this please let me know in the comments or respond to my question asked on stackoverflow.

28 comments:

snigdha said...

Hi Stefan
it's a very helpful example indeed but could you please elaborate as i am stuck with on click of a button in a list.
thanks

Stefan Klumpp said...

Hey snigdha. Sure. Just let me know what your problem is and I'll try to help.

Mike said...

Stefan

Thanks for the tutorial. One question, how would you go about removing an item from the list when you click a button in that item? I'm currently using a custom cursor adapter and in my onclicklistener I remove the item form the database and then do a cursor requery, but all the items in my list disappear. If I go away and come back it correctly shows the remaining items. It's driving me crazy. Do you have any ideas? Thanks again.

Stefan Klumpp said...

Hey Mike,

Sorry, I've not worked with cursors/databases on Android, yet. So I'm not really able to help you at the moment. I'd suggest you ask your problem on http://stackoverflow.com/ - there you should usually get a response very quickly.

level32 said...

@Mike

extend ArrayAdapter instead of BaseAdapter.

You can simply call remove(item) on the adapter.

Don't forget to call notifyDataSetChanged() on the adapter as well. This will update the ui.

ajree said...

Hi Stefan,

Thanks for this example - it saved me many hours.

I tried to optimize it a bit more and it seems to be working. Basic idea is to have only one View.OnClickListener per clickable element in all rows. So instead of calling new View.OnClickListener() on every clickable in every row, I put these listeners as members of the adapter class. Since they operate on tags, they know which widget triggered them.

public class MyAdapter extends BaseAdapter {

private View.OnClickListener mMoreInfoClickListener = new View.OnClickListener() {
//(...)
};

@Override
public View getView(int position, View convertView, ViewGroup parent) {
//(...)
holder.layout_bottom.setOnClickListener(mMoreInfoClickListener);
}


I'm pretty new to Java and Android, and I'm not sure if this is valid approach, but as I said it seems to be working nicely.

Stefan Klumpp said...

Hey ajiree,

Thanks for sharing your improvement. Using just one Listener definitely makes sense.

anais said...

Hi,

Thx for this tutorial.

But, I have some questions ??

When I click on the button, the OnClick in the Adapter works, but where I do the link with my Activity where is my List?

I Have a list with one ImageView and TextView. When user click on textview , it open another Activity. When user click on Button, I would like to open a dialog.

Anonymous said...

Thanks Stefan. This helped.

Punit

chrisonline said...

Hi Stefan!

I've tried your solution and its great, but i have a problem.

After adding the .isClickable to the Layout my button is clickable but not the whole listview anymore.

I need click on the ListView (expands more fields) and click on the button inside the ListView.

Like the Gmail app.

Do you have a solution?

Anonymous said...

Hi Stefan,

thank you for this helpful tutorial .
I want to make my list look like yours, I would like alternate the backround color of raws . would you please help me and indicate wich method can i use . or a source code if you have .
thank you

Stefan Klumpp said...

Just use a simple "if (rowNumber % 2) { set color 1 } else { set color 2}

Anonymous said...

@chrisonline

try onitemclick of listview
your whole listitem is then clickable

android said...

@chrisonline

try onitemclick of listview

like this

yourList..setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView parent, View view, int position, long id) {
//to do when item is clicked
}
};
your whole listitem is then clickable

Anonymous said...

Thanks for helping clear this up! Your tutorial was great.

The API Example link in the article appears to be broken, btw. The List14 example has moved to
http://developer.android.com/resources/samples/ApiDemos/src/com/example/android/apis/view/List14.html

Stefan Klumpp said...

Thanks. Link is fixed :-)

taf said...

Answer to focus from stack overflow:

Probably you've found how to do it, but you can call ListView.setItemsCanFocus(true), and your buttons will catch focus...

Rajen Raiyarela said...

Hello SteFan,

I m confused on one things, i have a situation in which i have a list view with imageview (Which i want to make clickable), so if user clicks on any item, it will do some process and if user click on imageview, then a google map will open which will show position marked using latitude and longitude. i have a array of latitude and longitude. Now how i need to settag, so i can get a particular set of latitude and longitude for a google map, when user click on image view ?

Anonymous said...

Hi,
I am pretty new to android and hence dont know much about customized list views and adapters. I would be thankful if you could provide me with the complete code and a bit of explanation regarding it.
Thnx..

Simao M said...

Did you manage to solve your focus problem?

Thanks.

GKB said...

Posted a follow-up on SO on how to get the adapter position for a clicked View in a ListView row:
http://stackoverflow.com/questions/1709166/android-listview-elements-with-multiple-clickable-buttons/5320540#5320540

Stefan Klumpp said...

@GKB: Thanks :-)

i_raqz said...

Thank you for the great blog in here...but could you please help me on the same lines...I have posted my question in SO..kindly have a look

http://stackoverflow.com/questions/6535558/call-notifyondatasetchanged-from-another-adapter

win_wizardy said...

hi there, i also newbie here..
i really hope that you can give more code for this example..please

i had my hard time trying to make this works..

Anonymous said...

I would be grateful if you could do a write-up for custom ListViews!

Thanks

rachana said...

Hello thnx for tutorial
but i have a little problem which is related to ListView.

I have a list view to which i bind a user information list having 2 TextViews and 1 button.
On click of the button i want to get whole object of that user and pass it to next page.
I have written everything i could but my button click is not working.
Can u please help me in it?

Gabriel Suárez said...

si eres de barcelona, debes hablar español no? en todo caso te queria preguntar si sabes acerca de personalizar tableLayouts en Android, porque tengo una duda hacerca de incluir ListViews dentro de una celda en la tabla, no se como se podria o que tanta versatilidad tiene, asimismo con la lista dentro de un GridView, estoy tratando de pensar como hacerlo y honestamente no se me ocurre nada, saludos y que buen trabajo

Adil Soomro said...

Great post.!

but I wonder why did you use TextView and ImageView inside RelativeLayout instead of using single TextView with drawable at right property.

However the post was very helpful for me.