目录
前言
哎呀,这个我会。不就是 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 listmfragments; private fragmenttransaction mcurtransaction; private fragmentmanager mfragmentmanger; public viewpagerclearadapter(fragmentmanager fragmentmanager, list fragments) { this(fragmentmanager, fragments, 0); } public viewpagerclearadapter(fragmentmanager fragmentmanager, list 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, private val pagetitles: list ? = null, behavor: int = 0 ) : fragmentstatepageradapter(fragmentmanager, behavor) { private val fragmentmap = mutablemapof () private val fragmentpositions = hashmapof () 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的使用可以看我之前的文章:。
我们再标题栏加一个测试的按钮,查看增删改的功能是否能行?
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(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): 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 的方式做的。
如果大家有类似的使用场景,其实按需选择即可,也不知道大家平时都是怎么做的,如果有更好的方案可以交流一下哦。