Fragment 知识梳理(3) - FragmentPagerAdapter 和 FragmentStatePagerAdapter 的数据更新问题

一、概述

在上一篇文章中,我们通过源码的角度了解FragmentPagerAdapterFragmentStatePagerAdapter的原理。这其实是为我们分析数据更新问题做一个铺垫。
在实际的开发当中,我们在ViewPager中嵌套的Fragment中并不是固定不变的,需要动态地添加和删除,下面我们就从几个大家经常会遇到的问题入手,然后分析问题的原因,最后我们尝试总结一种比较好的数据更新方式。

二、使用FragmentPagerAdapter

2.1 一段有问题的代码

public class DemoActivity extends AppCompatActivity {

    private static final int INCREASE = 4;
    private FPAdapter mFPAdapter;
    private List<String> mTitles;
    private int mGroup = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_not_update);
        TextView updateTv = (TextView) findViewById(R.id.tv_update);
        updateTv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                updateFragments();
            }
        });
        initFPAFragments();
    }

    private void initFPAFragments() {
        mTitles = new ArrayList<>();
        for (int i = 0; i < INCREASE; i++) {
            mTitles.add("index=" + i + ",group=0");
        }
        ViewPager viewPager = (ViewPager) findViewById(R.id.vp_content);
        mFPAdapter = new FPAdapter(getSupportFragmentManager(), mTitles);
        viewPager.setAdapter(mFPAdapter);
    }

    private void updateFragments() {
        mTitles.clear();
        mGroup++;
        for (int i = 0; i < INCREASE; i++) {
            mTitles.add("index=" + i + ",group=" + mGroup);
        }
        mFPAdapter.notifyDataSetChanged();
    }

    private class FPAdapter extends FragmentPagerAdapter {

        private List<String> mTitles;

        public FPAdapter(FragmentManager fm, List<String> titles) {
            super(fm);
            mTitles = titles;
        }

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

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

    }

}

之所以会写出这样的代码,很大一部分是受到我们平时写ListViewBaseAdapter的影响,因为我们浅意识地认为,调用了notifyDataSetChanged()方法之后,ViewPager就会去调用getItem方法来获取新的Fragment以替换旧的Fragment,就好像我们使用ListVIew的时候,它会去回调BaseAdaptergetView方法来获取新的View一样,运行上面的Demo,会有发现以下几个问题:

  • 第一个问题:调用notifyDataSetChanged()之后,ViewPager当前存在的页面中的Fragment不会发生变化。
  • 第二个问题:对于重新添加的界面,不会回调getItem来获取新的Fragment

2.2 原因分析 - 问题一

我们首先分析问题一:调用notifyDataSetChanged()之后,ViewPager当前存在的页面中的Fragment不会发生变化。

首先,我们确定分析的场景:启动DemoActivity,按照之前的分析,现在会给ViewPager添加两个页面,分别是index=0index=1,接着我们调用PagerAdapter#notifyDataSetChanged(),最终会走到ViewPager#dataSetChanged方法,我们看一下里面做了什么:

    void dataSetChanged() {
        //在我们的例子中,返回的是4
        final int adapterCount = mAdapter.getCount();
        mExpectedAdapterCount = adapterCount;
        //此时为true.
        boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1
                && mItems.size() < adapterCount;
        int newCurrItem = mCurItem;
        boolean isUpdating = false;
        //遍历列表,这个
        for (int i = 0; i < mItems.size(); i++) {
            final ItemInfo ii = mItems.get(i);
            //这里是关键,默认都是返回POSITION_UNCHANGED
            final int newPos = mAdapter.getItemPosition(ii.object);
            //第一种情况:如果返回的是POSITION_UNCHANGED,那么表示这个界面在ViewPager中的位置没有变,那么不需要更新.
            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
                continue;
            }
            //第二种情况:如果返回的是POSITION_NONE,就表示这个界面在ViewPager中不存在了,那么就把它移除.
            if (newPos == PagerAdapter.POSITION_NONE) {
                mItems.remove(i);
                i--;

                if (!isUpdating) {
                    mAdapter.startUpdate(this);
                    isUpdating = true;
                }

                mAdapter.destroyItem(this, ii.position, ii.object);
                needPopulate = true;

                if (mCurItem == ii.position) {
                    // Keep the current item in the valid range
                    newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
                    needPopulate = true;
                }
                continue;
            }
           //第三种情况:界面仍然存在,但是其在ViewPager中的位置发生了改变.
            if (ii.position != newPos) {
                if (ii.position == mCurItem) {
                    // Our current item changed position. Follow it.
                    newCurrItem = newPos;
                }

                ii.position = newPos;
                needPopulate = true;
            }
        }

        if (isUpdating) {
            mAdapter.finishUpdate(this);
        }

        Collections.sort(mItems, COMPARATOR);

        if (needPopulate) {
            // Reset our known page widths; populate will recompute them.
            final int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (!lp.isDecor) {
                    lp.widthFactor = 0.f;
                }
            }

            setCurrentItemInternal(newCurrItem, false, true);
            requestLayout();
        }
    }

要理解上面的这段代码,首先要明白mItems是什么,以及mItemsItemInfo中各个成员变量的含义:

  • mItems中的每一个ItemInfo和存在与ViewPager中的界面一一关联。
  • ItemInfo中的含义:
    static class ItemInfo {
        Object object; //通过PagerAdapter#instantiateItem所返回的Object.
        int position; //这个Item所处的位置,也就是上面我们所说的index.
        boolean scrolling;
        float widthFactor;
        float offset;
    }

此时,也就是我们位于index=0的页面,mItems的内容为:


而如果我们滑动到index=2的页面,那么mItems的内容变为:

我们注意上面有一句关键的话:

final int newPos = mAdapter.getItemPosition(ii.object);

对于它的返回值,有三种处理方式:

  • PagerAdapter.POSITION_UNCHANGED:这个ItemInfo在整个ViewPager的位置没有发生改变。
  • PagerAdapter.POSITION_NONE:这个ItemInfo在整个ViewPager中已经不存在了。
  • ii.position != newPos,也就是说ItemInfoViewPager仍然需要存在,但是它的位置发生了改变。

也就是说,notifyDataSetChanged()只处理ViewPager当前已经存在的界面,而对于这些界面如何处理,则要依赖于getItemPosition的返回值,但是FragmentPagerAdapter的返回值默认是POSITION_UNCHANGED,因此,当前已经存在的界面不会发生任何改变。

2.3 原因分析 - 问题二

问题二:对于重新添加的界面,不会回调getItem来获取新的Fragment
这个其实和notifyDataSetChanged()没有关系,而是和FragmentPagerAdapter寻找Fragment的方式有关,它会优先从FragmentManager中寻找,找不到了才会回调getItem来取新的Fragment
但是我们在移除界面的是调用的是detach方法,因此FragmentManager中仍然保存了Fragment的实例,在重新添加的时候就不会再回调getItem来取了,这个我们在前一篇文章中已经分析过,就不贴具体的代码了。

三、FragmentStatePagerAdapter

我们把上面例子中的FragmentPagerAdapter替换成为FragmentStatePagerAdapter,采用一样的更新方式,此时调用notifyDataSetChanged()之后,ViewPager当前存在的页面中的Fragment依然不会发生变化,不刷新的原因和FragmentPagerAdapter是相同的。
FragmentPagerAdapter不同的是,对于重新添加的界面,会回调getItem来获取新的Fragment,这个原因在之前的文章中也分析过了。

四、实现一个高效的动态FragmentPagerAdapter

我们的需求和下面的这个界面类似:



需求包括:

  • 支持动态地添加和移除界面,界面的个数和频道的个数相同,并且是可变的。
  • 当频道发生变化时,界面也要根据频道的顺序进行相应的改变,但是,如果上次存在的频道,在编辑之后仍然存在,那么应当复用之前的界面。

因此,我们继承于FramentStatePagerAdapter

public abstract class FixedPagerAdapter<T> extends FragmentStatePagerAdapter {

    private List<ItemObject> mCurrentItems = new ArrayList<>();

    public FixedPagerAdapter(FragmentManager fragmentManager) {
        super(fragmentManager);
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        while (mCurrentItems.size() <= position) {
            mCurrentItems.add(null);
        }
        Fragment fragment = (Fragment) super.instantiateItem(container, position);
        ItemObject object = new ItemObject(fragment, getItemData(position));
        mCurrentItems.set(position, object);
        return object;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        mCurrentItems.set(position, null);
        super.destroyItem(container, position, ((ItemObject) object).fragment);
    }

    @Override
    public int getItemPosition(Object object) {
        ItemObject itemObject = (ItemObject) object;
        if (mCurrentItems.contains(itemObject)) {
            T oldData = itemObject.t;
            int oldPosition = mCurrentItems.indexOf(itemObject);
            T newData = getItemData(oldPosition);
            if (equals(oldData, newData)) {
                return POSITION_UNCHANGED;
            } else {
                int newPosition = getDataPosition(oldData);
                return newPosition >= 0 ? newPosition : POSITION_NONE;
            }
        }
        return POSITION_UNCHANGED;
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        super.setPrimaryItem(container, position, ((ItemObject) object).fragment);
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return super.isViewFromObject(view, ((ItemObject) object).fragment);
    }

    public abstract T getItemData(int position);

    public abstract int getDataPosition(T t);

    public abstract boolean equals(T oldD, T newD);

    public class ItemObject {

        public Fragment fragment;
        public T t;

        public ItemObject(Fragment fragment, T t) {
            this.fragment = fragment;
            this.t = t;
        }
    }

}

这里:

  • 我们通过一个mCurrentItems保存了当前页面中对应的Fragment和其所包含的数据。
  • 最重要的是我们重写了getItemPosition方法,根据不同的情况返回位置,这里需要子类提供三个方面的信息:
  • 新数据在某个位置的数据。
  • 某个数据在新数据中的位置。
  • 判断两个数据是否相等的标准。

现在,我们的Adapter只需要重写很少的代码,就能实现数据的更新:

public class DemoActivity extends AppCompatActivity {

    private static final int INCREASE = 4;
    private FixedPagerAdapter mFixedPagerAdapter;
    private List<String> mTitles;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_not_update);
        TextView updateTv = (TextView) findViewById(R.id.tv_update);
        updateTv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                updateFragments();
            }
        });
        initFragments();
    }

    private void initFragments() {
        mTitles = new ArrayList<>();
        for (int i = 0; i < INCREASE; i++) {
            mTitles.add(String.valueOf(i));
        }
        ViewPager viewPager = (ViewPager) findViewById(R.id.vp_content);
        mFixedPagerAdapter = new MyFixedPagerAdapter(getSupportFragmentManager(), mTitles);
        viewPager.setAdapter(mFixedPagerAdapter);
    }

    private void updateFragments() {
        mTitles.clear();
        mTitles.add("3");
        mTitles.add("2");
        mFixedPagerAdapter.notifyDataSetChanged();
    }

    private class MyFixedPagerAdapter extends FixedPagerAdapter<String> {

        private List<String> mTitles;

        public MyFixedPagerAdapter(FragmentManager fragmentManager, List<String> titles) {
            super(fragmentManager);
            mTitles = titles;
        }

        @Override
        public String getItemData(int position) {
            return mTitles.size() > position ? mTitles.get(position) : null;
        }

        @Override
        public int getDataPosition(String s) {
            return mTitles.indexOf(s);
        }

        @Override
        public boolean equals(String oldD, String newD) {
            return TextUtils.equals(oldD, newD);
        }

        @Override
        public Fragment getItem(int position) {
            return LogcatFragment.newInstance(mTitles.get(position));
        }

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

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

推荐阅读更多精彩内容