Monday, August 29, 2011

Android Listview and OnItemClickListener GOTCHA


Android ListView and Custom View Gotchas


I first used the Android ListView to attach to some a list of objects and display them using the Android default/simple list view:
int layoutID = android.R.layout.simple_list_item_1;
ArrayList<Location> myList = fillLocationList();
ArrayAdapter<Location> aaBasic = new ArrayAdapter<Location>(this, layoutID, myList);

In order for it to show the string that I wanted, I had to override Location's toString, thats all fine. I also wanted to perform an action when the user selected an item in the list. This also worked well.
this.locationListView.setOnItemClickListener(new OnItemClickListener()
        {
			@Override
			public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
					long arg3)
			{
				onLocationSelected(locations.get(pos));
				
			}      	
        });  

Next I created my own ArrayAdapter and my own view. That view had multiple TextView's so that I could show more information in each list element. Inside of my ArrayAdapter, I load my custom view and set the text of each TextView to a property of my object. This also works fine and the user can still select an item and my listener gets called.
int layoutID = R.mylocationitem;
ArrayList<Location> myList = fillLocationList();
MyArrayAdapter aa = new MyArrayAdapter(this, layoutID, myList);
I'm not showing the details of MyArrayAdapter here. But it is the standard example that you see. It loads the layout and accesses the widgets inside the inflated view and sets the widget properties to match those of my object (Location). So now instead of a simple string list, I have a more complicated list.

Next, I decide that I want my custom view to contain both a TextView and a RatingBar. I want the user to see a name of a location as well as its "rating". The ratingbar is readonly so I have to disable it. (Since then, I found out that RatingBar has a property named 'isIndicator' and when this is true, the rating bar is readonly. This is better than disabling it but does not change the issue here.)

Problem 1 - my listener stops working as soon as I add the RatingBar.


I know that what is happening is that my RatingBar is forcing the view to take focus so that the user can interact with the rating bar. Thus the custom view sees the user click rather than the list adapter. But I am not able to "tip off" android that I dont want this to happen. I see that some android templates specify that a list item control should not be "Clickable" but this does not work for me. Neither does the disabling of the rating bar. I tried things like this:
view.setFocusable(false);
//			ratingBar.setFocusable(false);
//			ratingBar.setFocusableInTouchMode(false);
//			ratingBar.setClickable(false);

But none of that works. Next I give up on getting OnItemClick to work and decide to handle the click event in my item view element itself. Naively, I first try to use aOnClickListener.
			ratingBar.setOnClickListener(new OnClickListener()
			{
This was naive, reading the android documentation more and I realize it is the processing of user-touch events which lead to a click event. The user has to tap down and then up to cause a click. But the widgets are handling the touch events. So I need to handle touch events instead.
			OnTouchListener touchListener =		new OnTouchListener()
			{
				@Override
				public boolean onTouch(View arg0, MotionEvent arg1)
				{
					HomeActivity ha = (HomeActivity) getContext();
                                        ha.OnItemSelected(position);
					return true; //If I dont return true, I stop getting Touch events
				}
			
			};
                       newView.setOnTouchListener(touchListener);
This is starting to look good. Note that my listener is now on the entire view rather than any of the individual widgets. Ideally, I'd like to raise the OnItemSelected event myself but I dont know how to do that, so I just access the parent activity directly and make a call. Okay, dont yell at me yet, I know this isnt right but lets make some comments:
  • position is a final member of the adapter. I have to make it final so that I can access it in the anonymous listener class.
  • I added a method named "OnItemSelected" to my calling activity. This takes place of the OnItemSelected listener which I could not get to work.
  • NOTE: In testing, I found that if I returned false from onTouch, I would no longer get any user touch events. So I return true.
  • Okay, this code basically accomplishes the goal: now when the user taps an item in my list, the above listener gets called and my activity gets notified and it goes ahead and works correctly. This happens when the user taps an item. 'BUT' it also happens if the user drags his/her finger (e.g. when scrolling the list). OOPS!

Problem 2 - now my item select code works, but the user cannot scroll

Ooops, it seems I am not paying attention to the MotionEvent argument. Here are the applicable touch events:
  1. MotionEvent.ACTION_DOWN - the user has pressed down on the screen.
  2. MotionEvent.ACTION_UP - the user has taken their finger off the screen.
  3. MotionEvent.ACTION_MOVE - the user is dragging
  4. MotionEvent.ACTION_CANCEL - in my experience, I see CANCEL once the user drags outside of the window. At the part, the list starts to scroll and I get anACTION_CANCEL. And I get no further messages after this.
  5. MotionEvent.ACTION_OUTSIDE - I did not see this in my testing.

So, I change my logic to look for both a ACTION_DOWN and a ACTION_UP before I perform the OnItemSelected action. In testing, I found that no matter how crisply I 'tapped', I always got some intermediate ACTION_MOVE events. So I used logic to allow at most 8 of these. If the users does any more than that, then I assume they are just screwing around. HA.

Okay, so now things work well. And I'm looking for a nice way to centralize this code so that it easy for me to reuse this code. But in testing, I found one more problem.

Problem 3 - unexpected list events. I would see that often when I closed the activity which held the list, my OnItemSelect logic was being called. In my app, that meant that a new activity was launched. Spooky. I caught this in the debugger and it seemed that indeed list custom view events were being fired from a routine named "Die" (I think it is Activity.Die - no kidding). Strange, but really after my activity is no longer active, I should not listen to these events any more and this was an easy fix:

  1. In Activity.OnPause - I deactivate my event notifications (alternatively you can let them fire and just ignore them)
  2. In Activity.OnResume - I reactivate/activate my event notifications.

No comments:

Post a Comment