如何防止在新实例化的Spinner上触发onItemSelected事件?

441

我曾经想到了一些不太优雅的方法来解决这个问题,但我知道我一定错过了什么。

我的onItemSelected会在用户与控件进行任何交互之前立即触发,这是不希望发生的行为。 我希望UI在用户选择某些内容之前等待,并且在此之后才执行相应操作。

我甚至尝试在onResume()中设置监听器,希望这样做有所帮助,但没有效果。

如何阻止在用户触摸控件之前就触发 onItemSelected?

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.
    }
}
}

3
你可以看一下这个解决方案,它简单实用。https://dev59.com/R2435IYBdhLWcg3w7k6b#10102356 - Günay Gültekin
1
一个简单的解决方案是将Spinner中的第一项设为空,在onItemSelected方法中检测字符串是否为空,如果不为空,则可以启动活动! - Muhammad Babar
这个模式可以正常工作:https://dev59.com/p2Yr5IYBdhLWcg3wxNC8#44715988 - saksham
33个回答

399

使用Runnables是完全不正确的。

setOnItemSelectedListener(listener)之前,在初始选择中使用setSelection(position, false);

这样可以设置你的选择,没有动画会导致该项被选中监听器被调用。但是监听器为空,所以不运行任何内容。然后分配监听器。

因此,请按照以下确切顺序进行操作:

Spinner s = (Spinner)Util.findViewById(view, R.id.sound, R.id.spinner);
s.setAdapter(adapter);
s.setSelection(position, false);
s.setOnItemSelectedListener(listener);

51
+1 隐藏的宝石!将"animate"参数传入false不会调用监听器回调函数。太棒了! - pkk
3
+1 奇怪但优雅的解决方案 :) 幸运的是,我已经不得不调用setSelection... - Martin T.
37
当Spinner UI元素被组装时,监听器仍将触发,因此它将无论何时都会触发,这并不能防止OP所描述的不需要的行为。如果不是在onCreateView()期间或之前声明,那么这个方法效果很好,但这不是他们要求的。 - Rudi Kershaw
6
有用,但解决的问题与 OP 提出的不同。OP 指的是选择事件在视图首次出现时自动触发(尽管程序员没有进行 setSelection 操作)。 - ToolmakerSteve
2
setSelection(..) 方法中的 "false" 值参数对我来说是解决方案。谢谢! - Dani
显示剩余19条评论

208

参考 Dan Dyer 的回答,尝试在 post(Runnable) 方法中注册 OnSelectListener

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

通过这样做,我终于实现了所期望的行为。

在这种情况下,这也意味着监听器仅在更改的项上触发。


1
我收到一个错误,说:“AdapterView<SpinnerAdapter>类型中的setOnItemSelectedListener(AdapterView.OnItemSelectedListener)方法不适用于参数(new Runnable(){})。为什么会这样呢? - Jakob Harteg
6
@theFunkyEngineer - 这段代码应该在主线程方法中运行,例如 onCreate()onResume() 等。在这种情况下,这是一个非常好的技巧,没有竞态条件的危险。我通常会在布局代码之后的 onCreate() 中使用这个技巧。 - Richard Le Mesurier
1
这是一个很棒的解决方案,绝对不是黑客行为!这种功能通常是在框架深处实现的。很遗憾Spinner内部没有做到这一点。然而,这是最干净的方式来确保在Activity创建后运行某些代码。这能够工作是因为当Activity尝试通知它们时,监听器还没有被设置在Spinner上。 - jophde
小提示:如果您在ListView(RecyclerView)中使用Spinner,请不要忘记在重置ViewHolder中的适配器之前调用spinner.setOnItemSelectedListener(null),以避免调用“旧”侦听器。 - Patrick Dorn
1
这是一个可接受的解决方案,而不是盲目尝试。其他解决方案更容易在未来出现行为变化问题。 - Kuldeep Dhaka
显示剩余3条评论

82

我本以为您的解决方案会起作用——我认为如果在设置监听器之前设置适配器,选择事件将不会触发。

话虽如此,一个简单的布尔值标志将允许您检测到流氓的第一个选择事件并忽略它。


15
哎,是的。这就是我所说的不太优雅的解决方案。似乎应该有更好的方法。不过还是谢谢你。 - FauxReal
5
这个 Dev 邮件列表的帖子(http://groups.google.com/group/android-developers/browse_thread/thread/d93ce1ef583a2a29)提供了更多关于这个问题的见解,很遗憾没有给出解决方案... - BoD
25
布局组件的过程会触发选择监听器,因此您需要在布局完成后才添加该监听器。由于布局似乎是在 onResume()onPostResume() 之后某个时间点发生的,因此我无法找到一个适当、简单的地方来执行此操作,因为所有正常的钩子都在布局发生时已经完成了。 - Dan Dyer
28
我建议避免使用这个布尔标记,因为如果行为发生改变,它可能会导致错误。更加可靠的解决方案是保留一个变量来存储“当前选定的索引”,并将其初始化为所选的第一项。然后在选择事件上检查它是否等于新位置,如果是,则返回并不执行任何操作。当然,在选择时要更新变量。 - daniel.gindi
2
这个不起作用。@casanova的答案有效。那应该是被接受的答案。 - Siddharth
显示剩余12条评论

55

我创建了一个小型实用方法,可以在不通知用户的情况下更改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);
                }
            });
        }
    });
}

它会禁用监听器,改变选择,然后重新启用监听器。

诀窍在于调用是异步的,需要在连续的处理程序发布中完成。


太棒了。我有多个旋转器并尝试将它们所有的监听器设置为null,然后设置它们的值,然后将它们全部设置回它们应该是的状态,但由于某种原因,这并没有起作用。我尝试了这个函数,它却奏效了。我不知道为什么我的方法不起作用,但这个方法有效,所以我不在意:D - JStephen
6
请注意:如果您快速两次调用setSpinnerSelectionWithoutCallingListener方法,使得第二次调用在第一次已将监听器设置为null时进行,那么您的下拉框(spinner)将永远被卡在空监听器的状态。我建议以下修复方法:在spinner.setSelection(selection)之后添加if (listener == null) return; - Violet Giraffe

35

不幸的是,似乎针对这个问题最常见的两种解决方案,即计算回调次数和发布一个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(); }
}

3
这应该是得票最高的答案。它简单而又精妙。它允许你保持所有当前的实现相同,只有一个初始化行不同。确实使旧项目的修改变得相当容易。除此之外,我通过实现OnTouchLisener接口,在下拉菜单打开时关闭了键盘,一举两得。现在,我所有的下拉菜单都表现得像我想要的那样。 - user3829751
优美的答案。当我将addAll()添加到适配器中时,仍会触发第0个元素,但我的第0个元素是一个省略号,表示中立(不执行任何操作)行为。 - jwehrle

32

我在使用旋转器时遇到了很多问题,它会在我不想要的时候触发,并且这里所有的答案都不可靠。它们能用,但只有有时候有效。你最终会遇到它们将失败并引入错误到你的代码中的情况。

对我而言可行的方法是将上次选择的索引存储在一个变量中,并在监听器中进行评估。如果它与新选择的索引相同,则不执行任何操作并返回;否则继续监听器。这样做:

//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

}

相信我说的话,这绝对是最可靠的解决方案。虽然它是一个技巧,但它很有效!


如果您尝试更改值,这是否有效?在我的情况下,我正在尝试将值设置为类似于3的东西,而实际上是0,而不触发更改侦听器。您是说只有用户选择它时,int i才会返回不同的值吗? - JStephen
嗨,JStephen,我不确定你的意思。但是int i将是当onItemSelected被触发时spinner的位置。问题在于,无论何时加载spinner,都会触发onItemSelected,而没有任何实际的用户交互,从而导致在这种情况下出现不需要的行为。在此初始点上,int i将等于0,因为这是当spinner首次加载时的默认起始索引。因此,我的解决方案是检查确保选择了一个实际不同的项目,而不是重新选择当前选择的项目...这回答了你的问题吗? - Chris
嗨,Chris,我有一个页面,从数据库中提取信息供用户编辑。当页面打开时,我填充旋转器,然后将它们的位置设置为数据库中的值。因此,如果我将它们的位置设置为3,例如,这会导致onItemSelected触发,i设置为3,这与初始值不同。我想你是说只有在用户自己实际更改了值时,才会设置i。 - JStephen
4
如果用户选择了位置0会怎样?它们将被忽略。 - Yetti99
我认为最后一个位置的方式不是一个好主意。我通过从SharedPreferences加载位置并使用setSelection来初始化微调器。很多时候,SharedPrefs中的值与创建微调器时的默认值不同,因此在初始化时将触发onItemSelected。 - Arthez
如果您始终将UI的旋转器位置设置为零,则这是更简单的解决方案。它还可以与onItemSelected的双重调用一起使用,我最近验证了每次更改旋转器中的选择时。 - Jose_GD

27

为了更详细地说明如何使用onTouchListener来区分自动调用setOnItemSelectedListener(这是Activity初始化等的一部分)与由实际用户交互触发的调用,我在尝试了这里的其他建议后执行以下操作,并发现它能够以最少的代码行数很好地解决问题。

只需为您的Activity / Fragment设置一个布尔字段,例如:

private Boolean spinnerTouched = false;

在设置Spinner的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;

1
这很好用,而且自 Android 6+ 以来,这是唯一可行的方法。但是,您还必须对 setOnKeyListener() 执行相同的操作,否则当用户使用键盘导航时它不起作用。 - Stéphane
非常好用,其他解决方案在不同的手机上都有一些问题。 - Ziwei Zeng
这很简单,绝对完美!不需要额外的废话,只需记住逻辑即可。我很高兴一直滚动到这里! - user3833732
你可以通过子类化Spinner并在重写的preformClick()方法中设置标志spinnerTouched = true来代替setOnKeyListener(),该方法在触摸/按键两种情况下都会被调用。其余部分保持不变。 - Almighty
我只是想提一下,这似乎解决了我最近在这里发布的关于DropDownPreferences的同样的错误:https://stackoverflow.com/questions/61867118/dropdownpreference-setonpreferencechangelistener-calls-itself-when-initialized 说实话,我简直不敢相信 :D - Daniel Wilson

26

我遇到了类似的情况,我有一个简单的解决方案适用于我。

似乎方法setSelection(int position)setSelected(int position, boolean animate)有不同的内部实现。

当您使用第二种方法setSelected(int position, boolean animate)并将动画标志设置为false时,您会获得选择但不触发onItemSelected监听器。


更好的方法是不要担心对onItemSelected的额外调用,而是确保它显示正确的选择。因此,在添加侦听器之前调用spinner.setSelection(selectedIndex)可以让它始终正常工作。 - andude
1
Spinner 没有 setSelected(int position, boolean animate) 方法。 - shift66
4
你需要使用的实际调用是 setSelection(int position, boolean animate); - Brad
这解决了一个更普遍的问题,即当代码多次修改Spinner内容和选择时,仅为用户交互保留onItemSelected。 - alrama
4
遗憾的是,在API23中,虽然动画标志为假,但仍会调用onItemSelected - mcy

13
spinner.setSelection(Adapter.NO_SELECTION, false);

4
代码本身可能已经很清晰了,但稍作解释会更有助于理解 :) - nhaarman

11
这将会发生在你在代码中进行选择的情况下;
   mSpinner.setSelection(0);
代替上述声明,请使用
   mSpinner.setSelection(0,false);//just simply do not animate it.

编辑:这种方法不适用于小米 Android 版本的 Mi UI。


2
这对我来说绝对解决了问题。我阅读了有关Spinner小部件的文档...它绝对很棘手,难以理解以下区别:setSelection(int position, boolean animate) -> 直接跳转到适配器数据中的特定项目。setSelection(int position) -> 设置当前选定的项目。 - Matt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接