The touch screens brought simplicity to our lives. We can scroll, zoom and click with just a simple gesture, using our fingers. Showing it to me 15 years ago, would’ve make me think that’s an act of pure magic!
However, it brought lots of issues regarding the handling of touch events with other inputs, like keyboard or trackball. With keyboard, we can just press the Up and Down keys to select different items, and afterward hit “Enter” to click on the selected item, making it our “final” pick. How can we do that in a touch screen? If pressing an item on the list perform a “Click” action, how can we perform a “Select” action? The Android OS developers thought about that, and decided to create a mode, called “Touch Mode”, which handles focus and selection of items differently. More about it on the Android Documentation. Long story short – they chose to ignore the “selection” of list items in touch mode!
Their decision to make touch interaction simpler for the developer, limits the power of ListView (The old “Simplicity <-> Control” trade-off. Apple is well known for choosing the first approach most of the times). As I needed to make such Selectable-List, I had to find the right way, using the components given to me. The user-experience I was looking for, was that each click on a list item will “select” it, and there will be a different button to press in order to finalize my selection, like a “Set” button under the list itself.
The idea is simple.
- Use the setOnItemClickListener of ListView to set a “Selected Flag” for the current item
- Refresh the list
- Make sure the selected item is painted differently than the other list items.
The flag representation must be for each item in the list, whether it’s a Cursor-Based adapter or an Array-Adapter, we need to make sure to have a field for that information. On a cursor-based adapter, we can add a column to the DB table, making it in charge of that function. In an Array-Adapter, we can make data-structure, Keeping our information aside to a selected-state information. A good example to such data-structure is this:
class StateListItem { public String itemTitle; public long id; public Boolean isItemSelected; public StateListItem(String name, long id) { this.itemTitle = name; this.isItemSelected = false; this.id = id; } @Override public String toString() { return this.itemTitle; } }
With the ID, Title and the selected state (isItemSelected), we have all the data we need to make this work.
Now, in order to make the list item look different when it’s selected, we’ll just override the adapter’s getView() function, like this:
@Override public View getView(int position, View convertView, ViewGroup parent) { View currView = super.getView(position, convertView, parent); StateListItem currItem = getItem(position); if (currItem.isItemSelected) { currView.setBackgroundColor(Color.RED); } else { currView.setBackgroundColor(Color.BLACK); } return currView; }
We don’t care about the logic of getView, we just want to change the view’s background to Red when selected, and keep it Black when not. That’s why we’re calling the super‘s getView first, and then making our adjustments.
You’re probably wondering why do I need to set the default color for each item, and not relying it to be the default color unless I change it to Red. Well, that’s due to the ListView’s recycling mechanism (Info on that can be found here). The ListView is recycling views, which makes us reset any visual property every time we populate them, otherwise – we’ll get a strange behavior of the items’ appearance.
After knowing how to mark the selected item, and use that to change its looks, we can finish the rest of our customized list adapter, handling the way we want to change and keep the list’s current selected item. That’s the how it looks:
public class SelectableListAdapter extends ArrayAdapter { // Keeping the currently selected item int mCurrSelected = -1; // Since most of the actions gets the id but needs the position, // we'll map Ids to Positions private HashMap mIdToPosition; public SelectableListAdapter(Context context, int textViewResourceId) { super(context, textViewResourceId); init(); } private void init() { mIdToPosition = new HashMap(); } @Override public View getView(int position, View convertView, ViewGroup parent) { View currView = super.getView(position, convertView, parent); StateListItem currItem = getItem(position); if (currItem.isItemSelected) { currView.setBackgroundColor(Color.RED); } else { currView.setBackgroundColor(Color.BLACK); } return currView; } public void add(String object, long id) { StateListItem newItem =new StateListItem(object, id); // Adding the new item to the Id->Position HashMap mIdToPosition.put(id, getCount()); super.add(newItem); } @Override public void remove(StateListItem object) { super.remove(object); mIdToPosition.remove(object.id); } /** * Setting the item in the argumented position - as selected. * @param position * @return */ public long setSelectable(int position) { // The -1 value means that no item is selected if (mCurrSelected != -1) { getItem(mCurrSelected).isItemSelected = false; } // Selecting the item in the position we got as an argument if (position != -1) { getItem(position).isItemSelected = true; mCurrSelected = position; } // Making the list redraw notifyDataSetChanged(); return getSelectedId(); } public long setSelectableId(long id) { // First, we need to get the position for our item's id int pos = mIdToPosition.get(id); return setSelectable(pos); } @Override public long getItemId(int position) { return super.getItem(position).id; } /** * Needed to notify the ListView's system that the IDs we use here are unique to each item */ @Override public boolean hasStableIds() { return true; } public long getSelectedId() { if (mCurrSelected == -1) return -1; else { return getItemId(mCurrSelected); } } }
A cursor-based adapter will be similar to that, just with a different approach to the data – as described before.
This adapter can also be used with any ListView and can easily be adapted to fit ExpandableListView as well!.
Great stuff – made my day .
Got it work but for those who read it ater me please add also usage from onItemClick mmethod .
Hello,
Thx for the tip, but for me convertView in getView is always null for the 1st item. any idea?
for example :
if (currItem.isItemSelected) {
convertView.setBackgroundColor(Color.GREEN);
} else {
if(convertView!=null){
convertView.setBackgroundColor(Color.WHITE);
}
}
the 1st item is not white
Hey there,
That’s actually my fault, the setBackgroundColor should be on the currView, and not on the convertView. Fixed it! The convertView is suppose to be null for the first few items on the list. Why? because of the way the recycling mechanism works. You can check that link that I gave somewhere on this post about that.
Oh udinic, thank you so much. 🙂
i want this to be done on a separated list adapter but i doubt if that’s possible.
Why the doubt? In this post I gave an example of a new list adapter, implementing this feature. Maybe I don’t understand what you mean…please explain more about what do you want to do
Select one item and now select a second item. Dont you think now, both will be highlighted.
Hey Udinic, You have given a nice article/tutorial for the the purpose I am looking to achieve. Somehow, after implementing you steps above, my listView only is not getting updated. I know I have messed up with my code (which is quite large). I need a favor from you. It would be great If you can give the whole sample application with this custom adapter implemented. Please let me know if it is possible.
You can also assign a selector xml file as the background drawable for the list item rather than doing it in java. Even easier.
Due to the recycling nature of the list items on the list, this option will not work. If you’ll set a background upon press, that data will be lost as soon as you scroll that item outside the visible area of the list.
Even worse than that – some other unrelated list item will get that data (objects recycling..) and it’ll look like the user has pressed it.
Hi,
This is really cool and helped me a lot. I have one query though:
I want to do the selecting on long press, which works roughly the same – the only issue is that the default highlight that you get when you press the list item isn’t done (how it normally goes blue then back to black when you take your finger off the screen again). I think that this is because of the else part of the if statement setting the non selected ones to black regardless of the pressed state.
If you know how I could hook into that to get the default press behavour back, that would be awesome. If not – thanks anyway for the rest.
I second the request for a working example. My compiler can’t find a type for ViewGroup, and other stuff. don’t see where you are loading up the adapter, etc.
the else{ currView.setBackgroundColor(Color.BLACK);} was a big help to me.
I modified slightly but works for me.
el logo: https://udinic.files.wordpress.com/2013/06/blog_logo.png
tiene un fallo, tiene la pieza central repetida, la que es azul, verde, amarilla que hace esquina, es la misma que la de abajo del todo que es azul y amarilla, un cubo de rubik no tiene dos piezas iguales 😀