Home » Android » Why is Android recycling the wrong view type in my SpinnerAdapter?

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

Posted by: admin June 15, 2020 Leave a comment

Questions:

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=> 

Answer:

# 05-06 14:01:28.721 I/TAG( 5879): getView #1 VT=0 CV=LinearLayout text=>

Answer:

# 05-06 14:01:28.729 I/TAG( 5879): getView #2 VT=1 CV=TextView text=>

Answer:

# 05-06 14:01:28.745 I/TAG( 5879): getView #3 VT=1 CV=TextView text=>

Answer:

# 05-06 14:01:28.745 I/TAG( 5879): getView #4 VT=0 CV=LinearLayout text=>

Answer:

# 05-06 14:01:28.745 I/TAG( 5879): getView #5 VT=1 CV=TextView text=>

Answer:

# 05-06 14:01:28.753 I/TAG( 5879): getView #6 VT=1 CV=TextView text=>

Answer:

# 05-06 14:01:28.768 I/TAG( 5879): getView #7 VT=1 CV=TextView text=>

Answer:

# 05-06 14:01:28.768 I/TAG( 5879): getView #8 VT=1 CV=TextView text=>

Answer:

# 05-06 14:01:28.768 I/TAG( 5879): getView #9 VT=1 CV=TextView text=>

Answer:

# 05-06 14:01:28.776 I/TAG( 5879): getView #10 VT=1 CV=TextView text=>

Answer:

# 05-06 14:01:28.792 I/TAG( 5879): getView #11 VT=1 CV=TextView text=>

Answer:

# 05-06 14:01:32.081 I/TAG( 5879): getView #12 VT=1 CV=TextView text=>

Answer:

# 05-06 14:01:34.690 I/TAG( 5879): getView #13 VT=1 CV=LinearLayout text=>

Answer:

# 05-06 14:01:35.573 I/TAG( 5879): getView #14 VT=1 CV=TextView text=>

Answer:

# 05-06 14:01:37.237 I/TAG( 5879): getView #15 VT=1 CV=TextView text=>

Answer:

#

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.

How to&Answers:

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;
}

Answer:

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.