6

I'm trying to customize a Spinner to be multi-selectable. I have successfully made it multi-selectable, but when an item is reselected, the view inside of the spinner does not get updated.

The Problem

When an item is reselected in the Spinner, the getView() of my SpinnerAdapter gets called, produces the View that I want it to, but that View somehow does not get displayed. I have stepped through everything in the debugger and I cannot find any differences between when a different item is selected or the same item is reselected. I have been through all of the code in Spinner AbsSpinner and AdapterView and I cannot find what may be causing this.

For Example

The spinner is populated with 6 items: Choice 1-6. The spinner originates with all items unselected. Choice 1, 2, and 4 are selected. The view inside the spinner displays "Choice 1, Choice 2, Choice 4" correctly.

Choice 4 is unselected (reselection as far as the Spinner is concerned). Choice 4 is correctly unchecked from the list, but the view inside the spinner does not update. It still displays "Choice 1, Choice 2, Choice 4". Stepping through the debugger, getView() on my SpinnerAdapter gets called and everything. For some reason, the View doesn't actually get displayed.

Choice 2 is unselected (this is not a "re-selection" to the Spinner). Here everything functions as expected. Both the list items and the view inside the Spinner are updated.

Some Code

The multi-selectable Spinner

public class MultiSelectSpinner extends Spinner {

    private OnItemSelectedListener listener;

    public MultiSelectSpinner(Context context) {

        super( context );
    }

    public MultiSelectSpinner(Context context, AttributeSet attrs) {

        super( context, attrs );
    }

    public MultiSelectSpinner(Context context, AttributeSet attrs, int defStyle) {

        super( context, attrs, defStyle );
    }

    @Override
    public void setSelection( int position ) {

        super.setSelection( position );
        if ( listener != null ) {

            listener.onItemSelected( this, getSelectedView(), position, getAdapter().getItemId( position ) );
        }
    }

    public void setOnItemSelectedEvenIfUnchangedListener( OnItemSelectedListener listener ) {

        this.listener = listener;
    }
}

Spinner Adapter

Only posting the important pieces here. Leaving out getters/setters, etc.

public class FormChoiceSpinnerAdapter implements SpinnerAdapter, OnItemSelectedListener {

    private Choice[] choices;
    private String title;
    private final DataSetObservable dataSetObservable = new DataSetObservable();

    public FormChoiceSpinnerAdapter(String[] choices, String title) {

        setChoices( new Choice[choices.length] );
        for (int i = 0; i < choices.length; i++) {

            getChoices()[i] = new Choice( choices[i] );
        }
    }

    @Override
    public View getView( int position, View convertView, ViewGroup parent ) {

        Context context = parent.getContext();
        if ( convertView == null ) {

            convertView = LayoutInflater.from( context ).inflate( android.R.layout.simple_spinner_item, parent, false );
        }

        String displayString = "";

        for (int i = 0; i < getChoices().length; i++) {

            Choice choice = getChoices()[i];

            if ( choice.isSelected() ) {

                displayString += choice.getLabel() + ", ";
            }
        }

        if ( displayString.length() > 0 ) {

            displayString = displayString.trim().substring( 0, displayString.length() - 2 );
        }
        else {

            displayString = getTitle() + "...";
        }

        ( (TextView) convertView ).setText( displayString );

        return convertView;
    }

    @Override
    public View getDropDownView( int position, View convertView, ViewGroup parent ) {

        Context context = parent.getContext();
        if ( convertView == null ) {

            convertView = LayoutInflater.from( context ).inflate( R.layout.simple_dropdown_item, parent, false );
        }

        Choice choice = getChoices()[position];
        TextView text = (TextView) convertView.findViewById( android.R.id.text1 );
        text.setText( choice.getLabel() );

        ImageView selectedImage = (ImageView) convertView.findViewById( R.id.image_selected );
        int visibility = choice.isSelected() ? View.VISIBLE : View.GONE;
        selectedImage.setVisibility( visibility );

        return convertView;
    }

    @Override
    public void onItemSelected( AdapterView<?> parent, View view, int position, long id ) {

        Choice choice = getChoices()[position];

        choice.setSelected( !choice.isSelected() );

        ImageView imageView = (ImageView) view.findViewById( R.id.image_selected );

        if ( imageView != null ) {
            imageView.setVisibility( choice.isSelected() ? View.VISIBLE : View.GONE );
        }    
    }

    @Override
    public void onNothingSelected( AdapterView<?> parent ) {

        //No op
    }

    public static class Choice {

        private boolean selected;
        private String label;

        public Choice(String label) {

            this.label = label;
            selected = false;
        }
    }
}
James McCracken
  • 15,488
  • 5
  • 54
  • 62
  • Hello mate! I am trying to achieve almost the same result by using `Spinner` but no luck at the moment. Could you post whole code of this element and share the knowledge :) ? – JakubW Jul 09 '15 at 09:42
  • Unfortunately, I don't access to this code anymore. I never did find a solution so it may be worth your time to just copy/modify `Spinner` to do what you want. – James McCracken Jul 09 '15 at 15:22

2 Answers2

1

I know this is very late, but I found a solution for a similar problem I was having. Try adding adapter.notifyDataSetChanged() to your onItemSelected(AdapterView<?> parent, View view, int pos, long id) Override method.

My spinner is supposed to show a default message until the user actually selects a value from it. It was working fine unless to user selected the first item in the dropdown, then the spinner wouldn't update itself. Adding adapter.notifyDataSetChanged() worked.

I also looked through some of the source code and I couldn't find exactly what was happening, but I have an idea. Since the Spinner doesn't call the OnItemSelectedListener when selecting the value that is already selected (unless you extend Spinner, which I did with the help of this SO answer), my guess is that if you try to deselect your most recently selected value, Spinner doesn't recognize that as a deselection, and therefore won't redraw the view. For you, selecting 1, 2, 4 in that order, then trying to deselect 4 wouldn't trigger the redraw response, but then deselecting 2 would cause the redraw.

I could be wrong, but I know that our problems were similar and this worked for me.

Community
  • 1
  • 1
peteross
  • 236
  • 3
  • 12
0

You need to update the View from the UI thread. See this answer from another question that is similar to yours which details updating the UI thread using an AsyncTask:

https://stackoverflow.com/a/4370785/793150

I had the same issue with a view not updating with fragments, and I believe that this solution is applicable to this issue as well.

I hope this resolves your issue!

Community
  • 1
  • 1
alice.harrison
  • 80
  • 1
  • 12
  • I don't believe it is. Like I said, I had the same issue with updating my fragments. If you have a method for updating the view, you need to call the UI thread from an AsyncTask. This separates the update from the main ui thread and updates it independently. That way instead of waiting for android to recycle and rebuild this view, you're proactively updating it yourself. That being said, I am not infallible. It IS possible I am wrong. – alice.harrison Jun 25 '13 at 00:58