Saturday, June 15, 2013

Efficient Custom ListView Adapter Selections

A customer requested I create a ListView of their business objects using custom graphics and text based on the status of the object.  I had no problem creating it but they complained that it took too long to refresh when making selections.  After puzzling over the code for awhile trying to determine how to speed it up, I realized that the convertView paramter passed in to the GetView method that I was overriding in my custom listview adapter was actually the existing view of the object, and if it wasn't null I didn't have to recreate it.

So now in my code I check first and I only create the view from scratch if it's null.  Otherwise, I only modify the existing view to change the background color to signify that it has been selected.  I also store the two Drawables that are the backgrounds so they don't have to be retrieved and assigned memory for each object.  This allows the ListView to refresh quickly as it doesn't have to reacquire the underlying objects and rebuild the entire view based on the object's properties each time the selection is changed.  This seems like a simple thing, but maybe someone else that is struggling with ListView performance will happen across my post.

namespace Droid
{
 public class ListViewObjectAdapter : BaseAdapter<MyObject> {
  List<MyObject> items;
  public int selectedListItem = -1;
  Activity context;
  Drawable bgDefault;
  Drawable bgHighlight;

  public ListViewobjectAdapter(Activity context, List<MyObject> items)
   : base()
  {
   this.context = context;
   this.items = items;
   Drawable bgDefault = context.Resources.GetDrawable(Resource.Drawable.gray_button_focused);
   Drawable bgHighlight = context.Resources.GetDrawable(Resource.Drawable.gray_button_default);
  }

  public override long GetItemId(int position)
  {
   return position;
  }

  public void SetItems(List<MyObject> items)
  {
   this.items = items;
   this.NotifyDataSetInvalidated ();
  }

  public override MyObject this[int position]
  {
   get { return items[position]; }
  }

  public override int Count
  {
   get { return items.Count; }
  }

  public override View GetView(int position, View convertView, ViewGroup parent)
  {
   View view = convertView;

   if (view == null)
   {
    string SENT_PREFIX = context.Resources.GetString(Resource.String.sent_object_prefix);
    string UNSENT_PREFIX = context.Resources.GetString(Resource.String.unsent_object_prefix);

    MyObject item = items[position];
    Subobject subobject = Helper.GetSubobjectById (item.SubobjectId);

    view = context.LayoutInflater.Inflate (Resource.Layout.ListViewObjectItem, null);
    view.FindViewById<TextView> (Resource.Id.tvTitle).Text = Helper.GetInfoBySubOject (SubObject);

    if (item.Date.Date.Equals (DateTime.Now.Date))
     view.FindViewById<TextView> (Resource.Id.tvobjectId).Text = item.Date.ToShortTimeString ();
    else
     view.FindViewById<TextView> (Resource.Id.tvobjectId).Text = item.Date.ToShortDateString ();

    view.FindViewById<TextView> (Resource.Id.tvobjectIdText).Text = "";

    if (item.IsValid) {
     view.FindViewById<ImageView> (Resource.Id.Image2).SetImageResource (Resource.Drawable.icon_accept);
     view.FindViewById<TextView> (Resource.Id.tvDescription).Text = "Extra Info: " + Helper.GetAdditionalInfoByObjectId(item.MyObjectId);
    } else {
     view.FindViewById<ImageView> (Resource.Id.Image2).SetImageResource (Resource.Drawable.icon_reject);
     ValidSubobject SubobjectMeasure = Helper.GetValidSubObjectByObjectId (item.MyObjectId);
     view.FindViewById<TextView> (Resource.Id.tvDescription).Text = "MyInfo: " + SubobjectMeasure.Info.ToString () + ";
    }
   }
   RelativeLayout rlListViewItem = (RelativeLayout)view.FindViewById (Resource.Id.rlListViewItem);
   if(position == selectedListItem) {  
    rlListViewItem.SetBackgroundDrawable(bgHighlight);
   } else {
    rlListViewItem.SetBackgroundDrawable(bgDefault );
   }

   return view;
  }
 }
}