Обратный вызов onItemSelected Spinner вызывается дважды после поворота, если выбрано ненулевое положение

Когда я создаю свое действие, я настраиваю Spinner, назначаю ему слушателя и начальное значение. Я знаю, что обратный вызов onItemSelected вызывается автоматически во время инициализации приложения. Что мне показалось странным, так это то, что это происходит дважды при повороте устройства, что вызывает у меня некоторые проблемы, которые мне придется как-то обойти. Этого не происходит, если начальный выбор счетчика равен нулю. Мне удалось изолировать проблему, вот простейшее действие, вызывающее ее:

public class MainActivity extends Activity implements OnItemSelectedListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.i("Test","Activity onCreate");
    setContentView(R.layout.activity_main);
    ((Spinner)findViewById(R.id.spinner1)).setSelection(2);
    ((Spinner)findViewById(R.id.spinner1)).setOnItemSelectedListener(this);
}
@Override
public void onItemSelected(AdapterView<?> spin, View selview, int pos, long selId)
{
    Log.i("Test","spin:"+spin+" sel:"+selview+" pos:"+pos+" selId:"+selId);
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {}
}

А вот логарифм, отображаемый, когда приложение запускается, а затем устройство вращается:

    I/Test( 9881): spin:android.widget.Spinner@4052f508 sel:android.widget.TextView@40530b08 pos:2 selId:2
    I/Test( 9881): Activity onCreate
    I/Test( 9881): spin:android.widget.Spinner@40535d80 sel:android.widget.TextView@40538758 pos:2 selId:2
    I/Test( 9881): spin:android.widget.Spinner@40535d80 sel:android.widget.TextView@40538758 pos:2 selId:2

Это ожидаемое поведение? Я что-то упускаю?


person athos    schedule 28.01.2013    source источник
comment
Вы нашли решение? Я все еще придерживаюсь этого ...   -  person Raphael Oliveira    schedule 04.04.2013


Ответы (9)


Удалось найти решение в другом вопросе stackoverflow:

spinner.post(new Runnable() {
    public void run() {
        spinner.setOnItemSelectedListener(listener);
    }
});
person Raphael Oliveira    schedule 04.04.2013
comment
Не могли бы вы дать ссылку на этот вопрос SOF? - person AlvaroSantisteban; 16.06.2014
comment
Конечно @AlvaroSantisteban, если я не ошибаюсь, это происходит из этого вопроса: stackoverflow.com/questions/2562248/, но у него больше года, поэтому я не больше не помню подробностей :(. - person Raphael Oliveira; 16.06.2014
comment
Спустя 3 года большое спасибо ‹3 Решил мою проблему с запуском Spinner дважды, когда я возвращаюсь от фрагмента B к фрагменту A, где Spinner лежит на фрагменте A. - person Konayuki; 09.05.2016
comment
Не делай этого! Уродливый обходной путь. Правильный и самый простой способ - использовать setSelection (#, false) перед установкой слушателя. См. stackoverflow.com/a/44979172/4966267 - person One Code Man; 03.08.2017
comment
Это решает исходный двойной вызов при установке слушателя. К сожалению, не решает двойной вызов каждый раз, когда вы меняете выбор - person Jose_GD; 29.11.2017
comment
@OneCodeMan Выполнение этого действия решает исходный вызов onItemSelected при инициализации, но затем вы все равно получаете вызов onItemSelected (вместо 2) после поворота. Установка слушателя внутри runnable устраняет обе проблемы: больше нет ложных вызовов onItemSelected. - person Flyview; 02.12.2018
comment
@Flyview, это другая ситуация. На самом деле, это не проблема Spinners. В вашем Spinner XML отсутствует параметр [android: saveEnabled = false]. Потому что, если вы сами обрабатываете состояния представления, вы должны отключить эту функцию (не только для Spinners). В противном случае система Android попытается восстановить состояние просмотра (в данном случае снова запустив onItemSelected) в событии onRestoreInstanceState (), которое обрабатывается после onCreate (). - person One Code Man; 08.01.2019
comment
@OneCodeMan хммм в новейших библиотеках и Android 10, setSelection (#, false) больше даже не работает, чтобы предотвратить его срабатывание во время инициализации или вращения. Кроме того, публикация runnable больше не работает для инициализации, хотя я был уверен, что это так, когда я сделал свой первый комментарий. Теперь я использую комбинацию логического флага для начальной инициализации и сообщения обработчика для вращения! - person Flyview; 11.02.2020

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

Создайте свой слушатель для счетчика:

public class SpinnerInteractionListener implements AdapterView.OnItemSelectedListener, View.OnTouchListener {

    boolean userSelect = false;

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        userSelect = true;
        return false;
    }

    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
        if (userSelect) { 
            // Your selection handling code here
            userSelect = false;
        }
    }

}

Добавьте слушателя в счетчик как OnItemSelectedListener и как OnTouchListener:

SpinnerInteractionListener listener = new SpinnerInteractionListener();
mSpinnerView.setOnTouchListener(listener);
mSpinnerView.setOnItemSelectedListener(listener);
person Andres Q.    schedule 11.02.2015
comment
ИМО, это довольно элегантное решение. Самостоятельный и простой для понимания. Я давно пользуюсь хаком View.post(), но рад, что пришел осмотреться на случай, если кто-то придумает что-то новое. +1 - person Tony Chan; 26.03.2016
comment
Я думаю, что это чище, чем установка прослушивателя только на следующее событие цикла, учитывая, что прослушиватель выбора должен по умолчанию отправлять только события, если пользователь сам выбрал его. Только убедитесь, что вы также установили сенсорный слушатель, я забыл на секунду. - person EpicPandaForce; 06.06.2016
comment
Это решение кажется недоступным ... пользователи, которые используют свои устройства с помощью TalkBack или другого приложения для чтения с экрана, скорее всего, не будут запускать выбор таким образом. - person Alex Lockwood; 09.07.2016

Просто используйте setSelection (#, false) перед установкой слушателя:

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    spinner.setSelection(2, false);
    spinner.setOnItemSelectedListener(this);
}

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

person One Code Man    schedule 07.07.2017
comment
Это решает исходный дублированный вызов, но не решает второй вызов каждый раз, когда вы меняете выбор в счетчике - person Jose_GD; 29.11.2017
comment
Проверьте свою логику @Jose_GD, возможно, вы снова запускаете выбор, прямо или косвенно, каким-то действием внутри вашего прослушивателя OnItemSelected. Начни проверять своего слушателя. - person One Code Man; 29.11.2017
comment
Да, спасибо. Ничего такого, что могло бы вызвать этот обратный вызов снова. Решил это с флагами окончательно. - person Jose_GD; 30.11.2017
comment
Хорошо, но обратите внимание на то, что вы делаете, потому что Spinner не запускает слушателя дважды сам по себе, и если это происходит, что-то странное в вашем коде и может вызвать другое неожиданное поведение. - person One Code Man; 05.12.2017
comment
Я предпочитаю это решение по сравнению с другими альтернативами, предложенными в качестве ответов, поскольку оно простое и не использует никаких дополнительных переменных. - person Livio; 12.02.2020

При первом запуске onItemSelected view еще не накачан. Второй раз уже надут. Решение состоит в том, чтобы обернуть методы внутри onItemSelected с помощью if (view != null).

@Override
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
    if (view != null) { 
        //do things here

    }
}
person Tanapruk Tangphianphan    schedule 11.06.2017
comment
Большое спасибо!! Это решило мой вариант использования onItemSelected (), который дважды запускался, когда я возвращался к своему фрагменту (onBackPressed) из другого фрагмента. Такое поведение прядильщика сводило меня с ума. Вы сэкономили мне часы :) - person Vinay Vissh; 01.07.2017
comment
Это ненужный обходной путь, проверьте решение stackoverflow.com/a/44979172/4966267 - person One Code Man; 29.11.2017

Вот что я сделал:

Сделайте локальную переменную

Boolean changeSpinner = true;

В saveInstanceMethod сохраните выбранную позицию элемента прядильщика

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt("ItemSelect",mySpinner.getSelectedItemPosition());
}

Затем в созданном действии получите этот int из savedInstanceState, и если int равен! = 0, установите для логической переменной значение false;

@Override
    public void onActivityCreated(Bundle savedInstanceState) {

    if (savedInstanceState!=null) {
        if (savedInstanceState.getInt("ItemSelect")!=0) {
           changeSpinner = false;
        }
    }

}

И, наконец, на OnItemSelected из счетчика сделайте это

mySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    public void onItemSelected(AdapterView<?> parent,android.view.View v, int position, long id) {
        if (changeSpinner) {
           [...]
        } else {
           changeSpinner= true;
        }
    });

Итак, первый раз, когда вызывается, ничего не будет делать, просто сделайте логическую переменную истинной, а второй раз будет выполнять код. Может быть, не лучшее решение, но оно работает.

person DejaVu    schedule 14.02.2015
comment
Похоже на хорошее решение. - person T-D; 09.10.2016

Я обновляю ответ @Andres Q. в Котлине.

Создайте внутренний класс, в котором вы используете Spinner

inner class SpinnerInteractionListener : AdapterView.OnItemSelectedListener, View.OnTouchListener {
        override fun onNothingSelected(parent: AdapterView<*>?) {

        }

        override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
            if (userSelect) {
                //Your selection handling code here
                userSelect = false
            }
        }

        @SuppressLint("ClickableViewAccessibility")
        override fun onTouch(v: View?, event: MotionEvent?): Boolean {
            userSelect = true
            return false
        }

        internal var userSelect = false
    }

Затем объявите переменную экземпляра вне onCreate() как глобально как

lateinit var spinnerInteractionListener: SpinnerInteractionListener

затем инициализируйте его внутри onCreate()

spinnerInteractionListener = SpinnerInteractionListener()

и используйте это как

spinnerCategory.onItemSelectedListener = spinnerInteractionListener
spinnerCategory.setOnTouchListener(spinnerInteractionListener)

здесь spinnerCategory - это Spinner

person Kishan Solanki    schedule 03.10.2018

Попробуй это:

boolean mConfigChange = false;

@Override
protected void onCreate(Bundle savedInstanceState) {
    // TODO Auto-generated method stub
    mConfigChange = false;
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_mainf);

    Log.i("SpinnerTest", "Activity onCreate");
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.colors,
            android.R.layout.simple_spinner_item);
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    ((Spinner) findViewById(R.id.spin)).setAdapter(adapter);

     ((Spinner) findViewById(R.id.spin)).setSelection(2);
    ((Spinner) findViewById(R.id.spin)).setOnItemSelectedListener(this);

}

@Override
protected void onResume() {
    mConfigChange = true;
    super.onResume();
}

@Override
public void onItemSelected(AdapterView<?> spin, View selview, int pos, long selId) {
    if (!mConfigChange)
        Log.i("Test", "spin:" + spin + " sel:" + selview + " pos:" + pos + " selId:" + selId);
    else
        mConfigChange = false;
}
person Nimish Choudhary    schedule 28.01.2013
comment
Спасибо, но это, похоже, не решает мою проблему, это просто делает первый обратный вызов неэффективным (но второй по-прежнему запускается после вращения). Я уже делал что-то подобное, проблема в очевидном несоответствии между количеством вызовов при создании первого действия и ротацией устройств. - person athos; 28.01.2013

Вы можете просто позвонить в setSelection, если знаете, что у вас есть список элементов и позиция, которую нужно выбрать, таким образом вы избежите двойного вызова onItemSelected.

Я написал статью о том, что я считаю лучшим подходом Как избежать двойного вызова onItemSelected в Spinners

person jamesbluecrow    schedule 02.04.2016

Я написал функцию расширения, которая пропускает все события выбора, кроме инициированных пользователем. Не забудьте переопределить defPosition, если вы используете не первую позицию счетчика по умолчанию.

fun Spinner.setFakeSelectSkipWatcher(execute: (position: Int) -> Unit, defPosition: Int = 0) {
val listener = object : AdapterView.OnItemSelectedListener {
    var previousIsNull = -1
    var notSkip = false
    override fun onItemSelected(p0: AdapterView<*>?, view: View?, position: Int, p3: Long) {
        if (notSkip) execute(position)
        else {
            if ((view != null && position == defPosition) ||
                (view == null && position == defPosition) ||
                (view != null && previousIsNull == 1 && position != defPosition)
            ) notSkip = true
        }
        previousIsNull = if (view == null) 1 else 0
    }
    override fun onNothingSelected(p0: AdapterView<*>?) {}
}
onItemSelectedListener = listener

}

person Dew Point    schedule 18.04.2021