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

来源:互联网 发布:淘宝网上有卖烟的吗 编辑:程序博客网 时间:2024/04/29 23:39

分析

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

    /**     * <p>A spinner does not support item click events. Calling this method     * will raise an exception.</p>     * <p>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 {        /**         * <p>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.</p>         *         * 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里看到以下内容:
这里写图片描述
可以很明显的看到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无响应解决方案。如果哪位网友有更好的方法,麻烦在评论里告知,谢谢!

0 0
原创粉丝点击