Why does getView return wrong convertView objects on SeparatedListAdapter?
Asked Answered
S

1

15

I adapted the SeparatedListAdapter from Jeff Sharkey to my needs and got something like this:

public class SeparatedListAdapter<T> extends BaseAdapter {

    @SuppressWarnings("unused")
    private final String LOG_TAG = getClass().getSimpleName();

    public final static int TYPE_SECTION_HEADER = 0;

    public final Map<T, Adapter> sectionAdapters;
    public final ArrayAdapter<T> headerAdapter;

    public SeparatedListAdapter(ArrayAdapter<T> headerAdapter) {
        super();
        this.sectionAdapters = new LinkedHashMap<T,Adapter>();
        this.headerAdapter = headerAdapter;
    }

    public void addSection(T section, Adapter adapter) {
        this.headerAdapter.add(section);
        this.sectionAdapters.put(section, adapter);
    }

    public void clearSections() {
        this.headerAdapter.clear();
        this.sectionAdapters.clear();
    }

    @Override
    public int getCount() {
        // total together all sections, plus one for each section header
        int total = 0;
        for(Adapter adapter : this.sectionAdapters.values())
            total += adapter.getCount() + 1;
        return total;
    }

    @Override
    public int getViewTypeCount() {
        //count headers, then total all sections
        int total = this.headerAdapter.getViewTypeCount();
        for(Adapter adapter : this.sectionAdapters.values())
            total += adapter.getViewTypeCount();
        return total;
    }

    @Override
    public int getItemViewType(int position) {
        int type = 1;
        for(Object section : this.sectionAdapters.keySet()) {
            Adapter adapter = sectionAdapters.get(section);
            int size = adapter.getCount() + 1;

            // check if position inside this section 
            if(position == 0) return TYPE_SECTION_HEADER;
            if(position < size) return type + adapter.getItemViewType(position - 1);

            // otherwise jump into next section
            position -= size;
            type += adapter.getViewTypeCount();

        }
        return -1;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        int sectionnum = 0;
        for(Object section : this.sectionAdapters.keySet()) {
            Adapter adapter = sectionAdapters.get(section);
            int size = adapter.getCount() + 1;

            // check if position inside this section 
            if(position == 0) 
                return headerAdapter.getView(sectionnum, convertView, parent);
            if(position < size) 
                return adapter.getView(position - 1, convertView, parent);

            // otherwise jump into next section
            position -= size;
            sectionnum++;
        }
        return null;
    }

    @Override
    public boolean areAllItemsEnabled() {
        return false;
    }

    @Override
    public boolean isEnabled(int position) {
        return (getItemViewType(position) != TYPE_SECTION_HEADER);
    }
}

The problem I encountered is that the API feeds onvertView objects from the wrong adapter to getView(), leading to problems. I think I correctly implemented getViewTypeCount() and getItemViewType() and checked that with the debugger. What else could go wrong?

Here is one of my adapters:

public static class MeetingAdapter extends ArrayAdapter<Meeting> {
    static class ViewHolder {
        TextView title;
    }
    private LayoutInflater inflater;

    public MeetingAdapter(Activity context, List<Meeting> meetings) {
        super(context, 0, meetings);
        this.inflater = LayoutInflater.from(context);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Meeting meeting = getItem(position);

        View rowView = convertView;
        ViewHolder holder;

        // instanceof should not be necessary!!! normally convertView should be of the right type!
        if (rowView == null || !(rowView.getTag() instanceof ViewHolder)) {
            rowView = inflater.inflate(R.layout.list_item, null);
            holder = new ViewHolder();
            holder.title = (TextView)rowView; // tmp - layout contains only a textview for the moment
            rowView.setTag(holder);
        } else {
            holder = (ViewHolder)rowView.getTag();
        }
        holder.title.setText(meeting.getTrack());
        return rowView;
    }
}

Help please? Getting crazy over this.

Skied answered 18/8, 2012 at 13:33 Comment(3)
I'm having the same problem, Header views are being fed to the adapters and vice-versa :/ Especially after updating the data and calling notifyDatasetChangedCob
Good workaround, thanks.Shelah
I'm surprised this doesn't have more upvotes.Hamsun
C
19

I traced the problem to be the listviews's RecybleBin object. It stores the possible convertViews based on their reported types from getItemViewType, however its contents are never updated after you change the dataset (thus positions), then when your adapter gets what used to be a type "X" view based on its position, but now it happens to be a type "Y". A solution is to manually check on getView wether not only the convertView is null, but wether it is of the correct type (that's really awkward sometimes... If you use the view holder pattern you could check if the tag matches your corresponding viewholder class)

Cob answered 5/9, 2012 at 19:1 Comment(3)
Oh I didn't see this reply as I moved on and just used the workaround (same as you proposed, see my code). Glad to hear I am not the only one!Skied
I was soon going to go crazy over this. Thank you :)Hamsun
The recycle bin technically stores the convert views based on the type that was initially read from getItemViewType but is then saved to the view's LayoutParams object. If you overwrite the view's LayoutParams you will overwrite the view type and thus break the recycling!Scurry

© 2022 - 2024 — McMap. All rights reserved.