Как сохранить OnItemSelected от запуска только что созданного Spinner? - PullRequest
396 голосов
/ 01 апреля 2010

Я придумал несколько менее элегантных способов решить эту проблему, но я знаю, что, должно быть, что-то упустил.

Мой onItemSelected немедленно отключается без какого-либо взаимодействия с пользователем, и это нежелательное поведение. Я хочу, чтобы пользовательский интерфейс ждал, пока пользователь что-то выберет, прежде чем он что-то сделает.

Я даже пытался настроить слушателя в onResume(), надеясь, что это поможет, но это не так.

Как я могу остановить это до того, как пользователь сможет прикоснуться к элементу управления?

public class CMSHome extends Activity { 

private Spinner spinner;

@Override
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    // Heres my spinner ///////////////////////////////////////////
    spinner = (Spinner) findViewById(R.id.spinner);
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
            this, R.array.pm_list, android.R.layout.simple_spinner_item);
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    spinner.setAdapter(adapter);
    };

public void onResume() {
    super.onResume();
    spinner.setOnItemSelectedListener(new MyOnItemSelectedListener());
}

    public class MyOnItemSelectedListener implements OnItemSelectedListener {

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

     Intent i = new Intent(CMSHome.this, ListProjects.class);
     i.putExtra("bEmpID", parent.getItemAtPosition(pos).toString());
        startActivity(i);

        Toast.makeText(parent.getContext(), "The pm is " +
          parent.getItemAtPosition(pos).toString(), Toast.LENGTH_LONG).show();
    }

    public void onNothingSelected(AdapterView parent) {
      // Do nothing.
    }
}
}

Ответы [ 33 ]

361 голосов
/ 27 июня 2013

Использование Runnables совершенно некорректно.

Используйте setSelection(position, false); в начальном выборе перед setOnItemSelectedListener(listener)

Таким образом, вы устанавливаете свой выбор без анимации, что вызывает вызов прослушивателя выбранного элемента. Но слушатель нулевой, поэтому ничего не запускается. Тогда ваш слушатель назначен.

Так что следуйте этой точной последовательности:

Spinner s = (Spinner)Util.findViewById(view, R.id.sound, R.id.spinner);
s.setAdapter(adapter);
s.setSelection(position, false);
s.setOnItemSelectedListener(listener);
188 голосов
/ 21 февраля 2012

Обращаясь к ответу Дэна Дайера, попробуйте зарегистрировать OnSelectListener в методе post(Runnable):

spinner.post(new Runnable() {
    public void run() {
        spinner.setOnItemSelectedListener(listener);
    }
});

Сделав это для меня, наконец-то произошло желаемое поведение.

В этом случае это также означает, что слушатель запускает только измененный предмет.

71 голосов
/ 01 апреля 2010

Я бы ожидал, что ваше решение сработает - хотя событие выбора не сработает, если вы установите адаптер до настройки прослушивателя.

При этом простой логический флаг позволит вам обнаружить мошенническое событие первого выбора и проигнорировать его.

47 голосов
/ 23 ноября 2012

Я создал небольшую утилиту для изменения выбора Spinner без уведомления пользователя:

private void setSpinnerSelectionWithoutCallingListener(final Spinner spinner, final int selection) {
    final OnItemSelectedListener l = spinner.getOnItemSelectedListener();
    spinner.setOnItemSelectedListener(null);
    spinner.post(new Runnable() {
        @Override
        public void run() {
            spinner.setSelection(selection);
            spinner.post(new Runnable() {
                @Override
                public void run() {
                    spinner.setOnItemSelectedListener(l);
                }
            });
        }
    });
}

Это отключает слушателя, изменяет выбор и повторно включает слушателя после этого.

Хитрость в том, что вызовы асинхронны с потоком пользовательского интерфейса, поэтому вы должны делать это в последовательных сообщениях обработчика.

33 голосов
/ 15 июня 2013

К сожалению, кажется, что два наиболее часто предлагаемых решения этой проблемы, а именно подсчет повторных вызовов и публикация Runnable для установки обратного вызова позднее, могут оба потерпеть неудачу, если, например, включены опции специальных возможностей. Вот вспомогательный класс, который работает вокруг этих проблем. Дальнейшее объяснение в блоке комментариев.

import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.Spinner;
import android.widget.SpinnerAdapter;

/**
 * Spinner Helper class that works around some common issues 
 * with the stock Android Spinner
 * 
 * A Spinner will normally call it's OnItemSelectedListener
 * when you use setSelection(...) in your initialization code. 
 * This is usually unwanted behavior, and a common work-around 
 * is to use spinner.post(...) with a Runnable to assign the 
 * OnItemSelectedListener after layout.
 * 
 * If you do not call setSelection(...) manually, the callback
 * may be called with the first item in the adapter you have 
 * set. The common work-around for that is to count callbacks.
 * 
 * While these workarounds usually *seem* to work, the callback
 * may still be called repeatedly for other reasons while the 
 * selection hasn't actually changed. This will happen for 
 * example, if the user has accessibility options enabled - 
 * which is more common than you might think as several apps 
 * use this for different purposes, like detecting which 
 * notifications are active.
 * 
 * Ideally, your OnItemSelectedListener callback should be
 * coded defensively so that no problem would occur even
 * if the callback was called repeatedly with the same values
 * without any user interaction, so no workarounds are needed.
 * 
 * This class does that for you. It keeps track of the values
 * you have set with the setSelection(...) methods, and 
 * proxies the OnItemSelectedListener callback so your callback
 * only gets called if the selected item's position differs 
 * from the one you have set by code, or the first item if you
 * did not set it.
 * 
 * This also means that if the user actually clicks the item
 * that was previously selected by code (or the first item
 * if you didn't set a selection by code), the callback will 
 * not fire.
 * 
 * To implement, replace current occurrences of:
 * 
 *     Spinner spinner = 
 *         (Spinner)findViewById(R.id.xxx);
 *     
 * with:
 * 
 *     SpinnerHelper spinner = 
 *         new SpinnerHelper(findViewById(R.id.xxx))
 *         
 * SpinnerHelper proxies the (my) most used calls to Spinner
 * but not all of them. Should a method not be available, use: 
 * 
 *      spinner.getSpinner().someMethod(...)
 *
 * Or just add the proxy method yourself :)
 * 
 * (Quickly) Tested on devices from 2.3.6 through 4.2.2
 * 
 * @author Jorrit "Chainfire" Jongma
 * @license WTFPL (do whatever you want with this, nobody cares)
 */
public class SpinnerHelper implements OnItemSelectedListener {
    private final Spinner spinner;

    private int lastPosition = -1;
    private OnItemSelectedListener proxiedItemSelectedListener = null;  

    public SpinnerHelper(Object spinner) {
         this.spinner = (spinner != null) ? (Spinner)spinner : null;        
    }

    public Spinner getSpinner() {
        return spinner;
    }

    public void setSelection(int position) { 
        lastPosition = Math.max(-1, position);
        spinner.setSelection(position);     
    }

    public void setSelection(int position, boolean animate) {
        lastPosition = Math.max(-1, position);
        spinner.setSelection(position, animate);        
    }

    public void setOnItemSelectedListener(OnItemSelectedListener listener) {
        proxiedItemSelectedListener = listener;
        spinner.setOnItemSelectedListener(listener == null ? null : this);
    }   

    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        if (position != lastPosition) {
            lastPosition = position;
            if (proxiedItemSelectedListener != null) {
                proxiedItemSelectedListener.onItemSelected(
                        parent, view, position, id
                );
            }
        }
    }

    public void onNothingSelected(AdapterView<?> parent) {
        if (-1 != lastPosition) {
            lastPosition = -1;
            if (proxiedItemSelectedListener != null) {
                proxiedItemSelectedListener.onNothingSelected(
                        parent
                );
            }
        }
    }

    public void setAdapter(SpinnerAdapter adapter) {
        if (adapter.getCount() > 0) {
            lastPosition = 0;
        }
        spinner.setAdapter(adapter);
    }

    public SpinnerAdapter getAdapter() { return spinner.getAdapter(); } 
    public int getCount() { return spinner.getCount(); }    
    public Object getItemAtPosition(int position) { return spinner.getItemAtPosition(position); }   
    public long getItemIdAtPosition(int position) { return spinner.getItemIdAtPosition(position); }
    public Object getSelectedItem() { return spinner.getSelectedItem(); }
    public long getSelectedItemId() { return spinner.getSelectedItemId(); }
    public int getSelectedItemPosition() { return spinner.getSelectedItemPosition(); }
    public void setEnabled(boolean enabled) { spinner.setEnabled(enabled); }
    public boolean isEnabled() { return spinner.isEnabled(); }
}
30 голосов
/ 05 августа 2014

У меня было много проблем с вращением вертушки, когда я не хотел, и все ответы здесь ненадежны. Они работают - но только иногда. В конечном итоге вы столкнетесь со сценариями, в которых они потерпят неудачу, и внесете ошибки в ваш код.

Что мне помогло, так это сохранить последний выбранный индекс в переменной и оценить его в слушателе. Если это то же самое, что новый выбранный индекс, ничего не делать и возвращать, иначе продолжите со слушателем. Сделайте это:

//Declare a int member variable and initialize to 0 (at the top of your class)
private int mLastSpinnerPosition = 0;

//then evaluate it in your listener
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {

  if(mLastSpinnerPosition == i){
        return; //do nothing
  }

  mLastSpinnerPosition = i;
  //do the rest of your code now

}

Поверьте мне, когда я говорю это, это, безусловно, самое надежное решение. Взломать, но это работает!

25 голосов
/ 07 декабря 2012

Я был в похожей ситуации, и у меня есть простое решение, работающее на меня.

Похоже, что методы setSelection(int position) и setSelected(int position, boolean animate) имеют различную внутреннюю реализацию.

Когда вы используете второй метод setSelected(int position, boolean animate) с ложным флагом анимации, вы получаете выбор без запуска onItemSelected listener.

20 голосов
/ 06 февраля 2016

Просто чтобы прояснить намеки на использование onTouchListener, чтобы различать автоматические вызовы setOnItemSelectedListener (которые являются частью инициализации Activity и т. Д.) И вызовы к нему, инициированные фактическим взаимодействием с пользователем, я сделал следующее после попытки некоторых других предложения и обнаружили, что он хорошо работает с наименьшим количеством строк кода.

Просто установите логическое поле для вашей активности / фрагмента, например:

private Boolean spinnerTouched = false;

Затем, перед тем как установить setOnItemSelectedListener вашего счетчика, установите onTouchListener:

    spinner.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            System.out.println("Real touch felt.");
            spinnerTouched = true;
            return false;
        }
    });

    spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    ...
         if (spinnerTouched){
         //Do the stuff you only want triggered by real user interaction.
        }
        spinnerTouched = false;
13 голосов
/ 22 июня 2014
spinner.setSelection(Adapter.NO_SELECTION, false);
8 голосов
/ 31 мая 2015

После того, как я долго выдергивал свои волосы, я создал свой собственный класс Spinner. Я добавил метод, который отключает и подключает слушателя соответствующим образом.

public class SaneSpinner extends Spinner {
    public SaneSpinner(Context context) {
        super(context);
    }

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

    public SaneSpinner(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    // set the ceaseFireOnItemClickEvent argument to true to avoid firing an event
    public void setSelection(int position, boolean animate, boolean ceaseFireOnItemClickEvent) {
        OnItemSelectedListener l = getOnItemSelectedListener();
        if (ceaseFireOnItemClickEvent) {
            setOnItemSelectedListener(null);
        }

        super.setSelection(position, animate);

        if (ceaseFireOnItemClickEvent) {
            setOnItemSelectedListener(l);
        }
    }
}

Используйте его в своем XML, как это:

<my.package.name.SaneSpinner
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/mySaneSpinner"
    android:entries="@array/supportedCurrenciesFullName"
    android:layout_weight="2" />

Все, что вам нужно сделать, это извлечь экземпляр SaneSpinner после инфляции и выбрать набор вызовов следующим образом:

mMySaneSpinner.setSelection(1, true, true);

При этом событие не запускается, и взаимодействие с пользователем не прерывается. Это значительно уменьшило сложность моего кода. Это должно быть в наличии Android, так как это действительно PITA.

...