Android中Spinner控件关于二次点击同一item无响应事件解析及处理方法

分析

在Android开发中难免会使用到Spinner控件,而且经常会对其绑定点击事件。下面就从源码上来解析下为什么Spinner不对同一item二次点击进行事件响应。
我们从Spinner的事件入手,我们来看以下几个事件绑定,首先是Spinner本身的

    /**
     * 

A spinner does not support item click events. Calling this method * will raise an exception.

*

Instead use {@link AdapterView#setOnItemSelectedListener}. * * @param l this listener will be ignored */ @Override public void setOnItemClickListener(OnItemClickListener l) { throw new RuntimeException("setOnItemClickListener cannot be used with a spinner."); }

好吧,显然 这个方法是无用的。再看Spinner的父类AbsSpinner发现其中并没有关于事件绑定的方法,继续往上找AbsSpinner的父类AdapterView,我们可以发现以下几个与点击事件相关的方法:

  1. setOnClickListener(View.OnClickListener l)
  2. setOnItemClickListener(AdapterView.OnItemClickListener listener)
  3. setOnItemLongClickListener(AdapterView.OnItemLongClickListener listener)
  4. setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener)

    由于Spinner本身override了setOnItemClickListener()方法 所以这个略过,那么剩下的

    @Override
    public void setOnClickListener(OnClickListener l) {
        throw new RuntimeException("Don't call setOnClickListener for an AdapterView. "
                + "You probably want setOnItemClickListener instead");
    }

setOnClickListener()这个也不用看了,继续

    /**
     * Register a callback to be invoked when an item in this AdapterView has
     * been clicked and held
     *
     * @param listener The callback that will run
     */
    public void setOnItemLongClickListener(OnItemLongClickListener listener) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        mOnItemLongClickListener = listener;
    }

这个顾名思义长点击事件,这个不符合我们使用情况,最后只剩下一个方法也是我们平时用的

    /**
     * Register a callback to be invoked when an item in this AdapterView has
     * been selected.
     *
     * @param listener The callback that will run
     */
    public void setOnItemSelectedListener(@Nullable OnItemSelectedListener listener) {
        mOnItemSelectedListener = listener;
    }


    /**
     * Interface definition for a callback to be invoked when
     * an item in this view has been selected.
     */
    public interface OnItemSelectedListener {
        /**
         * 

Callback method to be invoked when an item in this view has been * selected. This callback is invoked only when the newly selected * position is different from the previously selected position or if * there was no selected item.

* * Impelmenters can call getItemAtPosition(position) if they need to access the * data associated with the selected item. * * @param parent The AdapterView where the selection happened * @param view The view within the AdapterView that was clicked * @param position The position of the view in the adapter * @param id The row id of the item that is selected */
void onItemSelected(AdapterView parent, View view, int position, long id); /** * Callback method to be invoked when the selection disappears from this * view. The selection can disappear for instance when touch is activated * or when the adapter becomes empty. * * @param parent The AdapterView that now contains no selected item. */ void onNothingSelected(AdapterView parent); }

从onItemSelected的注释中可知google设计Spinner只对有别于之前选中的选中项进行事件响应,那么Spinner是如何触发选中事件呢?

    /**
     * Called after layout to determine whether the selection position needs to
     * be updated. Also used to fire any pending selection events.
     */
    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();
        }
    }

 void selectionChanged() {
        // We're about to post or run the selection notifier, so we don't need
        // a pending notifier.
        mPendingSelectionNotifier = null;

        if (mOnItemSelectedListener != null
                || AccessibilityManager.getInstance(mContext).isEnabled()) {
            if (mInLayout || mBlockLayoutRequests) {
                // If we are in a layout traversal, defer notification
                // by posting. This ensures that the view tree is
                // in a consistent state and is able to accommodate
                // new layout or invalidate requests.
                if (mSelectionNotifier == null) {
                    mSelectionNotifier = new SelectionNotifier();
                } else {
                    removeCallbacks(mSelectionNotifier);
                }
                post(mSelectionNotifier);
            } else {
                dispatchOnItemSelected();
            }
        }
    }

我们可以找到上面的方法、通过注释可知这个方法是在layout确定是否需要对选中位置进行更新操作后调用的,同样常被用来触发任何待定(未发送)的选择事件。
那么选中事件是否响应取决于以下条件

(mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)

这个条件的意思就是:当前选择的跟之前选中的不一样的才会触发selectionChanged()方法来
post(mSelectionNotifier);那么解决方法来了,只要让以上条件保持成立就可以了即保证mOldSelectedPosition或者mOldSelectedRowId不为当前选中值,最简单的就是把他们改成初始值,那么这里我们就需要使用放射的机制来达到这个目的。

方法一

示例如下

mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {

            @Override
            public void onItemSelected(AdapterView parent, View view, int position, long id) {

                try {
                    Class clazz = AdapterView.class;
                    Field field = clazz.getDeclaredField("mOldSelectedPosition");
                    field.setAccessible(true);
                    field.setInt(mSpinner,AdapterView.INVALID_POSITION);
                } catch(Exception e){
                    e.printStackTrace();
                }
            }

            @Override
            public void onNothingSelected(AdapterView parent) {
                System.out.println(parent.getId());
            }
        });

当然你也可以在onTouch事件里对值进行修改:

mSpinner.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                try {
                    Class clazz = AdapterView.class;
                    Field field = clazz.getDeclaredField("mOldSelectedPosition");
                    field.setAccessible(true);
                    field.setInt(mSpinner,AdapterView.INVALID_POSITION);
                } catch(Exception e){
                    e.printStackTrace();
                }
                return false;
            }
        });

方法二

不可厚非,通过以上方法是可以达到目的,但是未免太过于暴力,那么是否还有其他方法呢?回到checkSelectionChanged() 的注释看看:

Called after layout to determine whether the selection position needs to
be updated. Also used to fire any pending selection events.

从这里可以得知一个信息,在选中item时会触发layout更新事件,那么从这里能不能获得我们想到的东西,我们继续往上找到AdapterView的父类ViewGroup。我们可以找到以下方法:

 /**
     * Interface definition for a callback to be invoked when the hierarchy
     * within this view changed. The hierarchy changes whenever a child is added
     * to or removed from this view.
     */
    public interface OnHierarchyChangeListener {
        /**
         * Called when a new child is added to a parent view.
         *
         * @param parent the view in which a child was added
         * @param child the new child view added in the hierarchy
         */
        void onChildViewAdded(View parent, View child);

        /**
         * Called when a child is removed from a parent view.
         *
         * @param parent the view from which the child was removed
         * @param child the child removed from the hierarchy
         */
        void onChildViewRemoved(View parent, View child);
    }

    /**
     * Register a callback to be invoked when a child is added to or removed
     * from this view.
     *
     * @param listener the callback to invoke on hierarchy change
     */
    public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
        mOnHierarchyChangeListener = listener;
    }

那么我们能不能通过判断子view的差异来判断是否为二次点击同一item(view)事件。当Spinner点击item时触发这个监听,有以下两种情况:

  1. 前后点击不同item
    监听回调的顺序是:onChildViewRemoved{从父类view中移除选中状态的item_A视图并使之处于分离状态}–>onChildViewAdded{往父类view新增选中状态的item_B视图}–>onChildViewRemoved{完成对处于分离状态的item_A视图移除}
  2. 前后点击同一个item
    监听回调的顺序是:onChildViewRemoved{选中状态的item_A视图}–>onChildViewAdded{选中状态的item_A视图}

那么我们通过对onChildViewRemoved()方法进行断点调试可以在parent里看到以下内容:
Android中Spinner控件关于二次点击同一item无响应事件解析及处理方法_第1张图片
可以很明显的看到mNextSelectedRowId、mOldSelectedRowId、mSelectedRowId、mOldSelectedPosition、mSelectedPosition这几个变量,那么只要取到这前后的两个选中项id就可以满足我们的需求了、因为不管是那种情况onChildViewRemoved是一定会被执行的,相对上一个方法会更温和些。示例如下:

mSpinner.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {

            @Override
            public void onChildViewAdded(View parent, View child) {
            }

            @Override
            public void onChildViewRemoved(View parent, View child) {

                try {
                    Class clazz = AdapterView.class;
                    Field mOldSelectedPosition = clazz.getDeclaredField("mOldSelectedPosition");
                    Field mSelectedPosition = clazz.getDeclaredField("mSelectedPosition");
                    mOldSelectedPosition.setAccessible(true);
                    mSelectedPosition.setAccessible(true);
                    if (mOldSelectedPosition.getInt(mSpinner) == mSelectedPosition.getInt(mSpinner)) {
                        //响应事件
                        System.out.println("mSelectedPosition" + mSelectedPosition.getInt(mSpinner));
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }

        });

以上就是两种处理Spinner二次点击同一Item无响应解决方案。如果哪位网友有更好的方法,麻烦在评论里告知,谢谢!

你可能感兴趣的:(Android)