错误终结者:Fragment在ViewPager中的正确应用

前言

创作过程:2020年5月22下午4点左右开始写,晚上9点55写下尾声。晚上11点-12点补充第五、第六部分。

有段时间没写文章了,这次不是因为懒...而是的确很忙,最近在重构项目里的一个重要模块。

搞起来真的酸爽,为了策应其他组的模块化,重构的时候也进行了我们的模块化处理,混乱的依赖也是x了狗了....

今天的文章内容是关于ViewPager的,很多同学可能会吐槽:怎么还写这种“低级”的内容!为什么?因为绝大多数的同学都用错了,当然这主要的原因是搜索引擎推出来的文章大多都是错的!

正文

一、错误用法

不知道有多少同学是这样用ViewPager的?

classTestViewPagerActivity: BaseActivity() {
private lateinit var adapter: ViewPagerAdapter
private val fragments = mutableListOf<Fragment>().apply {
        add(TestFragment1.newInstance("页面-1")
        add(TestFragment2.newInstance("页面-2"))
        add(TestFragment3.newInstance("页面-3"))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test_view_pager)
        adapter = ViewPagerAdapter(fragmentData, supportFragmentManager)
        vp.adapter = adapter
}
    inner classViewPagerAdapter(val fragments: List<Fragment>, fm: FragmentManager) : FragmentPagerAdapter(fm) {
override fun getItem(position: Int): Fragment{
return fragments[position]
}
override fun getCount(): Int{
return fragments.size
}
}
}

如果看到这的同学觉得这个用法没什么问题。那么毫无疑问这篇文章你必须要读一读,因为上述的用法完全曲解的Fragment在ViewPager中的应用。

二、正确用法

我猜有同学可能有疑问了,那正确用法是什么样呢?

当然有同学反驳:凭什么你说你的写法是对的呢?这还用问吗?还不是因为我大!!!....Google的文档了:ViewPager

classTestViewPagerActivity: BaseActivity() {
private lateinit var adapter: ViewPagerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test_view_pager)
        adapter = ViewPagerAdapter(fragmentData, supportFragmentManager)
        vp.adapter = adapter
}
    inner classViewPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {
override fun getItem(position: Int): Fragment{
returnwhen(position) {
0-> TestFragment1.newInstance("页面-1")
1-> TestFragment2.newInstance("页面-2")
else-> TestFragment3.newInstance("页面-3")
}
}
override fun getCount(): Int{
return3
}
}
}

大家看出这俩种用法的不同了吗?没错不同点只在于getItem()方法的实现。搞懂getItem()的调用,也就搞懂了Fragment在ViewPager里的正确用法。所以接下来咱们直接上源码直观感受ViewPager的设计 。

三、FragmentPagerAdapter源码

ViewPager对Fragment的支持非常的简单,整体流程:

  1. setAdapter时会基于当前position进行初始化当前Fragment

    1. 接下来会基于mOffscreenPageLimit的值对需要“预加载”的Fragment进行初始化

    1. 初始化该初始化的Fragment之后,调用commit()通知FragmentManager去attach Fragment

    这3步走完,我们当前的Fragment就已经出来了。

    接下来咱们通过源码来具体理解一下上述的1、2、3这几个步骤。

    当我们setAdapter时,会走到popuate方法:

    void populate(int newCurrentItem) { // ViewPager中
    // ....
    // 基于当前position的位置判断Item(Fragment)是否存在来决定,是否要初始化当前的Fragment
    if(curItem == null&& N > 0) {
    // 而这里会走到instantiateItem
            curItem = addNewItem(mCurItem, curIndex);
    }
    // 初始化当前之后,会基于limit,初始化该预加载的....
    // 此方法在FragmentPagerAdapter中会调用fm的commit
        mAdapter.finishUpdate(this);
    }
    /**
     * 这里会调用instantiateItem(),这里真正的实现在FragmentPagerAdapter里
     */
    ItemInfo addNewItem(int position, int index) {
    ItemInfo ii = newItemInfo();
        ii.position = position;
        ii.object = mAdapter.instantiateItem(this, position);
        ii.widthFactor = mAdapter.getPageWidth(position);
    if(index < 0|| index >= mItems.size()) {
            mItems.add(ii);
    } else{
            mItems.add(index, ii);
    }
    return ii;
    }
    

    一直走到这,我们才看到FragmentPagerAdapter对Fragment初始化的控制:

    publicObject instantiateItem(@NonNullViewGroup container, int position) {
    if(mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
    }
    // 基于position找到itemId,这方法的默认实现就是position
    finallong itemId = getItemId(position);
    // 生成一个tag
    String name = makeFragmentName(container.getId(), itemId);
    // 通过上边生成的tag,在fragmentManager中试图找到一个Fragment的实例
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    // 如果找到,直接调用attach
    if(fragment != null) {
    if(DEBUG) Log.v(TAG, "Attaching item #"+ itemId + ": f="+ fragment);
            mCurTransaction.attach(fragment);
    } else{
    // 否则调用getItem(),基于我们自己的实现拿到Fragment实例。
            fragment = getItem(position);
    if(DEBUG) Log.v(TAG, "Adding item #"+ itemId + ": f="+ fragment);
            mCurTransaction.add(container.getId(), fragment,
                    makeFragmentName(container.getId(), itemId));
    }
    if(fragment != mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
    if(mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
    } else{
                fragment.setUserVisibleHint(false);
    }
    }
    return fragment;
    }
    

    代码的注释详细的说明了FragmentPagerAdapter如果基于当前position进行初始化Fragment的逻辑。简单再梳理一遍:

    • 基于一套规则生成的tag,通过findFragmentByTag()来找是否已经生成过Fragment。

    • 如果没有,调用getItem(),拿到我们自己重写后return的Fragment实例。

    因为以上的流程,我们可以明确开篇第一种用法一定错误的!因为从源码我们可以get到一个信息:对于Adapter来说,只有FragmentManage中找不到Fragment实例时才会调用getItem()去初始化Fragment。因此这其实是一种常见的懒加载机制。

    而开篇第一种写,在初始化的时候就把所有Fragment都new了一遍,很明显是无意义的!因为如果我们ViewPager有3个Fragment,用户不滑到第3个Fragment,那么new这个Fragment就是浪费的。

    接下来咱们再聊一聊第2步中的mOffscreenPageLimit,有经验的老铁们都知道这个是用于预加载的,而且这个值最低是1。populate()方法中基于mOffscreenPageLimit来决定预加载position左右俩边多少个Fragment,1就意味着左右各预加载1个。

    由于mOffscreenPageLimit最小是1的原因,所以我们一次至少要加载2个Fragment。而有时我们又偏偏需要在滑动到某个Fragment的时候再执行一些数据加载的操作。

    在面对这种场景下,我们一般都会用onHiddenChanged()/setUserVisibleHint()等方法来尝试做可见性的逻辑回调。其实如果项目中的fragment库版本较新的时候会发现系统提供了更方便且优雅的方式。

    四、更优雅的滑动到当前Fragment时加载数据

    新版本下的fragment,在使用FragmentStatePagerAdapter,我们会发现默认的构造方法是过时的:

    @Deprecated
    publicFragmentStatePagerAdapter(@NonNullFragmentManager fm) {
    this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
    }
    

    会发现系统在构造函数中增加了第二个参数,除了默认BEHAVIORSETUSERVISIBLEHINT的,系统还提供了BEHAVIORRESUMEONLYCURRENTFRAGMENT。而这个行为和它的名字一样,只有在滑动这个Fragment上时才会调这个Fragment的onResume()方法。

    但是注意是回调onResume()。而onResume之前的方法,已经在getItem()中实例化Fragment的时候调完了。

    因此我们仅仅想在当前Fragment可见的时候做初始化操作,可以直接使用BEHAVIORRESUMEONLYCURRENTFRAGMENT。

    五、getItemPosition()滥用

    前段时间在公司项目中,看到有小伙伴重写了getItemPosition()方法:

    publicint getItemPosition(@NonNullObject object) {
    return POSITION_NONE;
    }
    

    这么写有没有问题?说有也有,说没有也没有!为什么这么模棱两可的回答呢?因此这个方法很特殊。

    这个方法的注释是这么说的:return POSITIONUNCHANGED时,意味着当前视图没有发生改变,return POSITIONNONE意味着发生改变。注释可能有些抽象,咱们结合源码来理解这个方法。

    这个方法只会在ViewPager的dataSetChanged()中被调用,因此我们可以确认重写这个方法只会在主动尝试更新ViewPager时生效。

    void dataSetChanged() {
    // for循环所有Fragment,然后基于getItemPosition()返回值判断是否需要remove
    for(int i = 0; i < mItems.size(); i++) {
    finalItemInfo ii = mItems.get(i);
    finalint newPos = mAdapter.getItemPosition(ii.object);
    if(newPos == PagerAdapter.POSITION_UNCHANGED) {
    continue;
    }
    if(newPos == PagerAdapter.POSITION_NONE) {
    // 可以看到,如果是POSITION_NONE,就会remove当前i下的Fragment
    // 省略部分代码
                mItems.remove(i);          
                mAdapter.destroyItem(this, ii.position, ii.object);
    }
    // 省略部分代码
    }
    // 省略部分代码
    // 此方法中会再次调用populate()去重新走初始化的操作
        setCurrentItemInternal(newCurrItem, false, true);
    }
    

    有了上述源码的逻辑,其实我们就能够明白getItemPosition()的意义:当我们想使用notifyDataSetChanged()去刷新ViewPager时,getItemPosition()的返回时决定当前的Fragment是否需要被remove。因此当我们不需要remove当前的Fragment时,则return POSITIONUNCHANGED(这样此Fragment就不会发生任何状态变化),否者则return POSITIONNONE(这样此Fragment就会被remove,然后重新初始化新的Fragment)。我们就可以做出类似于RecyclerView的diff操作。

    基于自身产品逻辑,合理的重写getItemPosition(),避免不必要Fragment的销毁重建。

    六、如何主动get到ViewPager的Fragment实例

    我们都知道,FragmentManager为我们提供了findFragmentById()/findFragmentByTag()。同样对于ViewPager也是如此,在第三部分源码分析的时候,我们知道FragmentPagerAdapter中获取也是通过findFragmentByTag()尝试获取当前Fragment的实例,而tag的实现来自makeFragmentName(container.getId(), itemId)

    privatestaticString makeFragmentName(int viewId, long id) {
    return"android:switcher:"+ viewId + ":"+ id;
    }
    

    所以,我们获取ViewPager中的Fragment也可以借助这种方式。千万不要像搜索引擎里推出的那些答案:主动调用什么getItem()!有了上边源码的分析,我猜大家已经get到这些用法错的是多么离谱!!!

    尾声

    OK,本次想聊的就是这么多~以后的文章,我会力求在绝对正确的情况下再发出来,尽可能的不要误人子弟!

    毕竟就今天的ViewPager而言,其实我一开始也是用那种错误的写法,没错,就是受搜索引擎推出来的错误文章所误导!

    既然自己踩过坑,争取能填上一个是一个!

    推荐阅读:

    移动端技术交流喊你入群啦~~~

    UC浏览器视频播放缓存以及视频下载分析

    推荐几个堪称教科书级别的 Android 音视频入门项目

    觉得不错,点个在看呗~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值