56

I have a OnItemSelectedListener for my Spinner, but it is not called when the selected item is the same as the previous one. Apparently the OnClickListener is not an option for a Spinner. I need to catch everytime a user click on an item. Any idea?

Maybe the fact that this Spinner is inside the ActionBar disturbs normal behavior?

@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    inflater.inflate(R.menu.tracklist_menu, menu);
    Spinner spinner = (Spinner) menu.findItem(R.id.option_ordering_spinner)
            .getActionView();
    spinner.setAdapter(mSpinnerAdapter);
    spinner.setSelection(PrefsHelper.getOrderingSpinnerPos(prefs));
    spinner.setOnItemSelectedListener(new OnItemSelectedListener() {

        @Override
        public void onItemSelected(AdapterView<?> parent, View view,
                int position, long id) {
            String str = "selected";
            System.out.println(str);
            if (optionMenuInitialized) {

                switch (position) {
                case 0:
                    // rdm
                    getActivity()
                            .sendBroadcast(
                                    new Intent(
                                            MyIntentAction.DO_RESHUFFLE_PLAYLIST));
                    smp.setCurrentTracklistCursorPos(-1);
                    trackAdapter.notifyDataSetChanged();
                    break;
                case 1:
                    // artist
                    getActivity()
                            .sendBroadcast(
                                    new Intent(
                                            MyIntentAction.DO_ORDER_PLAYLIST_BY_ARTIST));
                    smp.setCurrentTracklistCursorPos(-1);
                    trackAdapter.notifyDataSetChanged();
                    break;
                case 2:
                    // folder
                    getActivity()
                            .sendBroadcast(
                                    new Intent(
                                            MyIntentAction.DO_ORDER_PLAYLIST_BY_FOLDER));
                    smp.setCurrentTracklistCursorPos(-1);
                    trackAdapter.notifyDataSetChanged();
                    break;
                }
                PrefsHelper.setOrderingSpinnerPos(prefEditor, position);
                prefEditor.commit();
            }
            optionMenuInitialized = true;
        }

        @Override
        public void onNothingSelected(AdapterView<?> parent) {
        }
    });
}
biegleux
  • 13,179
  • 11
  • 45
  • 52
elgui
  • 3,303
  • 4
  • 28
  • 37

11 Answers11

90

Ok, I finally found a solution, by creating my own class extending Spinner :

public class MySpinner extends Spinner {
OnItemSelectedListener listener;

public MySpinner(Context context, AttributeSet attrs) {
    super(context, attrs);
}

@Override
public void setSelection(int position) {
    super.setSelection(position);
    if (listener != null)
        listener.onItemSelected(null, null, position, 0);
}

public void setOnItemSelectedEvenIfUnchangedListener(
        OnItemSelectedListener listener) {
    this.listener = listener;
}
}
elgui
  • 3,303
  • 4
  • 28
  • 37
26

I found out this work instead of the one provided

/** Spinner extension that calls onItemSelected even when the selection is the same as its previous value */
public class NDSpinner extends Spinner {

  public NDSpinner(Context context)
  { super(context); }

  public NDSpinner(Context context, AttributeSet attrs)
  { super(context, attrs); }

  public NDSpinner(Context context, AttributeSet attrs, int defStyle)
  { super(context, attrs, defStyle); }

  @Override public void
  setSelection(int position, boolean animate)
  {
    boolean sameSelected = position == getSelectedItemPosition();
    super.setSelection(position, animate);
    if (sameSelected) {
      // Spinner does not call the OnItemSelectedListener if the same item is selected, so do it manually now
      getOnItemSelectedListener().onItemSelected(this, getSelectedView(), position, getSelectedItemId());
    }
  }

  @Override public void
  setSelection(int position)
  {
    boolean sameSelected = position == getSelectedItemPosition();
    super.setSelection(position);
    if (sameSelected) {
      // Spinner does not call the OnItemSelectedListener if the same item is selected, so do it manually now
      getOnItemSelectedListener().onItemSelected(this, getSelectedView(), position, getSelectedItemId());
    }
  }
}
6

To make your spinner change despite the value of the last index selected just use a:

spinner.setSelection(0); 

before your other selection is called

spinner.setSelection(number); 

this way, the spinner will trigger two times the OnItemSelected event. Just make sure the second time it does whatever you need.

Sᴀᴍ Onᴇᴌᴀ
  • 8,218
  • 8
  • 36
  • 58
Ramiro G.M.
  • 357
  • 4
  • 7
6

Rewrote the common solution but with:

  1. androidx in mind
  2. extended from AppCompatSpinner
  3. use built-in OnItemSelectedListener listener instead of creating own one
  4. added initial listener call hack

Here:

import android.content.Context;

import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatSpinner;


public class FixedSpinner extends AppCompatSpinner {
    // add other constructors that you need
    public FixedSpinner(Context context, int mode) {
        super(context, mode);
    }

    private void processSelection(int position) {
        boolean sameSelected = position == getSelectedItemPosition();
        final OnItemSelectedListener listener = getOnItemSelectedListener();
        if (sameSelected && listener != null) {
            // Spinner does not call the OnItemSelectedListener if the same item is selected, so do it manually now
            listener.onItemSelected(this, getSelectedView(), position, getSelectedItemId());
        }
    }

    @Override
    public void setSelection(int position) {
        processSelection(position);
        super.setSelection(position);
    }

    @Override
    public void setSelection(int position, boolean animate) {
        processSelection(position);
        super.setSelection(position, animate);
    }

    @Override
    public void setOnItemSelectedListener(@Nullable OnItemSelectedListener listener) {
        // This hack fixes bug, when immediately after initialization, listener is called
        // To make this fix work, first add data, only then set listener
        // Having done this, you may refresh adapter data as many times as you want
        setSelection(0, false);
        super.setOnItemSelectedListener(listener);
    }
}
soshial
  • 5,906
  • 6
  • 32
  • 40
5

Here a little better implementation:

public class SpinnerPlus extends Spinner {
    AdapterView.OnItemSelectedListener listener;

    public SpinnerPlus(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void setSelection(int position) {
        super.setSelection(position);
        if (listener != null)
            listener.onItemSelected(this, getSelectedView(), position, 0);
    }

    public void setOnItemSelectedEvenIfUnchangedListener(
            AdapterView.OnItemSelectedListener listener) {
        this.listener = listener;
    }
}
Alecs
  • 2,900
  • 1
  • 22
  • 25
5

Here is a better implementation -

Custom Spinner Class -

import android.content.Context;
import android.util.AttributeSet;
import androidx.appcompat.widget.AppCompatSpinner;

public class CSpinner extends AppCompatSpinner {

    private int lastPosition = 0;

    public CSpinner(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void setSelection(int position) {
        super.setSelection(position);
        boolean sameSelected = lastPosition == getSelectedItemPosition();
        OnItemSelectedListener onItemSelectedListener = getOnItemSelectedListener();
        if (sameSelected && onItemSelectedListener != null) {
            onItemSelectedListener.onItemSelected(this, getSelectedView(), position, getSelectedItemId());
        }
        lastPosition = position;
    }
}

Setting Listener -

spn.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
            Log.d("onItemSelected", String.valueOf(position));
        }

        @Override
        public void onNothingSelected(AdapterView<?> parent) {
            
        }
    });
Vishal Yadav
  • 3,642
  • 3
  • 25
  • 42
2

If it's still actual, correct call of the callback should be

@Override
public void setSelection(int position) {
    super.setSelection(position);
    if(listener != null)
        listener.onItemSelected(this, getChildAt(position), position, 0);
}
Vishal Yadav
  • 3,642
  • 3
  • 25
  • 42
Martin Edlman
  • 665
  • 7
  • 15
  • This is not the correct callback. First, you should use `getItemIdAtPosition(position)` as the last param, at second `getChildAt(position)` should not be used in this case. `Spinner`s use `View` recycling just like `ListView`s, so `getChildAt` cannot be used the retrive the position'th item from the adapter. – WonderCsabo Mar 11 '14 at 18:51
  • You're right about the last param to get correct ID. But to get view it's fine to use getChildAt() as we either get a correct view if it exists or null if it doesn't. In most cases the code gets called when an item is clicked and thus visible in a spinner, so getChildAt() returns correct view. At least I've never encountered a situation where wrong view was returned. – Martin Edlman Mar 26 '14 at 19:31
  • 1
    No, you are not correct. For example, the spinner has 100 items, the last is clicked. But there are only 10 visible. In this case, `getChildAt()` will always return null with 99 position becuase it does not have 100 child `Views`. You cannot rely on `getChildAt()` with any `AdapterView`s. You could call `onItemSelected(this, getSelectedView(), position, getItemIdAtPosition(position));`. – WonderCsabo Mar 27 '14 at 17:05
  • Maybe I'm mistaken, but do you want to say that getChildAt() returns only views for index 0-9 (for 10 visible items)? I didn't try but it sounds weird. I would expect it should return view from the underlaying adapter for all items in adapter - more specifically it should return view if it's visible or null when it's not. I'll try when I have some spare time which I don't have right now. Spinner with 100 items (index 0-99) will return null when I click 100th item (pos 99)? I don't understand. When I click the last item, then it must be visible and getChildAt(99) must return it. Or not? – Martin Edlman Mar 28 '14 at 18:44
  • Are you aware of [view recycling](http://lucasr.org/2012/04/05/performance-tips-for-androids-listview/)? There are no `View`s for every item, only some `View`s are added which are necessary. That's why `getChild(99)` will return null. 10 and 99 were only examples, clearly it depends on the current scenario. – WonderCsabo Mar 28 '14 at 22:20
1

Simplest solution :

spinner.performItemClick(view,position,id)

Nishant Shah
  • 2,022
  • 22
  • 20
0

I had same problem, I solved it by setting onItemSelectedListener every time adapter changes items.

Marek Kondracki
  • 1,372
  • 2
  • 8
  • 13
0

Further from the answer from @Vishal Yadav, if you want to set an initial position without triggering the OnItemSelectedListener by calling spinner.setSelection(pos, false); then the custom spinner should be:

public class CSpinner extends AppCompatSpinner {

    private int lastPosition = 0;

    public CSpinner(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void setSelection(int position, boolean animate) {
        OnItemSelectedListener listener = getOnItemSelectedListener();
        setOnItemSelectedListener(null);
        super.setSelection(position, animate);
        lastPosition = position;
        setOnItemSelectedListener(listener);
    }

    @Override
    public void setSelection(int position) {
        super.setSelection(position);
        boolean sameSelected = lastPosition == getSelectedItemPosition();
        OnItemSelectedListener onItemSelectedListener = getOnItemSelectedListener();
        if (sameSelected && onItemSelectedListener != null) {
            onItemSelectedListener.onItemSelected(this, getSelectedView(), position, getSelectedItemId());
        }
        lastPosition = position;
    }
}
LXJ
  • 1,180
  • 6
  • 18
-1

i found a simple solution

just call setAdapter again in place of notifyDataSetChanged for the second spinner