ViewPager的改写操作

我正在参与「启航计划」

前言

哎呀,这个我会。不便是 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
        }

增加的作用:

你可能不知道的ViewPager刷新操作

指定方位替换Fragment作用:

你可能不知道的ViewPager刷新操作

回转调集,应该是第一个和第三个Fragment需求重载:

你可能不知道的ViewPager刷新操作

删去指定的数据:

你可能不知道的ViewPager刷新操作

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 作用。

你可能不知道的ViewPager刷新操作

所以咱们一般都是手动的监听完成作用:


        //自界说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) {}
        })

作用:

你可能不知道的ViewPager刷新操作

那么增修正的操作又有什么差异呢?

   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不会改写的问题:

你可能不知道的ViewPager刷新操作

自行处理Tab之后的作用:

你可能不知道的ViewPager刷新操作

不管是第三方的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 之类的代码。

能够看到一些作用如下:

你可能不知道的ViewPager刷新操作

你可能不知道的ViewPager刷新操作

真是简略又方便!

2023-05-23更新 ViewPager2的改写

但是我并没有写 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()
    }

改写作用如下:

你可能不知道的ViewPager刷新操作

完美处理!

总结

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

那么在实际开发的过程中,咱们其实能够区分场景来运用,假如是静态的 ViewPager ,数量不可变的,能够直接用简略的数据适配器来完成,不需求重写 Fragment 标识之类的逻辑。

而假如是可变的 ViewPager ,咱们用ViewPager的几种办法都是可行的,假如是 ViewPager2 注意要差异的界说办法。

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

好了,也不知道大家平时都是怎样做的,假如有更好的计划能够沟通一下哦。

关于本文的内容假如想查看源码能够点击这儿 【传送门】。你也能够重视我的这个Kotlin项目,我有时间都会继续更新。

惯例,我如有讲解不到位或讹夺的地方,希望同学们能够指出或评论区沟通一下。

假如感觉本文对你有一点点的启发,还望你能点赞支撑一下,你的支撑是我最大的动力。

Ok,这一期就此结束。

你可能不知道的ViewPager刷新操作