在日常开发中遇到这样一个问题,创建一个ListView,通过蓝牙搜索设备,向Adapter中添加设备,通过ListView进行设备展示。可是在蓝牙搜索过程中发现了·想要的设备名字,点击Adapter无任何响应,困扰了许久,最终发现是因为Adapter刷新过快导致ListView点击事件被屏蔽了。
来一起先看一下源码AbsListView的onTouchDown事件
private void onTouchDown(MotionEvent ev) {
mHasPerformedLongPress = false;
mActivePointerId = ev.getPointerId(0);
hideSelector();
if (mTouchMode == TOUCH_MODE_OVERFLING) {
// Stopped the fling. It is a scroll.
if (mFlingRunnable != null) {
mFlingRunnable.endFling();
}
if (mPositionScroller != null) {
mPositionScroller.stop();
}
mTouchMode = TOUCH_MODE_OVERSCROLL;
mMotionX = (int) ev.getX();
mMotionY = (int) ev.getY();
mLastY = mMotionY;
mMotionCorrection = 0;
mDirection = 0;
stopEdgeGlowRecede(ev.getX());
} else {
final int x = (int) ev.getX();
final int y = (int) ev.getY();
int motionPosition = pointToPosition(x, y);
if (!mDataChanged) {
if (mTouchMode == TOUCH_MODE_FLING) {
// Stopped a fling. It is a scroll.
createScrollingCache();
mTouchMode = TOUCH_MODE_SCROLL;
mMotionCorrection = 0;
motionPosition = findMotionRow(y);
if (mFlingRunnable != null) {
mFlingRunnable.flywheelTouch();
}
stopEdgeGlowRecede(x);
} else if ((motionPosition >= 0) && getAdapter().isEnabled(motionPosition)) {
// User clicked on an actual view (and was not stopping a
// fling). It might be a click or a scroll. Assume it is a
// click until proven otherwise.
mTouchMode = TOUCH_MODE_DOWN;
// FIXME Debounce
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = ev.getX();
mPendingCheckForTap.y = ev.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
}
}
if (motionPosition >= 0) {
// Remember where the motion event started
final View v = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = v.getTop();
}
mMotionX = x;
mMotionY = y;
mMotionPosition = motionPosition;
mLastY = Integer.MIN_VALUE;
}
if (mTouchMode == TOUCH_MODE_DOWN && mMotionPosition != INVALID_POSITION
&& performButtonActionOnTouchDown(ev)) {
removeCallbacks(mPendingCheckForTap);
}
}
在这里可以看到当点击事件想要生效,需要至少64ms时间,此时mDataChanged必须为false,当Adapter刷新过快,小于64ms,就会与onTouchDown事件冲突,导致点击事件失效。
知道了原因就很好解决了,可以在Adapter中添加一个开关,重写一下Adapter的notifyDataSetChanged()方法。
// 重写方法,改动小
public void notifyDataSetChanged() {
if (isCanRefresh) {
super.notifyDataSetChanged();
}
}
public void setCanRefresh(boolean canRefresh) {
this.isCanRefresh = canRefresh;
}
紧接着自定义CanRefreshListView继承自ListView重写onTouchEvent方法,在里面设置,当按下时停止数据刷新,留出64ms时间即可。
public class CanRefreshListView extends ListView {
private DevicesAdapter mAdapter;
public CanRefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void setAdapter(ListAdapter adapter) {
super.setAdapter(adapter);
mAdapter=(DevicesAdapter) adapter;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
if (mAdapter != null) {
mAdapter.setCanRefresh(false);
resetListRefreshState();
}
break;
case MotionEvent.ACTION_UP:
resetListRefreshState();
break;
default:
break;
}
return super.onTouchEvent(ev);
}
// 恢复列表可刷新状态,加延迟是因为按下到抬起需要至少64毫秒来响应OnItemClick时间,延迟时间可以自己调。
private void resetListRefreshState() {
postDelayed(() -> {
if (mAdapter != null) {
mAdapter.setCanRefresh(true);
// 手动刷新一下列表,防止数据最后一次更新没有生效
mAdapter.notifyDataSetChanged();
}
}, 100);
}
}
此时就不会再出现点击事件无反应的现象了,可能以为这就结束了,开始我也这么以为,可是,还是会在偶然间程序崩溃,会出现这样一个错误:
java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. Make sure your adapter calls notifyDataSetChanged() when its content changes. [in ListView(2131296583, class com.contec.bluetoothtest.bean.CanRefreshListView) with Adapter(class com.contec.bluetoothtest.adapter.DevicesAdapter)]
这是因为偶然时候Adapter的数据已经更新了,但是 ListView还未执行更新,异步操作会导致错误,在
mAdapter.notifyDataSetChanged()加上requestLayout()即可,至此这个问题终于解决了。