Обратно извикване 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) {}
}

И ето logcat, показан, когато приложението се стартира и след това устройството се завърти:

    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. Липсва ви параметърът [android:saveEnabled=false] във вашия Spinner XML. Защото, ако управлявате сами състоянията на изгледа, трябва да деактивирате тази функция (не само за 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
IMO това е доста елегантно решение. Самостоятелен и лесен за разбиране. Използвам хака 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) от друг фрагмент. Това поведение на spinner ме подлудяваше. Спести ми часове :) - 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. в Kotlin.

Създайте вътрешен клас, в който използвате 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