欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

Android ViewPager你可能不知道的刷新操作分享,

来源: 开发者 投稿于  被查看 1472 次 评论:182

Android ViewPager你可能不知道的刷新操作分享,


目录
  • 前言
  • 一、清缓存重置Adapter的方案
  • 二、TabView+ViewPager的差分刷新
    • 2.1 使用arguments的方式
    • 2.2 使用Tag的方式
  • 三、自定义Tab或第三方Tab
    • 四、ViewPager2的区别
      • 总结

        前言

        哎呀,这个我会。不就是 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<Int, Int>()
            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
                }

        添加的效果:

        指定位置替换Fragment效果:

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

        删除指定的数据:

        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 效果。

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

                //自定义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, 0f, true)
                    }
                    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) {}
                })

        效果:

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

           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不会刷新的问题:

        自行处理Tab之后的效果:

        不管是第三方的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[1] = Lazy2Fragment1.obtainFragment()
        //            titles[1] = "Refresh2"
        //            mBinding.viewPager2.adapter?.notifyItemChanged(1)
                    //删除并刷新
                    fragments.removeAt(2)
                    mBinding.viewPager2.adapter?.notifyItemRemoved(2)
                    mBinding.viewPager2.adapter?.notifyItemRangeChanged(2, 1)
                }

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

        可以看到一些效果如下:

        真是简单又方便!

        话是这么说,但是感觉 ViewPager2 的风评并不是很好的样子,很多伙伴反馈有一些小问题。总是踩在坑上,不知道大家都是怎么选择的呢?

        总结

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

        那么在实际开发的过程中,我们其实可以区分场景,如果是静态的ViewPager,数量不可变的,可以直接用简单的数据适配器来实现,而如果是可变的ViewPager,大家可以区分三种情况来使用,都是可行的。

        我个人来说之前都是用清除缓存的方式,后来用的是修改 Fragment 的 argument 的方式做的。

        如果大家有类似的使用场景,其实按需选择即可,也不知道大家平时都是怎么做的,如果有更好的方案可以交流一下哦。

        到此这篇关于Android ViewPager你可能不知道的刷新操作分享的文章就介绍到这了,更多相关Android ViewPager刷新内容请搜索3672js教程以前的文章或继续浏览下面的相关文章希望大家以后多多支持3672js教程!

        您可能感兴趣的文章:
        • viewPager+fragment刷新缓存fragment的方法
        • Android使用ViewPager实现翻页效果
        • ViewPager实现图片切换效果
        • TabLayout+ViewPager2的简单使用详解
        • ViewPager实现轮播图引导页
        • Android ViewPager2 使用及自定义指示器视图实现

        用户评论