Fragment 知识梳理(4) - FragmentPagerAdapter 和 FragmentStatePagerAdapter 解析

一、概述

在平时的开发当中,用到ViewPager的场景主要是以下两种:

  • 对于主页中的每个子页面,用Fragment包裹起来,然后通过ViewPager来实现页面之间的切换。
  • 广告轮播图。

其中对于第一种情况,我们常常会使用到两个PagerAdapter的实现类,也就是FragmentStatePagerAdapterFragmentPagerAdapter,今天,我们就来学习一下它们的使用方法,并进行对比。

二、FragmentPagerAdapter

2.1 使用

在我们的例子中,我们定义了一个Acitivity,它的布局中包含有一个ViewPager。初始时候我们会给mFragments列表中新建4Fragment实例,然后把它传给继承于FragmentPagerAdapter的适配器,LogcatFragment就是用来打印Fragment的生命周期:

    private void initFPAFragments() {
        mFragments = new ArrayList<>();
        for (int i = 0; i < INCREASE; i++) {
            //初始时刻有4个Fragment,每个Fragment和一条数据相关联.
            mFragments.add(LogcatFragment.newInstance("index=" + i));
        }
        ViewPager viewPager = (ViewPager) findViewById(R.id.vp_content);
        mFPAdapter = new FPAdapter(getSupportFragmentManager(), mFragments);
        viewPager.setAdapter(mFPAdapter);
    }

    private class FPAdapter extends FragmentPagerAdapter {

        private List<Fragment> mFragments;

        public FPAdapter(FragmentManager fm, List<Fragment> fragments) {
            super(fm);
            mFragments = fragments;
        }

        @Override
        public Fragment getItem(int position) {
            Log.d("LogcatFragment", "get Item from FPAdapter, position=" + position);
            return mFragments.get(position);
        }

        @Override
        public int getCount() {
            return mFragments.size();
        }

    }

2.2 现象

使用过ViewPager的同学都知道,ViewPager有一个setOffscreenPageLimit,它表示对于当ViewPager处于IDLE状态时,它的左右两端最多会保留多少个页面,对于超出这个范围的页面有可能会需要从PageAdapter中进行重建,这里我们设置的是1,下面我们进行一系列的操作,并观察此时各个页面及其内部的Fragment的变化情况:

  • 第一步:当我们第一次启动Activity的时候,默认会添加它的左右两个界面,由于我们位于第一个(index=0),因此会添加它及其右边的界面(index=0),此时这两个页面当中内部的Fragment的生命周期如下图所示:

    从我们经常看到的Fragment生命周期的图来看,就是下面红色的部分:
  • 第二步:下面,我们滑动到index=1的界面,此时index=2的页面会被添加,它内部的Fragment所走的生命周期和上面完全相同,由于index=1左右两边的界面个数都为1,因此不会有页面被移除。
  • 第三步:继续往右滑动到index=2的界面,此时会添加index=3的页面,并移除index=0的页面,其内部包含的Fragment的生命周期打印为:

    可以看到对于添加的index=3的页面而言,它内部的Fragment所走的生命周期和index=0/1/2相同,而被移除的index=0的页面内部的Fragment所走的生命周期为:
  • 第四步:向右滑动到index=1的界面,此时index=0的界面需要被重新添加,而index=3的界面则需要被移除,此时的打印为:

    这时候,对于重新添加的页面index=0,它和第一次添加的时候有两点不同:
  • 没有再去自定义的FragmentPagerAdapter中取Fragment
  • 其内部的Fragment所走的生命周期不同,此时为:

最后,我们总结一下,对于三种情况的页面内部的Fragment所走生命周期的区别如下图所示:

  • 第一次添加的页面
  • 重新添加的页面
  • 移除的页面

2.3 源码解析

现在,我们就开始解释一下,为什么第一次添加重新添加的页面内部对应的Fragment会有所不同,我们只需要关注FragmentPagerAdapter内的两个函数:

  • public Object instantiateItem(ViewGroup container, int position),添加页面时回调。
  • public void destroyItem(ViewGroup container, int position, Object object),移除页面时回调。
    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        //这里的itemId返回的是对应position的页面的唯一标识符.
        final long itemId = getItemId(position);
        //1.先是通过FragmentManager来找.
        String name = makeFragmentName(container.getId(), itemId);
        Fragment fragment = mFragmentManager.findFragmentByTag(name);
        //2.如果找到了,那么调用attach方法.
        if (fragment != null) {
            if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
            mCurTransaction.attach(fragment);
        } else {
            //3.如果没找到,那么通过子类实现的getItem方法来获取.
            fragment = getItem(position);
            //这里调用的是add方法.
            mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));
        }
        //根据需要,回调下面这两个方法.
        if (fragment != mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }
        //返回给ViewPager.
        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        //4.移除界面时,调用的是detach方法.
        mCurTransaction.detach((Fragment)object);
    }

注意看上面的注释,就能解释上面我们看到的现象了:

  • 第一次添加界面的时候,由于FragmentManager中没有这个Fragment,因此需要通过自定义的FragmentPagerAdapter获取,然后调用add方法,也就是上面代码中的第(3)步,所走的生命周期为onAttach() -> onResume()
  • 移除界面时,使用的是detach方法,也就是上面代码中的第(4)步,接触过Fragment的人都知道,这时候仅仅是Fragment的界面被从View树上移除了而已,它的实例仍然被保存在FragmentManager当中,所走的生命周期为onPause() -> onDestroyView()
  • 重新添加界面时,由于此时去FragmentManager中能找到那个Fragment,所以调用的是attach方法,也就是上面代码中的第(2)步,所走的生命周期为onCreateView() -> onResume(),并且不需要再从自定义的FragmentPagerAdapter中获取Fragment

整个逻辑如下图所示:


三、FragmentStatePagerAdapter

3.1 现象

我们的代码基本不用改动,只需要把原来继承于FragmentPagerAdapter的子类替换为继承FragmentStatePagerAdapter就可以了。在第二章当中,我们分析得很详细,相信大家对于整个分析的套路已经理解,因此,为了减少篇幅,我们直接说结论,当进行和上面相同的操作之后,把页面分为三种类型:

  • 第一次添加
  • 重新添加
  • 移除

此时,它们内部的Fragment所走的生命周期为:

对于重新添加移除的界面,其内部的Fragment所走的生命周期都和FragmentPagerAdapter不同,下面,我们就从源码的角度,来看一下导致这些区别的原因。

3.2 源码解析

和前面类似,我们只关注添加和移除时调用的那两个方法:

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        //如果mFragments中存在对应位置的fragment,那么直接返回.
        if (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
            }
        }

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        Fragment fragment = getItem(position);
        //多了恢复状态的操作.
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }
       //保证mFragments的大小和ViewPager往右滑动的最远的index相同.
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
        //设置对应位置.
        mFragments.set(position, fragment);
        //这里很关键,调用的add方法.
        mCurTransaction.add(container.getId(), fragment);

        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment) object;
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        while (mSavedState.size() <= position) {
            mSavedState.add(null);
        }
        mSavedState.set(position, fragment.isAdded() ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
        mFragments.set(position, null);
        //调用的是remove方法.
        mCurTransaction.remove(fragment);
    }

从上面的源码当中,总结出以下几点:

  • FragmentStatePagerAdapter移除页面的时候,调用的是remove方法,也就是说,FragmentManager中不再有这个Fragment的实例,所走的生命周期为onPause() -> onDetach()
  • 无论是添加页面还是重新添加页面,它是通过add方法,并且每次都会通过自定义的FragmentStatePagerAdapter子类的getItem方法来获取Fragment,所以它们内部的Fragment所走生命周期相同,都是从onAttach() -> onResume()
  • 对于ViewPager当前界面中所对应的Fragment,是通过一个mFragments列表来管理的,由于此时没有FragmentManager来帮我们实现Fragment集合的状态的保存和恢复,所以就需要我们自己实现onSave/onRestore方法来进行状态的保存和恢复。

整个流程如下图所示:


四、总结

FragmentPagerAdapterFragmentStatePagerAdapter最大的区别就在于前者会把所有Fragment的示例都缓存在内存当中,而后者仅仅保存了ViewPager当前存在的页面所对应的Fragment,当页面被移除之后,这个Fragment的示例它也就不再保存了。
当然,在我们前面的例子中,虽然使用了FragmentStatePagerAdapter,但是由于我们在DemoActivity中用一个列表保存了所有的Fragment实例,因此它没有被回收,如果希望让页面被移除的时候,其对应的Fragment实例也被回收,那么我们的FragmentStatePagerAdapter的子类应该写成这样:

    private void initFSPAFragments() {
        ViewPager viewPager = (ViewPager) findViewById(R.id.vp_content);
        mFSPAdapter = new FSPAdapter(getSupportFragmentManager());
        viewPager.setAdapter(mFSPAdapter);
    }

    private class FSPAdapter extends FragmentStatePagerAdapter {

        public FSPAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            Log.d("LogcatFragment", "get Item from FSPAdapter, position=" + position);
            return LogcatFragment.newInstance("index=" + position);
        }

        @Override
        public int getCount() {
            return INCREASE;
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容