ListAdapter刷新过快导致点击事件无响应

在Android开发中,当ListView的Adapter快速刷新时,可能导致点击事件无响应。原因是Adapter刷新过快与onTouchDown事件冲突。解决办法是在Adapter中添加控制开关,重写notifyDataSetChanged(),并在自定义的ListView中阻止数据刷新期间的触摸事件。如果出现adapter内容改变但ListView未收到通知的异常,确保在mAdapter.notifyDataSetChanged()后调用requestLayout()。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在日常开发中遇到这样一个问题,创建一个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()即可,至此这个问题终于解决了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值