如题,在android里面的Spinner第二次选择同一个选项的时候是不会触发调用onItemSelected方法的。
void checkSelectionChanged() {
if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
selectionChanged();
mOldSelectedPosition = mSelectedPosition;
mOldSelectedRowId = mSelectedRowId;
}
// If we have a pending selection notification -- and we won't if we
// just fired one in selectionChanged() -- run it now.
if (mPendingSelectionNotifier != null) {
mPendingSelectionNotifier.run();
}
}
通过上面这段源码,大家应该就知道原因了。下面看怎么解决这个问题。
解决方案一
这种是网上普遍的解决方式:
@Override
public void onItemSelected(AdapterView> parentView, View view, int position, long id) {
Class> myClass = AdapterView.class;
try {
Field field = myClass.getDeclaredField("mOldSelectedPosition");
field.setAccessible(true);
field.setInt(mSpinner,AdapterView.INVALID_POSITION);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
在每次选中选项后,把mOldSelectedPosition设置为一个无效的位置。这样程序在每次执行checkSelectionChanged方法的时候都以为两次选择的不是同一个选项。
从表面上看,方案一已经解决了问题,但是用过这种方案,并且细心(或者踩过坑)的人就知道,其实这种方案是有BUG的。
我们接下来来分析一下BUG出现在哪里:
首先,在Spinner的layout方法里面:
/**
* Creates and positions all views for this Spinner.
*
* @param delta Change in the selected position. +1 means selection is moving to the right,
* so views are scrolling to the left. -1 means selection is moving to the left.
*/
@Override
void layout(int delta, boolean animate) {
int childrenLeft = mSpinnerPadding.left;
int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right;
if (mDataChanged) {
handleDataChanged();
}
// Handle the empty set by removing all views
if (mItemCount == 0) {
resetList();
return;
}
if (mNextSelectedPosition >= 0) {
setSelectedPositionInt(mNextSelectedPosition);
}
recycleAllViews();
// Clear out old views
removeAllViewsInLayout();
// Make selected view and position it
mFirstPosition = mSelectedPosition;
if (mAdapter != null) {
View sel = makeView(mSelectedPosition, true);
int width = sel.getMeasuredWidth();
int selectedOffset = childrenLeft;
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
selectedOffset = childrenLeft + (childrenWidth / 2) - (width / 2);
break;
case Gravity.RIGHT:
selectedOffset = childrenLeft + childrenWidth - width;
break;
}
sel.offsetLeftAndRight(selectedOffset);
}
// Flush any cached views that did not get reused above
mRecycler.clear();
invalidate();
checkSelectionChanged();
mDataChanged = false;
mNeedSync = false;
setNextSelectedPositionInt(mSelectedPosition);
}
大家把目光聚焦到倒数第四行代码checkSelectionChanged(),没错Spinner判断选项改变的函数是在layout中被调用的,而
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
layout(0, false);
mInLayout = false;
}
layout又在onLayout中被调用过,因此每次调用onLayout、layout函数的时候都会去检查选项是否改变。因此你会发现调用了多少次layout你的onItemSelected方法就会被调用几次。但是我们的原意是只有在选项被选中时调用一次onItemSelected方法(不管选中的是不是同一个),但是现在是调用的多次(一般是两次),这可能会给我们编程带来问题。
第二种方案
自定义Spinner
package hld.moa.util.view;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.Context;
import android.content.res.Resources;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.Spinner;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Created by Administrator on 2017/2/17.
*/
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
public class MySpinner extends Spinner {
public MySpinner(Context context) {
super(context);
init();
}
public MySpinner(Context context, int mode) {
super(context, mode);
init();
}
public MySpinner(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MySpinner(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
public MySpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) {
super(context, attrs, defStyleAttr, mode);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public MySpinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, int mode) {
super(context, attrs, defStyleAttr, defStyleRes, mode);
init();
}
@RequiresApi(api = Build.VERSION_CODES.M)
public MySpinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, int mode, Resources.Theme popupTheme) {
super(context, attrs, defStyleAttr, defStyleRes, mode, popupTheme);
init();
}
@TargetApi(Build.VERSION_CODES.KITKAT)
private void init() {
Class> myClass = Spinner.class;
try {
Class>[] params = new Class[1];
params[0] = OnItemClickListener.class;
Method m = myClass.getDeclaredMethod("setOnItemClickListenerInt", params);
m.setAccessible(true);
m.invoke(this, new OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
Class> myClass = AdapterView.class;
try {
Field field = myClass.getDeclaredField("mOldSelectedPosition");
field.setAccessible(true);
field.setInt(MySpinner.this, AdapterView.INVALID_POSITION);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
});
} catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
思路很简单,就是利用反射,获取到setOnItemClickListenerInt方法,并使用该方法对Spinner的下拉列表设置OnItemClickListener(Spinner的setOnItemClickListener方法是被禁用的,如果使用该方法,将直接抛异常)。然后在每次点击选项的时候将mOldSelectedPosition设置为无效的位置即可。
该方法只经过简单的测试,如果还有BUG希望大家反馈,同时希望对大家有用。