重拾Android之路之ViewPager(再详解)

前言:

每当使用ViewPager时,对于选用什么适配器缓存多少页面,是否需要懒加载以及Fragment数据刷新经常会有些疑问,网络上的答案很多,但是很少有一篇能够对一些疑问进行总结,本文主要在于记录,方便日后查看。

正文

1. FragmentPagerAdapter和FragmentPagerStateAdapter的区别及使用场景

setOffScreenPageLimit(int limit)设置viewpager左右预加载页

viewPager.setOffscreenPageLimit(1);

区别:

FragmentPagerAdapter将每一个生成的Fragment保存在内存中,limit外Fragment没有销毁,生命周期为onPause->onStop->onDestroyView , onCreateView ->onStart->onResume,但Fragment的成员变量都没有变,所以可以缓存根View,避免重复inflate

FragmentPageAdater下Fragment的生命周期
private View mRootView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    Log.e(TAG, "onCreateView: page_" + mPosition);
    if (mRootView == null) {
        mRootView = inflater.inflate(R.layout.fragment_test, container, false);
        initView(mRootView);
    }
    return mRootView;
}

FragmentStatePagerAdapter对limit外的Fragment销毁,生命周期为onPause->onStop->onDestoryView->onDestory->onDetach, onAttach->onCreate->onCreateView->onStart->onResume。

FragmentStatePageAdapter下Fragment的生命周期

使用场景:

对于需要缓存在内存中的固定较少数量的静态页面使用FragmentPagerAdapter,如引导页Tab页面

对于拥有大量页面的情况应使用FragmentStatePagerAdapter避免占用大量内存,如图片预览

2.是否有必要在适配器的public Fragment getItem(int position)方法中返回缓存List<Fragment>中的Fragment

对于FragmentPagerAdapterinstantiateItem()先从FragmentManager.findFragmentByTag()中查找FragmentManagerList缓存的Fragment

取不到则会调用getItem(),所以对于缓存在内存中的FragmentPagerAdapter没有必要再使用一个List缓存Fragment,因为FragmentPagerAdapter缓存每一个加载过的Fragment内存中。

instantiateItem源码
makeFragmentName源码

对于FragmentStatePagerAdapterinstantiateItem()则会缓存limit左右的Fragment,超过limit则会回收,当Fragment没有缓存时重新调用getItem(),因为页面比较多,所以也没必要使用List缓存Fragment占用内存,否则FragmentStatePagerAdapter没有意义。

instantiateItem源码
fragments源码
3.ViewPager为什么要懒加载,什么情况适用?

ViewPagersetOffScreenPageLimit()方法默认limit为1,即会预加载左右页面,而为了节省流量,理想情况是当用户切换到该界面时才会调用网络请求获取数据。相关方法为setUserVisibleHint(),当前页面为true,预加载页面为false,只有Fragment从可见到不可见或者从不可见到可见时会调用,Fragment初次创建时setUserVisibleHint先于onCreateView()调用,所以可以由此判断Fragment是否初始创建。

ViewPager首次显示的页面经过方法调用setUserVisibleHint(false)->setUserVisibleHint(true)->onCreateView()...,所以该页面的数据加载放在onCreateView中;其它预加载页面预加载时setUserVisibleHint(false)->onCreateView()...,当选中该页面显示时调用setUserVisibleHint(true),所以预加载页面数据加载放在setUserVisibleHint中。

懒加载limit内Fragment的生命周期.png
/**
 * 延迟加载Fragment
 */

public abstract class LazyLoadFragment extends BaseFragment {
    protected boolean bIsViewCreated;
    protected boolean bIsDataLoaded;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(getLayoutResId(), container, false);
        initView(view);

        bIsViewCreated = true;

        if (getUserVisibleHint() && !bIsDataLoaded) {
            loadData();
            bIsDataLoaded = true;
        }
        return view;
    }


    @Override
    public void onDestroyView() {
        super.onDestroyView();

        bIsViewCreated = false;
        bIsDataLoaded = false;
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);

        if (isVisibleToUser && bIsViewCreated && !bIsDataLoaded) {
            loadData();
            bIsDataLoaded = true;
        }
    }

    /**
     * @return 布局资源id
     */
    protected abstract int getLayoutResId();

    /**
     * 初始化View
     */
    protected abstract void initView(View view);

    /**
     * 加载数据
     */
    protected abstract void loadData();
}

因为懒加载需要设置setOffScreenPageLimit,所以适合有网络请求页面较少且需要缓存的Tab页面,配合FragmentPagerAdapter使用,因为limit要包括所有的界面,在limit内FragmentStatePagerAdapterFragmentPagerAdapter没有区别。

4.ViewPager刷新数据

一般使用PagerAdapternotifyDataSetChanged方法来刷新数据,但是很多时候数据没有更新,先来看PagerAdapternotifyDataSetChanged方法

notifyDataSetChanged

观察者模式:

观察者模式
nofifyChanged
onChanged

ViewPager中的PagerObserver实现了DateSetObserver

ViewPager_PagerObserver

ViewPager中的dataSetChanged方法会根据adapter.getItemPosition返回的值来判断是否DestroyItem

dataSetChanged

getItemPosition默认会返回POSITION_UNCHANGED,而ViewPager中dataSetChanged只有当返回POSITION_NONE时才会销毁页面重新创建

getItemPosition

继续看ViewPager中dataSetChanged方法

needPopulate
setCurrentItemInternal

接着到populate方法

populate

终于跑到adapter的instantiateItem方法了

addNewItem
FragmentPagerAdapter的inistantiateItem和destoryItem方法.png
FragmentStatePagerAdapter的destoryItem方法.png

所以如果想通过adapter.notifyDataSetChanged来刷新页面时,必须继承FragmentStatePagerAdapter,因为FragmentPagerAdapter会缓存Fragment,不会走getItem方法,同时将所要刷新页面的getItemPosition返回POSITION_NONE

@Override
public int getItemPosition(Object object) {
    return POSITION_NONE;
}

还有其他的一种做法,拿到Fragment,通过Fragment中的public方法来刷新页面,由FragmentPagerAdapterinstantiateItem方法内部通过tag查找Fragment,因此可以保存其相同的tag

private SparseArray<String> mTags = new SparseArray<>();
    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        mTags.put(position, makeFragmentName(container.getId(), position));
        return super.instantiateItem(container, position);
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        mTags.remove(position);
        super.destroyItem(container, position, object);
    }

    private String makeFragmentName(int viewId, int position) {
        return "android:switcher:" + viewId + ":" + position;
    }

然后获取Fragment

Fragment fragment = getSupportFragmentManager().findFragmentByTag(mTags.get(position));
fragment.XXX();

由第二节FragmentStatePagerAdapterinstantiateItem方法可知,其保存时没有对Fragment添加tag,ViewPager中的Fragment也不能指定id,只有通过调用

  Fragment fragment = (Fragment)(fragmentStatePagerAdapter.instantiateItem(viewpager, position));

来获取Fragment

总结

  • 有Tab时:
    需要设置setOffScreenPageLimit,FragmentPageAdapterFragmentStatePageAdapter效果相同,让Fragment都缓存在内存中,否则Fragment销毁了再次点击Tab选中又会重新创建会很突兀。需要网络请求时则执行延迟加载策略,无需网络请求时可以正常创建Fragment。

  • 无Tab时:
    无需设置SetOffScreenPageLimit,因为默认limit是1,会预加载左右界面,不会显得突兀。页面较多时则选用占用内存少的FragmentStatePageAdapter,如浏览大图页面;页面较少时则选用加载到内存的FragmentPageAdapter, 如引导页,需要注意的是FragmentPageAdapter在limit外的Fragment没有销毁,生命周期为onPause->onStop->onDestroyView, onCreateView->onStart->onResume,但Fragment的成员变量都没有变,所以可以缓存根View。

  • 如果需要刷新所有limit内的页面,继承FragmentStatePagerAdapter, 设置getItemPosition返回POSITION_NONE,再调用notifyDataSetChanged;如果只需要刷新单个页面,则通过获取Fragment的引用,再通过public方法来更新数据。

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

推荐阅读更多精彩内容