在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,我们可以发现以下几个与点击事件相关的方法:
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时触发这个监听,有以下两种情况:
那么我们通过对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无响应解决方案。如果哪位网友有更好的方法,麻烦在评论里告知,谢谢!