解决Spinner再次选择同一个Item不调用onItemSelected方法问题

如题,在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希望大家反馈,同时希望对大家有用。

你可能感兴趣的:(解决Spinner再次选择同一个Item不调用onItemSelected方法问题)