如何防止在新实例化的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个回答

8

我已经为此烦恼了很长时间,现在我创建了自己的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实例,然后像这样调用setSelection即可:
mMySaneSpinner.setSelection(1, true, true);

这样做就不会触发任何事件,也不会中断用户交互。这极大地简化了我的代码复杂度。既然它真的很痛苦,因此应该将它包含在原始的Android系统中。


1
这对我不起作用,它仍然会触发onItemSelected。 - Arthez
Arthez,请仔细检查一下您是否真的将true传递给了第三个参数。如果是的话,那么这里还有其他问题。如果可能的话,请发布您的代码。 - fusion44

8

如果你在布局完成后再添加监听器,就不会有来自布局阶段的不必要事件:

spinner.getViewTreeObserver().addOnGlobalLayoutListener(
    new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            // Ensure you call it only once works for JELLY_BEAN and later
            spinner.getViewTreeObserver().removeOnGlobalLayoutListener(this);

            // add the listener
            spinner.setOnItemSelectedListener(new OnItemSelectedListener() {

                @Override
                public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
                    // check if pos has changed
                    // then do your work
                }

                @Override
                public void onNothingSelected(AdapterView<?> arg0) {
                }

            });

        }
    });

这个方案可行,而且在我看来是解决 OP 特定问题最干净的方法。我想指出,在 J 版本以下的版本中,你可以通过调用 ViewTreeObserver.removeGlobalOnLayoutListener 来移除 ViewTreeObserver.OnGlobalLayoutListener,但该方法已被弃用,并且与此答案使用的方法名称相似。 - Jack Meister

6
我得到了一个非常简单的答案,100% 肯定有效:

我得到了一个非常简单的答案,100% 肯定有效:

boolean Touched=false; // this a a global variable

public void changetouchvalue()
{
   Touched=true;
}

// this code is written just before onItemSelectedListener

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

//inside your spinner.SetonItemSelectedListener , you have a function named OnItemSelected iside that function write the following code

if(Touched)
{
 // the code u want to do in touch event
}

3

由于没有什么方法适用于我,而且我的视图中有一个以上的旋转器(我认为持有一个布尔映射是过度杀伤),所以我使用标签来计算点击次数:

spinner.setTag(0);
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
            Integer selections = (Integer) parent.getTag();
            if (selections > 0) {
                // real selection
            }
            parent.setTag(++selections); // (or even just '1')
        }

        @Override
        public void onNothingSelected(AdapterView<?> parent) {
        }
    });

3

我已经找到了一种更加优雅的解决方案。它涉及到计算ArrayAdapter(在您的情况下是“adapter”)被调用的次数。假设您有1个Spinner并且您调用:

int iCountAdapterCalls = 0;

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);

在onCreate之后声明一个int计数器,然后在onItemSelected()方法内部放置一个“if”条件来检查适配器被调用的次数。在您的情况下,它只被调用了一次,因此:

if(iCountAdapterCalls < 1)
{
  iCountAdapterCalls++;
  //This section executes in onCreate, during the initialization
}
else
{
  //This section corresponds to user clicks, after the initialization
}

2

我的小贡献是在以上某些内容的基础上做了一些变化,这种方法在我几次使用中很适合。

声明一个整数变量作为默认值(或上次保存在首选项中的最后使用值)。

使用spinner.setSelection(myDefault)在注册监听器之前设置该值。

在onItemSelected中检查新的Spinner值是否等于你之前分配的值,然后再运行任何进一步的代码。

这样做的额外好处是如果用户再次选择相同的值,则不会运行代码。


1
很多答案已经有了,这是我的答案。我扩展了 AppCompatSpinner 并添加了一个方法 pgmSetSelection(int pos),允许在不触发选择回调的情况下进行程序化选择设置。我使用 RxJava 编写了这个方法,以便通过 Observable 传递选择事件。
package com.controlj.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AdapterView;

import io.reactivex.Observable;

/**
 * Created by clyde on 22/11/17.
 */

public class FilteredSpinner extends android.support.v7.widget.AppCompatSpinner {
    private int lastSelection = INVALID_POSITION;


    public void pgmSetSelection(int i) {
        lastSelection = i;
        setSelection(i);
    }

    /**
     * Observe item selections within this spinner. Events will not be delivered if they were triggered
     * by a call to setSelection(). Selection of nothing will return an event equal to INVALID_POSITION
     *
     * @return an Observable delivering selection events
     */
    public Observable<Integer> observeSelections() {
        return Observable.create(emitter -> {
            setOnItemSelectedListener(new OnItemSelectedListener() {
                @Override
                public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
                    if(i != lastSelection) {
                        lastSelection = i;
                        emitter.onNext(i);
                    }
                }

                @Override
                public void onNothingSelected(AdapterView<?> adapterView) {
                    onItemSelected(adapterView, null, INVALID_POSITION, 0);
                }
            });
        });
    }

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

    public FilteredSpinner(Context context, int mode) {
        super(context, mode);
    }

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

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

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

一个使用它的示例,在例如 Fragment 中的 onCreateView() 中调用:

    mySpinner = view.findViewById(R.id.history);
    mySpinner.observeSelections()
        .subscribe(this::setSelection);

在这里,setSelection()是封闭视图中的一个方法,看起来像这样,并且通过Observable从用户选择事件和其他编程方式调用,因此处理选择的逻辑对于两种选择方法都是通用的。

private void setSelection(int position) {
    if(adapter.isEmpty())
        position = INVALID_POSITION;
    else if(position >= adapter.getCount())
        position = adapter.getCount() - 1;
    MyData result = null;
    mySpinner.pgmSetSelection(position);
    if(position != INVALID_POSITION) {
        result = adapter.getItem(position);
    }
    display(result);  // show the selected item somewhere
}

1

在遇到相同问题后,我使用标签找到了解决方案。 其背后的思想很简单:每当程序改变下拉列表的选项时,确保标签反映所选位置。在监听器中,您检查所选位置是否等于标签。如果是,则表示下拉列表选择是通过程序更改的。

以下是我的新“下拉列表代理”类:

package com.samplepackage;

import com.samplepackage.R;
import android.widget.Spinner;

public class SpinnerFixed {

    private Spinner mSpinner;

    public SpinnerFixed(View spinner) {
         mSpinner = (Spinner)spinner;
         mSpinner.setTag(R.id.spinner_pos, -2);
    }

    public boolean isUiTriggered() {
         int tag = ((Integer)mSpinner.getTag(R.id.spinner_pos)).intValue();
         int pos = mSpinner.getSelectedItemPosition();
         mSpinner.setTag(R.id.spinner_pos, pos);
         return (tag != -2 && tag != pos);
    }

    public void setSelection(int position) {
        mSpinner.setTag(R.id.spinner_pos, position);
        mSpinner.setSelection(position);
    }

    public void setSelection(int position, boolean animate) {
        mSpinner.setTag(R.id.spinner_pos, position);
        mSpinner.setSelection(position, animate);
    }

    // If you need to proxy more methods, use "Generate Delegate Methods"
    // from the context menu in Eclipse.
}

你还需要一个XML文件,其中包含在Values目录中设置的标签。 我将文件命名为spinner_tag.xml,但这取决于你。 它看起来像这样:
<resources xmlns:android="http://schemas.android.com/apk/res/android">
  <item name="spinner_pos" type="id" />
</resources>

现在替换。
Spinner myspinner;
...
myspinner = (Spinner)findViewById(R.id.myspinner);

在你的代码中使用。
SpinnerFixed myspinner;
...
myspinner = new SpinnerFixed(findViewById(R.id.myspinner));

并使您的处理程序看起来有些像这样:

myspinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {

    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        if (myspinner.isUiTriggered()) {
            // Code you want to execute only on UI selects of the spinner
        }
    }

    @Override
    public void onNothingSelected(AdapterView<?> parent) {
    }
});

函数isUiTriggered()仅在用户更改了微调器时返回true。请注意,此函数具有副作用-它会设置标签,因此同一侦听器调用中的第二个调用将始终返回false
此包装器还将处理在布局创建期间调用侦听器的问题。
玩得开心, Jens.

0

我用最简单的方法完成了:

private AdapterView.OnItemSelectedListener listener;
private Spinner spinner;

onCreate();

spinner = (Spinner) findViewById(R.id.spinner);

listener = new AdapterView.OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> adapterView, View view, int position, long l) {

            Log.i("H - Spinner selected position", position);
        }

        @Override
        public void onNothingSelected(AdapterView<?> adapterView) {

        }
    };

 spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
            spinner.setOnItemSelectedListener(listener);
        }

        @Override
        public void onNothingSelected(AdapterView<?> adapterView) {

        }
    });

完成


这是一个有趣的解决方案。可以加入更多的解释。基本上,它有意忽略了第一个onItemSelected事件。它们在某些情况下可能很有效,但在启用辅助功能选项时可能不适用(请参见Jorrit的解释)。 - jk7

0
我会在创建 onClickListener 对象时存储初始索引。
   int thisInitialIndex = 0;//change as needed

   myspinner.setSelection(thisInitialIndex);

   myspinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {

      int initIndex = thisInitialIndex;

      @Override
      public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
         if (id != initIndex) { //if selectedIndex is the same as initial value
            // your real onselecteditemchange event
         }
      }

      @Override
      public void onNothingSelected(AdapterView<?> parent) {
      }
  });

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