Why is Android recycling the wrong view type in my SpinnerAdapter?
Asked Answered
C

2

8

I'm trying to make an ActionBar spinner that has separators. I have implemented a SpinnerAdapter that has 2 item view types (thanks to getViewTypeCount). The problem is that I'm being sent some convertViews from the other type.

Here's my SpinnerAdapter:

public abstract class SeparatorSpinnerAdapter implements SpinnerAdapter {
    Context mContext;
    List<Object> mData;
    int mSeparatorLayoutResId, mActionBarItemLayoutResId, mDropDownItemLayoutResId, mTextViewResId;

    public static class SpinnerSeparator {
        public int separatorTextResId;

        public SpinnerSeparator(final int resId) {
            separatorTextResId = resId;
        }
    }

    public abstract String getText(int position);

    public SeparatorSpinnerAdapter(final Context ctx, final List<Object> data, final int separatorLayoutResId, final int actionBarItemLayoutResId,
            final int dropDownItemLayoutResId, final int textViewResId) {
        mContext = ctx;
        mData = data;
        mSeparatorLayoutResId = separatorLayoutResId;
        mActionBarItemLayoutResId = actionBarItemLayoutResId;
        mDropDownItemLayoutResId = dropDownItemLayoutResId;
        mTextViewResId = textViewResId;
    }

    protected String getString(final int resId) {
        return mContext.getString(resId);
    }

    @Override
    public void registerDataSetObserver(final DataSetObserver observer) {
    }

    @Override
    public void unregisterDataSetObserver(final DataSetObserver observer) {
    }

    @Override
    public int getCount() {
        if (mData != null) {
            return mData.size();
        }
        return 0;
    }

    @Override
    public Object getItem(final int position) {
        return mData == null ? null : mData.get(position);
    }

    @Override
    public boolean isEmpty() {
        return getCount() == 0;
    }

    @Override
    public long getItemId(final int position) {
        return 0;
    }

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

    @Override
    public View getView(final int position, final View convertView, final ViewGroup parent) {
        return getView(mActionBarItemLayoutResId, position, convertView, parent);
    }

    public boolean isSeparator(final int position) {
        final Object item = getItem(position);
        if (item != null) {
            return item instanceof SpinnerSeparator;
        }
        return false;
    }

    @Override
    public int getItemViewType(final int position) {
        return isSeparator(position) ? 0 : 1;
    }

    @Override
    public int getViewTypeCount() {
        return 2;
    }

    @Override
    public View getDropDownView(final int position, final View convertView, final ViewGroup parent) {
        return getView(isSeparator(position) ? mSeparatorLayoutResId : mDropDownItemLayoutResId, position, convertView, parent);
    }

    private View getView(final int layoutResId, final int position, final View convertView, final ViewGroup parent) {
        View v;

        Log.i("TAG", "getView #" + position + "\tVT=" + getItemViewType(position) + "\tCV="
                + (convertView == null ? " null  " : convertView.getClass().getSimpleName()) + "\ttext=> " + getText(position));

        if (convertView == null) {
            final LayoutInflater li = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            v = li.inflate(layoutResId, parent, false);
        } else {
            v = convertView;
        }

        final TextView tv = (TextView) v.findViewById(mTextViewResId);
        if (tv != null) {
            tv.setText(getText(position));

            if (isSeparator(position)) {
                tv.setOnClickListener(null);
                tv.setOnTouchListener(null);
            }
        }

        return v;
    }
}

One implementation:

public class IssuesMainFilterAdapter extends SeparatorSpinnerAdapter {
    public IssuesMainFilterAdapter(final Context ctx, final List<Query> queries, final List<Project> projects) {
        super(ctx, buildDataArray(queries, projects), R.layout.issues_filter_spinner_separator, R.layout.issues_filter_spinner_in_actionbar,
                R.layout.issues_filter_spinner, R.id.issues_filter_spinner_text);
    }

    private static List<Object> buildDataArray(final List<Query> queries, final List<Project> projects) {
        final List<Object> data = new ArrayList<Object>();
        data.add(null); // "ALL"
        data.add(new SpinnerSeparator(R.string.issue_filter_queries));
        data.addAll(queries);
        data.add(new SpinnerSeparator(R.string.issue_filter_projects));
        data.addAll(projects);
        return data;
    }

    @Override
    public String getText(final int position) {
        final Object item = getItem(position);
        if (item == null) {
            return getString(R.string.issue_filter_all);
        } else if (item instanceof Query) {
            return ((Query) item).name;
        } else if (item instanceof Project) {
            return ((Project) item).name;
        } else if (item instanceof SpinnerSeparator) {
            return getString(((SpinnerSeparator) item).separatorTextResId);
        }
        throw new InvalidParameterException("Item has unknown type: " + item);
    }
}

As you may have noticed, I have set a log line into getView() so that I better understand what's going on:

05-06 14:01:28.721 I/TAG( 5879): getView #0 VT=1    CV=TextView text=> ####
05-06 14:01:28.721 I/TAG( 5879): getView #1 VT=0    CV=LinearLayout text=> ####
05-06 14:01:28.729 I/TAG( 5879): getView #2 VT=1    CV=TextView text=> ####
05-06 14:01:28.745 I/TAG( 5879): getView #3 VT=1    CV=TextView text=> ####
05-06 14:01:28.745 I/TAG( 5879): getView #4 VT=0    CV=LinearLayout text=> ####
05-06 14:01:28.745 I/TAG( 5879): getView #5 VT=1    CV=TextView text=> ####
05-06 14:01:28.753 I/TAG( 5879): getView #6 VT=1    CV=TextView text=> ####
05-06 14:01:28.768 I/TAG( 5879): getView #7 VT=1    CV=TextView text=> ####
05-06 14:01:28.768 I/TAG( 5879): getView #8 VT=1    CV=TextView text=> ####
05-06 14:01:28.768 I/TAG( 5879): getView #9 VT=1    CV=TextView text=> ####
05-06 14:01:28.776 I/TAG( 5879): getView #10    VT=1    CV=TextView text=> ####
05-06 14:01:28.792 I/TAG( 5879): getView #11    VT=1    CV=TextView text=> ####
05-06 14:01:32.081 I/TAG( 5879): getView #12    VT=1    CV=TextView text=> ####
05-06 14:01:34.690 I/TAG( 5879): getView #13    VT=1    CV=LinearLayout text=> ####
05-06 14:01:35.573 I/TAG( 5879): getView #14    VT=1    CV=TextView text=> ####
05-06 14:01:37.237 I/TAG( 5879): getView #15    VT=1    CV=TextView text=> ####

As you may have understood, my layouts for the real items are TextViews, and the separator layout is a LinearLayout.

As you can see, a "real" item (VT=1 in the log, see item #13) is being recycling a separator view (CV=LinearLayout). I would have thought that Android would provide a convertView of the same type, so the first separator would be recycled only if a view of the same type (i.e. another separator) would have to be created when scrolling.

Chromoplast answered 6/5, 2013 at 12:9 Comment(0)
C
9

As David found out, this is related to the Android framework. As noted here, the framework doesn't expect Spinners to have different view types.

This is the workaround I used to make my SpinnerAdapter work as I wanted:

  • store the view type in the view's tag;
  • inflate a new layout if there is no view to convert OR if the current view type differs from the view to convert from.

Here's the code of my custom getView method:

private View getView(final int layoutResId, final int position, final View convertView, final ViewGroup parent) {
    View v;

    if (convertView == null || (Integer)convertView.getTag() != getItemViewType(position)) {
        final LayoutInflater li = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        v = li.inflate(layoutResId, parent, false);
    } else {
        v = convertView;
    }

    v.setTag(Integer.valueOf(getItemViewType(position)));

    final TextView tv = (TextView) v.findViewById(mTextViewResId);
    if (tv != null) {
        tv.setText(getText(position));

        if (isSeparator(position)) {
            tv.setOnClickListener(null);
            tv.setOnTouchListener(null);
        }
    }

    return v;
}
Chromoplast answered 6/5, 2013 at 14:28 Comment(1)
+1 - This example led me to the solution. Basically, the tags are a hack around not being able to override getViewTypeCount. I would say that this answer could stand to have a bit more context thrown in; implementation of getItemViewType, the overridden getView (and perhaps the overridden getDropDownView), etc. Anyway, thanks!Grati
S
1

The problem is here:

public View getView(final int position, final View convertView, final ViewGroup parent) {
    return getView(mActionBarItemLayoutResId, position, convertView, parent);
}

This method will always return the same View type, whether called for a separator or a data item. You need to check the position here and return an appropriate view.

Strow answered 6/5, 2013 at 13:4 Comment(5)
This getView is called for the ActionBar item, not the drop-down view. Moreover, Android is calling getView and getDropDownView knowing the current item type and recycled view item type. From my understanding (and expectations), Android should keep two (or getViewTypeCount) pools of views that are recycled when the scrolling moves them out of visibility. So when calling getDropDownView, Android should pick a recyclable view of the same type, or provide null. Am I right?Chromoplast
Yes, Android should keep 2 pools of views and recycle them properly.Somehow that is messed up. It is getting confused. I see now that you've overridden getItemId() and are always returning 0. This may be confusing things. try removing that method and also removing your overridden hasStableIds() and see if that helps.Strow
Ah, found it. It's an Android bug :-( Multi-view recycling isn't supported for dropdown views in a spinner. Sorry for any inconvenience.Strow
I'd like to mention that I tried to return a unique ID in getItemId and make hasStableIds return true, it still wasn't working.Chromoplast
Yes, I was trying to find a problem that wasn't there. Sorry. I had assumed that multiple views were supported here, and that caused me to suggest some other attempts to fix it. I apologize for that.Strow

© 2022 - 2024 — McMap. All rights reserved.