技巧:如何高效刷新ViewPager?

共 32676字,需浏览 66分钟

 ·

2023-07-31 13:14

前言

不就是 mViewPagerAdapter.notifyDataSetChanged(); 嘛,简单!

这个可能真不是那么简单,我们以常用的 ViewPager + Fragment 的使用为例。你调用 notifyDataSetChanged 刷新方法,会走到 getItemPosition 方法中查询当前Item是否需要刷新,而它的默认实现是:

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

永远标记不刷新,那么不管你是添加Pager,删除Pager,改变Pager都是「不生效」的。

那有些同学就会说了,每次刷新还要做差分?我直接一把梭,直接重新设置一个 Adapter 不就万事大吉了?

反正每次接口数据回来都重新设置 Adapter ,还管什么性能不性能,效果实现了再说!

    mViewPager.setAdapter(null);
    mViewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager(),mFragmentList);
    mViewPager.setAdapter(mViewPagerAdapter);

    mViewPager.setOffscreenPageLimit(mFragmentList.size() - 1);
        

但是就算如此也是有问题的,当我们一个页面中根据不同的筛选条件,服务端返回不同数量的数组,我们就要展示不同数量的 ViewPager 如果这样刷新 ViewPager 就可能出现显示问题。

怎么解决?几种方案,接下来往下看:

一、清缓存重置Adapter的方案

如果除开性能问题,想直接每次直接替换一个 Adapter 其实也是可行的,如果替换之后显示的还是之前的页面,或者显示的索引不对,大概率是 ViewPager 之前缓存的 Fragment 没有清掉的。

所以我们需要自定义一个 Adapter , 在里面定义清除缓存的方法,每次设置 Adapter 之前就调用清除缓存之后再设置 Adapter 。

直接上代码:

    /**
     *  可以清除缓存的ViewPager
     */
    public class ViewPagerClearAdapter extends FragmentPagerAdapter {

        private List<Fragment> mFragments;
        private FragmentTransaction mCurTransaction;
        private FragmentManager mFragmentManger;

        public ViewPagerClearAdapter(FragmentManager fragmentManager, List<Fragment> fragments) {
            this(fragmentManager, fragments, 0);
        }

        public ViewPagerClearAdapter(FragmentManager fragmentManager, List<Fragment> fragments, int behavor) {
            super(fragmentManager, behavor);
            mFragments = fragments;
            mFragmentManger = fragmentManager;
        }

        @Override
        public Fragment getItem(int position) {
            return mFragments.get(position);
        }

        @Override
        public int getCount() {
            return mFragments.size() == 0 ? 0 : mFragments.size();
        }

        /**
         * 清除缓存fragment
         *
         * @param container ViewPager
         */
        public void clear(ViewGroup container) {
            if (this.mCurTransaction == null) {
                this.mCurTransaction = mFragmentManger.beginTransaction();
            }

            for (int i = 0; i < mFragments.size(); i++) {

                long itemId = this.getItemId(i);
                String name = makeFragmentName(container.getId(), itemId);
                Fragment fragment = mFragmentManger.findFragmentByTag(name);
                if (fragment != null) {//根据对应的ID,找到fragment,删除
                    mCurTransaction.remove(fragment);
                }
            }
            mCurTransaction.commitNowAllowingStateLoss();
        }

        /**
         * 等同于FragmentPagerAdapter的makeFragmentName方法,
         */
        private static String makeFragmentName(int viewId, long id) {
            return "android:switcher:" + viewId + ":" + id;
        }

使用的时候,先清除再设置即可:

           if (mViewPagerAdapter!=null){
                mViewPagerAdapter.clear(mViewPager);
            }

            mViewPager.setAdapter(null);
            mViewPagerAdapter = new ViewPagerClearAdapter(getChildFragmentManager(),mFragmentList);
            mViewPager.setAdapter(mViewPagerAdapter);

            if (mFragmentList.size() > 1) {
                mViewPager.setOffscreenPageLimit(mFragmentList.size() - 1);
            }

这样也算是间接的实现了刷新功能,但是有点傻,RecyclerView 感觉到暴怒,那么有没有类似 RecyclerView 那样的智能刷新呢?

二、TabView+ViewPager的差分刷新

前言中我们说到 ViewPager 的 notifyDataSetChanged 刷新方法,会走到 getItemPosition 方法,而内部的默认实现是不做刷新。

而重点的 getItemPosition 其实就是在 notifyDataSetChanged 执行的时候拿到当前的 Item 集合做的遍历操作,让每一个 Item 都去自行判断你有没有变化。

那么难点就是如何判断当前的对象或索引位置有没有变化呢?

2.1 使用arguments的方式

在 ViewPagerAdapter 中我们可以重写方法 instantiateItem 表示每次创建 Fragment 的时候执行,创建一个 Fragment 对象。

由于内部默认实现并没有添加 Tag ,所以我们可以通过调用 super的方式拿到 fragment 对象,给他设置一个参数,并记录每一个 Fragment 对应的索引位置。

然后我们再判断 Fragment 是否需要刷新的时候,拿到对应的参数,并获取当前 Fragment 的索引,判断Fragment有没有变化,索引有没有变化。

当都没有变化,说明此 Fragment 无需刷新,就返回 POSITION_UNCHANGED ,如果要刷新就返回 POSITION_NONE 。

如果返回 POSITION_NONE ,就会走到 destroyItem 的回调,会销毁 Framgent,如果有需要会重新创建新的 Fragment 。

完整的代码实现如下:

class ViewPagerFragmentAdapter @JvmOverloads constructor(
    private val fragmentManager: FragmentManager,
    private val fragments: List<Fragment>,
    private val pageTitles: List<String>? = null,
    behavor: Int = 0
) : FragmentStatePagerAdapter(fragmentManager, behavor) {

    private val fragmentMap = mutableMapOf<Int, Fragment>()
    private val fragmentPositions = hashMapOf<IntInt>()

    init {
        for ((index, fragment) in fragments.withIndex()) {
            fragmentMap[index] = fragment
        }
    }

    override fun getItem(position: Int): Fragment {
        return fragments[position]
    }

    override fun getCount()Int {
        return if (fragments.isEmpty()) 0 else fragments.size
    }

    override fun getPageTitle(position: Int): CharSequence? {
        return if (pageTitles == null"" else pageTitles[position]
    }

    override fun instantiateItem(container: ViewGroup, position: Int): Any {
        YYLogUtils.w("ViewPagerFragmentAdapter-instantiateItem")

        val fragment = super.instantiateItem(container, position) as Fragment

        val id = generateUniqueId()
        var args = fragment.arguments
        if (args == null) {
            args = Bundle()
        }

        args.putInt("_uuid", id)
        fragment.arguments = args

        // 存储 Fragment 的位置信息
        fragmentPositions[id] = position

        return fragment
    }

    private fun generateUniqueId()Int {
        // 生成唯一的 ID
        return UUID.randomUUID().hashCode()
    }

    override fun destroyItem(container: ViewGroup, position: Int, obj: Any) {
        super.destroyItem(container, position, obj)
    }

    override fun getItemPosition(obj: Any)Int {
        YYLogUtils.w("ViewPagerFragmentAdapter-getItemPosition")

        val fragment = obj as Fragment

        // 从 Fragment 中获取唯一的 ID
        val args = fragment.arguments
        if (args != null && args.containsKey("_uuid")) {
            val id = args.getInt("_uuid")

            // 根据 ID 获取 Fragment 在 Adapter 中的位置
            val position = fragmentPositions[id]
            return if (position != null && position == fragments.indexOf(fragment)) {
                // Fragment 未发生变化,返回 POSITION_UNCHANGED
                POSITION_UNCHANGED
            } else {
                // Fragment 发生变化,返回 POSITION_NONE
                POSITION_NONE
            }
        }

        // 如果不是 Fragment,则返回默认值
        return super.getItemPosition(obj)
    }


}

使用起来很简单,我们这里使用默认的TabView + ViewPager + 懒加载Fragment来看看效果:

        val fragments = mutableListOf(LazyLoad1Fragment.obtainFragment(), LazyLoad2Fragment.obtainFragment(), LazyLoad3Fragment.obtainFragment());
        val titles = mutableListOf("Demo1""Demo2""Demo3");
        val adapter = ViewPagerFragmentAdapter(supportFragmentManager, fragments, titles)

        override fun init() {
            //默认的添加数据适配器
            mBinding.viewPager.adapter = adapter
            mBinding.viewPager.offscreenPageLimit = fragments.size - 1

            mBinding.tabLayout.setupWithViewPager(mBinding.viewPager)
        }

我们这里使用的是原始懒加载的方案,关于每一种懒加载Fragment的使用可以看我之前的文章: Fragment懒加载的几种方式与性能对比。

我们再标题栏加一个测试的按钮,查看增删改的功能是否能行?

        mBinding.easyTitle.addRightText("刷新") {
            //添加并刷新
//            fragments.add(LazyLoad1Fragment.obtainFragment())
//            titles.add("Demo4")

            //更新指定位置并刷新
//            fragments[2] = LazyLoad2Fragment.obtainFragment()
//            titles[2] = "Refresh1"

            //反转换位置呢
//            fragments.reverse()
//            titles.reverse()

            //删除并刷新
            fragments.removeAt(2)
            titles.removeAt(2)

            mBinding.viewPager.adapter?.notifyDataSetChanged()
            mBinding.viewPager.offscreenPageLimit = fragments.size - 1
        }

添加的效果:

图1.gif

指定位置替换Fragment效果:

图2

反转集合,应该是第一个和第三个Fragment需要重载:

图3

删除指定的数据:

图4

2.2 使用Tag的方式

而使用 Tag 的方式替换其实是类似的道理,需要在创建 Fragment 的时候绑定 tag ,在查询是否需要刷新的方法中需要拿到tag进行判断:

            Fragment fragment = getItem(position);
            FragmentTransaction ft = ((FragmentActivity) mContext).getSupportFragmentManager().beginTransaction();
            ft.add(R.id.viewpager, fragment, "fragment" + position);
            ft.attach(fragment);
            ft.commit();

<!---->

        @Override
        public int getItemPosition(@NonNull Object object) {
            if (object instanceof Fragment) {
                Fragment fragment = (Fragment) object;
                Integer position = fragmentMap.get(fragment.getTag());
                if (position != null && position == fragments.indexOf(fragment)) {
                    // Fragment 未发生变化,返回 POSITION_UNCHANGED
                    return POSITION_UNCHANGED;
                } else {
                    // Fragment 发生变化,返回 POSITION_NONE
                    return POSITION_NONE;
                }
            }
            // 如果不是 Fragment,则返回默认值
            return super.getItemPosition(object);
        }

这里就不做过多的介绍,如果是简单的操作也是是可行的。只是需要重写创建Fragment流程。

由于我自用的并不是 Tag 的方式,因为并不想修改内部的创建 Fragment 方式,毕竟内部还涉及到 SavedState 与 BEHAVIOR 的一些处理。

如果你感兴趣可以自行实现!

三、自定义Tab或第三方Tab

如果我们用到一些自定义Tab的样式,或者使用一些第三方的TabLayout,那么我们该如何做?

CustomTabView 还能绑定到 ViewPager 吗?如果要做刷新又该如何操作?

例如我们使用自定义的Tab样式:


  override fun init() {

        titles.forEach {
             addTab(it)
        }

        mBinding.viewPager.adapter = adapter
        mBinding.viewPager.offscreenPageLimit = fragments.size - 1

        //自定义Tab不能这么设置了?
        mBinding.tabLayout.setupWithViewPager(mBinding.viewPager)

  }

    private fun addTab(content: String) {
        val tab: TabLayout.Tab = mBinding.tabLayout.newTab()
        val view: View = layoutInflater.inflate(R.layout.tab_custom_layout, null)
        tab.customView = view

        val textView = view.findViewById<TextView>(R.id.tab_text)
        textView.text = content

        mBinding.tabLayout.addTab(tab)
    }

是可以运行,但是不能用 setupWithViewPager 方式,如果用这种方式会默认给设置原生默认的 TabView 。而没有自定义 View 效果。

图5

所以我们一般都是手动的监听实现效果:


        //自定义Tab不能这么设置了?
//        mBinding.tabLayout.setupWithViewPager(mBinding.viewPager)

       // 需要手动的写监听绑定
        mBinding.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
            override fun onPageScrolled(i: Int, v: Float, i1: Int) {}
            override fun onPageSelected(i: Int) {
                mBinding.tabLayout.setScrollPosition(i, 0ftrue)
            }

            override fun onPageScrollStateChanged(i: Int) {}
        })

        mBinding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
            override fun onTabSelected(tab: TabLayout.Tab) {
                val position = tab.position
                mBinding.viewPager.setCurrentItem(position, true)
            }

            override fun onTabUnselected(tab: TabLayout.Tab) {}
            override fun onTabReselected(tab: TabLayout.Tab) {}
        })

效果:

图7

那么增删改的操作又有什么区别呢?

       mBinding.easyTitle.addRightText("Refresh") {
                //添加并刷新
                fragments.add(LazyLoad1Fragment.obtainFragment())
                titles.add("Demo4")
                addTab("Demo4")

                //删除并刷新
                fragments.removeAt(2)
                titles.removeAt(2)
                mBinding.tabLayout.removeTabAt(2)

                mBinding.viewPager.adapter?.notifyDataSetChanged()
                mBinding.viewPager.offscreenPageLimit = fragments.size - 1
            }

由于没有 setupWithViewPager 的方式绑定,所以当ViewPager变化之后我们需要手动的自己处理Tab相关的赋值与删除等操作:

否则会出现,ViewPager刷新了,但TabView不会刷新的问题:

图8

自行处理Tab之后的效果:

图9

不管是第三方的TabLayout,还是自定义的TabView,相比原生默认的 TabView 使用操作还是要复杂上一点。

四、ViewPager2的区别

而TabView + ViewPager2 + 懒加载Fragment 就更简单啦,都是基于RV实现的,我们可以直接调用RV的刷新方法。

  override fun init() {
        mBinding.viewPager2.bindFragment(
            supportFragmentManager,
            this.lifecycle,
            fragments,
        )

        TabLayoutMediator(mBinding.tabLayout, mBinding.viewPager2) { tab, position ->
            //回调
            tab.text = titles[position]
        }.attach()
    }

内部数据适配器的Adapter:

    /**
     * 给ViewPager2绑定Fragment
     */
    fun ViewPager2.bindFragment(
        fm: FragmentManager,
        lifecycle: Lifecycle,
        fragments: List<Fragment>
    ): ViewPager2 {
        offscreenPageLimit = fragments.size - 1

        adapter = object : FragmentStateAdapter(fm, lifecycle) {
            override fun getItemCount(): Int = fragments.size
            override fun createFragment(position: Int): Fragment = fragments[position]
        }
        return this
    }

后面我们给它加上一些操作方法:

mBinding.easyTitle.addRightText("Refresh2") {
    //添加并刷新
//    titles.add("Demo4")
//    fragments.add(Lazy2Fragment1.obtainFragment())
//    mBinding.viewPager2.adapter?.notifyItemInserted(fragments.size-1)


    //删除并刷新
    fragments.removeAt(2)
    mBinding.viewPager2.adapter?.notifyItemRemoved(2)
    mBinding.viewPager2.adapter?.notifyItemRangeChanged(2, 1)
}

可以看到我们是直接使用RV的Apdater来操作的,也就不需要魔改一些 Adapter 之类的代码。

可以看到一些效果如下:

图10
图11

真是简单又方便!

但是我并没有写 View ager2 的 replace 刷新逻辑,因为并不会销毁之前的 Fragment 并重建新的 Fragment 。这是由于Adapter中不能判断当前新的 Fragmet 与 之前的 Fragment 是不是同一个 Fragment。由于都是 Fragment 对象 Adapter 无法判断是否刷新就不会刷新,从而导致刷新数据时并不会销毁旧的 Fragment 并创建新的 Fragment。这一点与 ViewPager 的刷新方式是的类似的。

如何在 ViewPager2 的 Adapter 中判断是否需要刷新呢?

ViewPager 的 Adapter 是根据全部Item来遍历 getItemPosition 来标记是否刷新,而 ViewPager2 的 Adapter 是用 getItemId 的方式来标记 Fragment 的ID。

如何定义它的值呢?

getItemId 返回的是一个 Long 值,你可以自定义传进来

private val mFragments = mutableListOf<Pair<Long, MyFragment>>()

override fun createFragment(position: Int): Fragment {
mFragments[position].second 


override fun getItemCount(): Int {
return mFragments.size 


override fun getItemId(position: Int): Long {  mFragments[position].first 
}

当然,你也可以自己制定,比如我偷懒就用 Fragment 的 Name 作为标识,当 Fragment 替换了,它的 Name 就变化了,那么 Adapter 就能标识是否需要刷新。

ViewPager2 的数据适配器:

    class MyPager2Adapter(
        fm: FragmentManager,
        lifecycle: Lifecycle,
        private val fragments: List<Fragment>
    ) : FragmentStateAdapter(fm, lifecycle) {

        override fun getItemCount(): Int = fragments.size

        override fun createFragment(position: Int): Fragment {
            return fragments[position]
        }

        override fun getItemId(position: Int): Long {
           val name = fragments[position].javaClass.simpleName+ position
            val toLong = name.hashCode().toLong()
       
            return toLong
        }
    }

使用的时候如同 RV 一样使用,就能自动刷新指定的 Fragment 了:

    val mAdapter = MyPager2Adapter(supportFragmentManager, this.lifecycle, fragments)

    override fun init() {
    
        mBinding.easyTitle.addRightText("Refresh2") {

            //更新指定位置并刷新
            fragments[1] = Lazy2Fragment3.obtainFragment()
            titles[1] = "Refresh2"
            mAdapter.notifyItemChanged(1)

        }


        mBinding.viewPager2.adapter = mAdapter
        mBinding.viewPager2.offscreenPageLimit = fragments.size - 1

        TabLayoutMediator(mBinding.tabLayout, mBinding.viewPager2) { tab, position ->
            //回调
            tab.text = titles[position]
        }.attach()
    }

刷新效果如下:

图12

完美解决!

总结

在本文中我们可以回顾一下 ViewPager 的用法,Fragment 的懒加载用法,重要的是可变 ViewPager 的情况下如何操作。以及 ViewPager2 的不同点。

那么在实际开发的过程中,我们其实可以区分场景来使用,如果是静态的 ViewPager ,数量不可变的,可以直接用简单的数据适配器来实现,不需要重写 Fragment 标识之类的逻辑。

而如果是可变的 ViewPager ,我们用ViewPager的几种方法都是可行的,如果是 ViewPager2 注意要区别的定义方法。

本文只是重点演示了 ViewPager + Fragment 的场景,ViewPager / ViewPager2 内部 Item 不是 Fragmet 也行的,还更简单,思路其实是一样的。

投稿作者:Newki,原文链接:https://juejin.cn/post/7234060427411603511


「点击关注,Carson每天带你学习一个Android知识点。」

最后福利:学习资料赠送

  • 福利:本人亲自整理的「Android学习资料」
  • 数量:10名
  • 参与方式:「点击右下角”在看“并回复截图到公众号,随机抽取」

    点击就能升职、加薪水!




浏览 489
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报