如果选择了非零位置,则在旋转后调用 Spinner 的 onItemSelected 回调两次

Posted

技术标签:

【中文标题】如果选择了非零位置,则在旋转后调用 Spinner 的 onItemSelected 回调两次【英文标题】:Spinner's onItemSelected callback called twice after a rotation if non-zero position is selected 【发布时间】:2013-01-11 17:09:06 【问题描述】:

当我创建我的活动时,我设置了一个 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

这是预期的行为吗?我错过了什么吗?

【问题讨论】:

您找到解决方案了吗?我仍然坚持这个...... 【参考方案1】:

设法在另一个 *** 问题中找到解决方案:

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

【讨论】:

你能把那个 SOF 问题的链接放上去吗? 当然@AlvaroSantisteban,如果我没记错的话,它来自这个问题:***.com/questions/2562248/… 但这已经有一年多了,所以我不记得细节了:(。 3年后,非常感谢 不要这样做!是一个丑陋的解决方法。正确和最简单的方法是在设置监听器之前使用 setSelection(#, false)。见***.com/a/44979172/4966267 这解决了设置监听器时的初始双重调用。不幸的是,每次更改选择时都不能解决双重调用【参考方案2】:

一般来说,触发 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);

【讨论】:

IMO 这是一个非常优雅的解决方案。自成一体,易于理解。我已经使用View.post() hack 很长时间了,但很高兴我能四处看看,以防有人想出新的东西。 +1 我认为这比只在下一个 Looper 事件上设置监听器更干净,考虑到选择监听器默认情况下应该只在用户自己选择它的情况下发送事件。只要确保你也设置了触摸监听器,我忘了一秒钟。 这个解决方案似乎无法访问...使用 TalkBack 或其他屏幕阅读器应用程序的用户可能不会以这种方式触发选择。【参考方案3】:

在设置监听器之前使用 setSelection(#, false):

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

关键是第二个参数。它表示不为过渡设置动画,立即执行操作并防止 onItemSelected 被触发两次,因为系统已经进行了调用。

【讨论】:

这解决了最初的重复调用,但每次更改微调器中的选择时都不会解决第二次调用 检查您的逻辑@Jose_GD,也许您正在通过 OnItemSelected 侦听器中的某些操作直接或间接地再次触发选择。开始检查你的监听器。 我做到了,谢谢。没有什么可以再次触发该回调。终于用flag解决了。 好的,但是请注意您在做什么,因为 Spinner 不会自行触发两次侦听器,如果发生这种情况,您的代码中有一些奇怪的地方,可能会产生其他意外行为。 与作为答案提出的其他替代方案相比,我更喜欢这个解决方案,因为它很简单,不使用任何额外的变量。【参考方案4】:

onItemSelected 第一次运行时,view 尚未膨胀。第二次它已经膨胀了。解决方案是用if (view != null) 包装onItemSelected 中的方法。

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

    

【讨论】:

非常感谢!!这解决了我从另一个片段返回片段(onBackPressed)时 onItemSelected() 被触发两次的用例。微调器的这种行为让我发疯。你为我节省了几个小时:) 这是一个不必要的解决方法,查看解决方案***.com/a/44979172/4966267【参考方案5】:

这就是我所做的:

做一个局部变量

Boolean changeSpinner = true;

在saveInstanceMethod上保存spinner的选中项位置

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

然后在创建的活动上从 savedInstanceState 中获取该 int,如果 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;
        
    );

所以,第一次调用时什么也不做,只是将布尔变量设置为true,第二次将执行代码。 也许不是最好的解决方案,但它确实有效。

【讨论】:

看起来不错的解决方案。【参考方案6】:

我正在更新 @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

【讨论】:

【参考方案7】:

试试这个:

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;

【讨论】:

谢谢,但这似乎并没有解决我的问题,它只是使第一个回调调用“无效”(但第二个在轮换后仍然被触发)。我已经在做类似的事情了,问题是第一次活动创建与设备轮换的调用次数之间明显不一致。【参考方案8】:

一旦你知道有项目列表和要选择的位置,你就可以直接调用setSelection,这样你就可以避免onItemSelected被调用两次。

我创建了一篇关于我认为更好的方法的文章How to avoid onItemSelected to be called twice in Spinners

【讨论】:

【参考方案9】:

我编写了一个扩展函数,它会跳过除用户发起的选择事件之外的所有选择事件。 如果您使用的不是第一个默认微调器位置,请不要忘记覆盖 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

【讨论】:

【参考方案10】:

在 kotlin stateflow 中让它变得简单。在片段中,它也可以保存旋转数据。在我的代码中,我解决了

在 ViewModel 中:

private val _selectedPosition = MutableStateFlow(0)
val selectedPosition = _selectedPosition.asStateFlow()

fun setPosition(position: Int) 
    _selectedPosition.value = position

在片段中

val selectedPosition= viewModel.selectedPosition.value
spinner.setSelection(selectedPosition)

【讨论】:

以上是关于如果选择了非零位置,则在旋转后调用 Spinner 的 onItemSelected 回调两次的主要内容,如果未能解决你的问题,请参考以下文章

如果发现无效的表单字段,则停止 ladda spinner

回调:动画完成:在 .promise().done() 之后调用

Spinner 旋转器

Android Spinner列表选择框

选择项目后更改 Spinner 列表内容 Android

android 中如何设置Spinner点击后弹出的下拉列表的宽度样式