PagerAdapter.notifyDataSetChanged()闪屏问题解决

本文深入探讨了在使用ViewPager时遇到的闪烁问题,特别是在横屏查看大图并切换时。分析了重写getItemPosition()方法使用POSITION_NONE模式导致闪烁的根本原因,并提供了解决方案,即保存item的View对象直接更新数据,避免闪烁。

现象
功能需求:横屏查看大图,左右滑动切换大图,大图上有一些涂鸦、描述文字信息,可以通过title上面的按钮隐藏某一张的涂鸦、描述信息;
点击title.hiddenDoodleBtn去改变javabean中的值,执行adapter.notifyDataSetChanged()去更新UI,此时就会出现当前展示也闪烁的问题;
分析*
重写getItemPosition()使notifyDataSetChanged()去执行

@Override
public int getItemPosition(@NonNull Object object) {
    return POSITION_NONE;
}

使用POSITION_NONE模式可以达到notifyDataSetChanged()去更新的效果,但是会导致,每个item都是remove了之后,再重新新建后再添加到布局上,这就会导致更新时候闪烁的根本原因;
分析ViewPager.dataSetChanged()方法可以分析出结果:

void dataSetChanged() {
	………………
    for (int i = 0; i < mItems.size(); i++) {
        final ItemInfo ii = mItems.get(i);
        final int newPos = mAdapter.getItemPosition(ii.object);

        if (newPos == PagerAdapter.POSITION_UNCHANGED) {
            continue;
        }

        if (newPos == PagerAdapter.POSITION_NONE) {
            mItems.remove(i);	//删除item数据,导致闪烁根本原因
            i--;

            ………………
            continue;
        }

        ………………
    }

    ………………
}

解决
将instantiateItem()中得到的view保存在一个集合中,若需要更新的时候,直接从集合中得到item的View对象,用View直接去更新数据,就可以达到目的;
在使用这个过程中,原本是想做一个弱引用去保存View对象,避免View被回收抛nullException,但是用这种方法,无法更新;
错误代码:

public class ImgLabelPargeAdapter extends PagerAdapter {
    ………………
    private List<WeakReference<SimpleImgLoadingLayout>> mImageViews = new ArrayList<>();

    ………………
    @SuppressLint("CheckResult")
    @Override
    public Object instantiateItem(ViewGroup container, final int position) {
        final SimpleImgLoadingLayout view = new SimpleImgLoadingLayout(mContext);
        
        mImageViews.add(new WeakReference<>(view));
        container.addView(view);
        return view;
    }

    /**
     * 必须要实现的方法
     * 滑动切换的时销毁一个页面,ViewPager同时加载3个页面,假如此时你正在第二个页面,向左滑动,
     * 将销毁第1个页面
     */
    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView((View) object);
        mImageViews.remove(object);
    }
}

正确逻辑
这里可以不用弱引用,集合里面的item数据不会一直增加,他跟你的设置有关ViewPager.setOffscreenPageLimit(3),集合里面的数据不会超过4个,会执行destroyItem()方法去释放item数据,所以直接使用强应用关系即可,此时就可以执行更新了;

public class ImgLabelPargeAdapter extends PagerAdapter {
    private Context mContext;
    private List<ImgLabelBean> lDatas;
    private List<SimpleImgLoadingLayout> mImageViews = new ArrayList<>();

    /**
     * 设置是否显示标注信息
     *
     * @param position 当前itemView的索引
     * @param showTag  true:显示标签;false:隐藏标签;
     */
    public void showTagOption(int position, boolean showTag) {
        if (null == mImageViews || position >= mImageViews.size()) {
            return;
        }

        SimpleImgLoadingLayout sll = mImageViews.get(position);
        ImgLabelBean bean = lDatas.get(position);
        if (null == sll || null == bean) {
            return;
        }

        bean.setShowMark(showTag);
        sll.showTagOption(showTag);
    }

    /**
     * 简称/全称 切换
     *
     * @param position     当前itemView的索引
     * @param showFullName true:全称;false:简称;
     */
    public void showFullTagName(int position, boolean showFullName) {
        if (null == mImageViews || position >= mImageViews.size()) {
            return;
        }

        SimpleImgLoadingLayout sll = mImageViews.get(position);
        ImgLabelBean bean = lDatas.get(position);
        if (null == sll || null == bean) {
            return;
        }

        bean.setShowFullName(showFullName);
        sll.showFullTagName(showFullName);
    }

    /**
     * 清空保存的Item View的数据
     */
    public void clearView() {
        if (null != mImageViews) {
            mImageViews.clear();
        }
    }

    public ImgLabelPargeAdapter(Context context, List<ImgLabelBean> lAllLabel) {
        mContext = context;
        this.lDatas = lAllLabel;
    }

    @Override
    public int getCount() {
        if (lDatas == null) {
            return 0;
        }

        return lDatas.size();
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == object;
    }

    @Override
    public int getItemPosition(@NonNull Object object) {
        return POSITION_NONE;
    }

    /**
     * 必须要实现的方法
     * 每次滑动的时实例化一个页面,ViewPager同时加载3个页面,假如此时你正在第二个页面,向左滑动,
     * 将实例化第4个页面
     **/
    @SuppressLint("CheckResult")
    @Override
    public Object instantiateItem(ViewGroup container, final int position) {
		//这里需要将每个position都要有占位操作,否则由于缓存的原因 mImageViews.size()会小于position大小
        while (mImageViews.size() <= position) {
            mImageViews.add(null);
        }

        final ImgLabelBean labelBean = lDatas.get(position);
        final SimpleImgLoadingLayout view = new SimpleImgLoadingLayout(mContext);
        view.loadImgUrl(labelBean.getImgUrl(), GlideLoadUtil.COMPRESS_TYPE_ORGINAL, GlideLoadUtil.ERROR_ONLY);
        view.showTagOption(labelBean.isShowMark());
        view.showFullTagName(labelBean.isShowFullName());

        //避免图片还没有被imageView渲染出来就发送了数据的问题
        Observable.timer(100, TimeUnit.MILLISECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Long>() {
                    @Override
                    public void accept(Long aLong) throws Exception {
                        view.addMarkPoints(labelBean.getTagResults());
                    }
                });

        mImageViews.set(position, view);
        container.addView(view);
        return view;
    }

    /**
     * 必须要实现的方法
     * 滑动切换的时销毁一个页面,ViewPager同时加载3个页面,假如此时你正在第二个页面,向左滑动,
     * 将销毁第1个页面
     */
    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView((View) object);
        mImageViews.set(position, null);
    }
}

参考资料
Viewpager+FragmentStatePagerAdapter动态添加,删除,移动位置,(局部)更新页面(Fragment)不闪屏,不错位解决方案

PagerAdapter闪屏坑的修复

<think> 我们讨论的是ViewPager的刷新问题。用户之前的问题是如何让ViewPager一次性加载所有页面,现在的新问题是:如何将`viewPager.getAdapter().notifyDataSetChanged()`的异步刷新改为同步刷新。 首先,我们需要明确:在Android中,`notifyDataSetChanged()`的调用会触发ViewPager的重新布局和绘制,这个过程是异步的,因为它需要等待下一个UI刷新周期(即下一个VSync信号)来执行。这是Android UI框架的设计,我们无法将其改为真正的“同步”(即立即执行而不等待主线程消息队列),但我们可以确保在调用后立即触发ViewPager的刷新,并等待刷新完成。 然而,根据引用[2]和[3]的内容,我们知道ViewPager在调用`notifyDataSetChanged()`时,界面可能不会立即刷新,甚至有时根本不刷新。引用[3]提供了一种解决方案:在PagerAdapter中重写`getItemPosition`方法并返回`POSITION_NONE`,这样每次调用`notifyDataSetChanged()`时,ViewPager会认为所有的页面位置都发生了变化,从而重新加载所有页面。但这会导致所有页面被销毁并重新创建,开销较大。 用户希望将刷新改为同步,即调用`notifyDataSetChanged()`后立即完成刷新。我们可以尝试以下方法: 1. **强制ViewPager立即刷新**: 在调用`notifyDataSetChanged()`后,我们可以尝试强制ViewPager立即执行布局和绘制。但是,Android的UI更新是批量处理的,通常我们无法强制立即刷新,但可以尝试以下方法: - 调用`viewPager.requestLayout()`和`viewPager.invalidate()`,但这仍然会等到下一个绘制周期。 - 使用`viewPager.setAdapter(null)`然后立即`viewPager.setAdapter(adapter)`,但这会导致ViewPager完全重置,可能会有闪烁,而且不符合“同步”的预期(因为重置也是异步的)。 2. **使用Handler.post立即执行刷新**: 我们可以尝试将刷新操作放在一个Runnable中,然后通过Handler立即将其发布到主线程的消息队列,这样它会在当前主线程任务执行完后立即执行,但仍然是异步的。 3. **等待刷新完成**: 实际上,我们无法在调用`notifyDataSetChanged()`后“同步”等待刷新完成,因为刷新是在主线程的下一个绘制周期进行的。但是,我们可以通过监听ViewPager的绘制事件来得知刷新何时完成。例如,使用`ViewTreeObserver`监听绘制事件。 然而,用户的需求可能是希望在刷新完成后立即执行某些操作。我们可以这样: 步骤: a. 调用`adapter.notifyDataSetChanged()` b. 然后,我们注册一个ViewTreeObserver的OnDrawListener,在绘制完成后执行我们需要的操作。 示例代码: ```java viewPager.getAdapter().notifyDataSetChanged(); viewPager.getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() { @Override public void onDraw() { // 移除监听,避免多次调用 viewPager.getViewTreeObserver().removeOnDrawListener(this); // 在这里执行刷新完成后的操作 } }); ``` 注意:`onDraw()`会在每次绘制时调用,所以我们执行一次就移除监听。 但是,这种方法只能捕获到下一次绘制事件,而绘制事件可能由其他原因触发,不一定是我们的刷新。因此,我们需要确保在刷新后立即监听,并且只关心第一次绘制。 另外,引用[3]提到,重写`getItemPosition`返回`POSITION_NONE`可以强制刷新,我们可以在适配器中这样做: ```java @Override public int getItemPosition(Object object) { return POSITION_NONE; } ``` 这样,每次调用`notifyDataSetChanged()`时,ViewPager会重新加载所有页面。结合上面的绘制监听,我们可以确保在刷新完成后执行操作。 但是,如果我们的需求是“同步”刷新,即阻塞当前线程直到刷新完成,这是不可取的,因为会阻塞主线程,导致ANR。所以,我们只能采用回调的方式。 因此,我们推荐的做法是: 1. 在适配器中重写`getItemPosition`返回`POSITION_NONE`(如果确实需要每次刷新都重建所有页面)。 2. 调用`notifyDataSetChanged()`。 3. 使用`ViewTreeObserver`监听绘制事件,在绘制完成后执行后续操作。 但是,如果只是希望ViewPager立即刷新而不需要等待绘制完成,我们可以尝试调用`viewPager.setOffscreenPageLimit()`来触发一次重新布局?或者使用其他方法? 实际上,ViewPager在调用`notifyDataSetChanged()`后,内部会调用`populate()`方法重新填充页面。我们可以尝试通过反射强制调用`populate()`方法,但这样不推荐。 另一种思路:直接调用ViewPager的`dataSetChanged()`方法(私有方法)?同样不推荐。 所以,总结一下,我们可以通过监听绘制事件来在刷新完成后执行操作,但不能真正实现同步刷新(阻塞直到完成)。我们只能让刷新操作尽快发生,并在完成后得到通知。 因此,我们这样回答: 在Android中,`notifyDataSetChanged()`的刷新是异步的,这是由Android UI框架的机制决定的。但是,我们可以通过以下步骤来实现“立即刷新”并在刷新完成后执行操作: 1. 确保适配器正确重写`getItemPosition`方法,使得调用`notifyDataSetChanged()`时ViewPager能够刷新页面(返回`POSITION_NONE`会强制重建所有页面): ```java @Override public int getItemPosition(Object object) { return POSITION_NONE; } ``` 2. 调用`notifyDataSetChanged()`后,通过监听ViewPager的绘制事件来执行刷新完成后的操作: ```java viewPager.getAdapter().notifyDataSetChanged(); viewPager.getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() { @Override public void onDraw() { viewPager.getViewTreeObserver().removeOnDrawListener(this); // 这里执行刷新完成后的操作 } }); ``` 3. 为了确保刷新尽快执行,我们可以手动触发一次布局请求: ```java viewPager.requestLayout(); ``` 注意:由于`onDraw`可能会被频繁调用,我们在第一次绘制后立即移除监听。 但是,如果ViewPager当前不可见(比如在后台),则不会触发绘制。因此,我们需要考虑这种情况,或者使用其他监听方式,例如`ViewTreeObserver.OnGlobalLayoutListener`(监听布局变化)。但布局变化不一定代表页面内容刷新完成。 另外,我们也可以自定义ViewPager,重写`onAdapterChanged`或`dataSetChanged`方法,然后暴露一个回调,但这样侵入性较强。 由于ViewPager的刷新是异步的,我们无法将其改为同步阻塞的方式,但可以通过回调的方式在刷新完成后执行后续操作。 因此,最终方案:使用适配器重写`getItemPosition`返回`POSITION_NONE`(如果数据变化会导致页面位置变化,比如删除或插入页面,这是必要的),然后通过监听绘制事件来得到刷新完成的时机。 注意:重写`getItemPosition`返回`POSITION_NONE`会导致所有页面被销毁并重建,如果页面很多且复杂,可能会有性能问题。如果只是页面内容变化(例如更新文本、图片等),而页面数量不变,我们可以通过其他方式更新页面内容(例如在Fragment中自己更新,而不需要重建整个Fragment)。这时,我们就不需要返回`POSITION_NONE`,而是返回`POSITION_UNCHANGED`(默认)或具体的位置,然后通过其他方式更新页面内容。 所以,我们需要根据实际情况选择: 情况1:数据变化导致页面数量或顺序变化(如删除、插入)-> 使用`POSITION_NONE`,然后刷新。 情况2:只是更新现有页面的内容 -> 不需要重建Fragment,可以在Fragment内部更新,此时不需要返回`POSITION_NONE`,但是需要找到更新特定页面的方法。 对于情况2,我们可以: - 在Fragment中定义一个更新方法,然后在Activity中通过适配器找到该Fragment并调用更新方法。 - 或者,在适配器的`instantiateItem`方法中,我们可以将Fragment保存起来,然后当需要更新时,直接调用该Fragment的更新方法。 这样,我们就不需要调用`notifyDataSetChanged()`,或者即使调用,由于页面没有重建,我们保存的Fragment引用仍然有效,然后我们主动更新它。 所以,如果用户的问题是关于更新页面内容(而不是页面结构变化),我们可以采用更高效的方式,而不需要强制ViewPager重建页面,也就不需要等待刷新完成。 由于用户的问题描述是“刷新为异步的问题”,我们还需要考虑刷新后立即获取更新后的视图状态。如果用户需要在刷新后立即获取视图的尺寸等信息,那么等待绘制完成是必要的。 综上所述,我们给出两种方案: 方案一(适用于页面结构变化,如增删页面): - 适配器重写`getItemPosition`返回`POSITION_NONE` - 调用`notifyDataSetChanged()` - 通过`ViewTreeObserver`监听绘制完成 方案二(适用于更新页面内容): - 不在适配器中返回`POSITION_NONE`(默认即可) - 通过其他方式更新页面内容(如直接操作Fragment) - 这样就不需要等待ViewPager刷新,因为页面不会重建,内容会立即更新(但更新操作也是需要时间的,如果更新后需要立即获取视图状态,同样需要等待绘制) 因此,用户需要根据实际场景选择方案。 最后,回答用户的问题:无法将`notifyDataSetChanged()`改为同步刷新,但可以通过监听绘制事件来在刷新完成后执行操作。同时,根据刷新内容的不同,我们可以选择不同的更新策略。 由于引用[2]和[3]都提到了`notifyDataSetChanged()`无效的问题,所以我们按照上述方案解决。 我们按照用户要求,在回答中引用相关引用,但注意用户提供的引用[1]是关于ViewPager的基本介绍,引用[2][3]是关于刷新无效的解决方法,引用[4]是关于小圆点的样式。我们主要参考[2][3]。 所以,最终回答如下: </think> 在 Android 开发中,`ViewPager.getAdapter().notifyDataSetChanged()` 的刷新本质上是异步的,这是由 Android UI 框架的消息循环机制决定的。要实现"同步刷新"效果(即立即更新视图),可通过以下方法实现: ### 解决方案:强制同步刷新机制 #### 1. **重写 Adapter 的 `getItemPosition` 方法(核心)** ```java public class MyPagerAdapter extends PagerAdapter { // ...其他方法 @Override public int getItemPosition(Object object) { return POSITION_NONE; // 关键!强制所有页面重新加载 } } ``` - **作用**:将返回值设为 `POSITION_NONE` 会强制 ViewPager 销毁并重建所有页面 - **原理**:调用 `notifyDataSetChanged()` 时,ViewPager 会检查每个页面的位置状态[^3] #### 2. **立即触发布局刷新** ```java // 在 Activity/Fragment 中 viewPager.getAdapter().notifyDataSetChanged(); // 立即触发同步布局 viewPager.post(new Runnable() { @Override public void run() { viewPager.requestLayout(); // 强制重新布局 viewPager.invalidate(); // 强制重绘 } }); ``` #### 3. **使用 Handler 同步执行(确保主线程顺序)** ```java new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { adapter.notifyDataSetChanged(); viewPager.setAdapter(adapter); // 重置Adapter强制同步 // 可选:立即测量和布局 viewPager.measure( View.MeasureSpec.makeMeasureSpec(viewPager.getWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(viewPager.getHeight(), View.MeasureSpec.EXACTLY) ); viewPager.layout(viewPager.getLeft(), viewPager.getTop(), viewPager.getRight(), viewPager.getBottom()); } }); ``` ### 完整示例代码 ```java public class MainActivity extends AppCompatActivity { private ViewPager viewPager; private MyPagerAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); viewPager = findViewById(R.id.viewPager); adapter = new MyPagerAdapter(); viewPager.setAdapter(adapter); Button refreshBtn = findViewById(R.id.refreshButton); refreshBtn.setOnClickListener(v -> syncRefreshViewPager()); } // 同步刷新方法 private void syncRefreshViewPager() { // 1. 更新数据源 updateDataSource(); // 2. 通知适配器 adapter.notifyDataSetChanged(); // 3. 强制同步布局和绘制 viewPager.post(() -> { viewPager.requestLayout(); viewPager.invalidate(); // 4. 如果需要精确控制,重置适配器 viewPager.setAdapter(null); viewPager.setAdapter(adapter); }); } class MyPagerAdapter extends PagerAdapter { // ...数据源和构造方法 @Override public int getItemPosition(Object object) { return POSITION_NONE; // 强制刷新关键 } // ...其他必要方法 } } ``` ### 关键原理说明 1. **异步本质**: - `notifyDataSetChanged()` 只是标记数据脏位 - 实际刷新发生在下一个 VSync 信号(16ms周期) 2. **同步化技巧**: - `POSITION_NONE` 强制 ViewPager 重新实例化所有页面[^3] - `requestLayout()+invalidate()` 跳过等待立即刷新 - Handler 确保在主线程顺序执行 3. **性能权衡**: ```mermaid graph LR A[同步刷新需求] --> B{数据变更类型} B -->|页面结构变化| C[使用POSITION_NONE] B -->|仅内容更新| D[直接更新Fragment视图] C --> E[重置Adapter+强制布局] D --> F[避免重建提升性能] ``` ### 注意事项 1. **内存与性能**: - `POSITION_NONE` 会导致频繁重建 Fragment,建议配合 `setOffscreenPageLimit(0)` 减少内存占用 - 对于简单更新,推荐直接操作 Fragment 内部视图而非全局刷新 2. **替代方案**: ```java // 在Fragment中更新视图 Fragment fragment = (Fragment) adapter.instantiateItem(viewPager, position); if(fragment != null) { fragment.updateView(newData); // 自定义更新方法 } ``` 3. **ViewPager2 改进**: ```java // ViewPager2 使用DiffUtil实现高效同步更新 adapter.notifyItemChanged(position, payload); // 局部更新 ``` > **最佳实践**:对于高频更新场景,建议使用 ViewPager2 + `RecyclerView.Adapter` 组合,通过 `DiffUtil` 实现高效增量更新[^1][^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值