大家都用过ViewPager吧😄,使用ViewPager时,需要给它配一个Adapter,通常我们要继承下面三个Adapter来,分别是:
———>最基本的PagerAdapter
——————>继承PagerAdapter的FragmentPagerAdapter
——————>继承PagerAdapter的FragmentStatePagerAdapter
那么这三个Adapter有什么区别,我们应该怎么选择呢?
首先,我们知道如果继承了PagerAdapter,需要我们实现下面两个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public abstract int getCount () ;public Object instantiateItem (ViewGroup container, int position) { return instantiateItem((View) container, position); }
第一个是要告诉ViewPager一共有几个页面,第二个则是ViewPager需要加载页面了,你需要根据position创建不同的页面对象,这里通常是View对象。
当然,如果你的ViewPager中内容比较复杂,需要用Fragment来自动管理其生命周期,那么可以使用FragmentPagerAdapter和FragmentStatePagerAdapter中的一种,那么他俩有什么区别呢?
首先我们看一下FragmentPagerAdapter的instantiateItem
和destroyItem
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Override public Object instantiateItem (ViewGroup container, int position) { ... final long itemId = getItemId(position); String name = makeFragmentName(container.getId(), itemId); Fragment fragment = mFragmentManager.findFragmentByTag(name); if (fragment != null ) { if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment); mCurTransaction.attach(fragment); } else { fragment = getItem(position); if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment); mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId)); } ... return fragment; } @Override public void destroyItem (ViewGroup container, int position, Object object) { if (mCurTransaction == null ) { mCurTransaction = mFragmentManager.beginTransaction(); } if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object + " v=" + ((Fragment)object).getView()); mCurTransaction.detach((Fragment)object); } public long getItemId (int position) { return position; } private static String makeFragmentName (int viewId, long id) { return "android:switcher:" + viewId + ":" + id; }
可以看出instantiateItem
方法在添加Fragment时,会带一个Fragment的名字,其实就是Tag:
1 mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));
这个Tag是根据一定规则从makeFragmentName
中获得的,这里唯一变化的就是id,默认的id可从getItemId(position)中看到,就是当前的position,保证了每个fragment的tag是唯一的。
当ViewPager通过instantiateItem
获取Fragment的时候,会先根据之前设置的Tag找一下这个Fragment存不存在:
1 mFragmentManager.findFragmentByTag(name)
根据获取到的Fragment有两种添加方式,如果根据Tag找到了之前存在的Fragment,就attach上去:
1 mCurTransaction.attach(fragment);
如果没找到,那就把新创建的Fragment add上去:
1 mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));
当不用这个Fragment的时,会调用destroyItem
方法,可以看到这里是detach掉了Fragment,并没有销毁。
综上,一个页面只会创建一次,创建时根据当前的position给Fragment设置一个Tag,当不需要时只是把Fragmetn detach掉,并不会销毁,下次需要时通过Tag复用Fragment。
所以,如果你有大量的Fragment要展示,FragmentPagerAdapter会持有每一个Fragment不释放,最终走向OOM。
所以,无论使用PagerAdapter还是FragmentPagerAdapter,有多少页面,就会创建多少页面对象,页面很多的情况下,会非常占用内存,虽然这样,但是它们也有各自的应用场景,例如,App的首次安装的启动引导页面个数是固定的,而且如果比较复杂,通常做成Fragment,这时使用FragmentPagerAdapter较为合适;而如果页面中有需要自动轮播卡片的地方,则可以使用PagerAdapter实现,因为其页面一般都是View,结构简单,数量也是固定的。
那如果我需要做一个可以无限滑动的卡片,就需要用到FragmentStatePagerAdapter了,我们看一下FragmentStatePagerAdapter的instantiateItem
和destroyItem
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public abstract Fragment getItem (int position) ;@Override public Object instantiateItem (ViewGroup container, int position) { ... if (mCurTransaction == null ) { mCurTransaction = mFragmentManager.beginTransaction(); } Fragment fragment = getItem(position); while (mFragments.size() <= position) { mFragments.add(null ); } ... mCurTransaction.add(container.getId(), fragment); return fragment; } @Override public void destroyItem (ViewGroup container, int position, Object object) { Fragment fragment = (Fragment) object; ... mCurTransaction.remove(fragment); }
可以看到,FragmentStatePagerAdapter在初始化页面时,通过getItem这个抽象方法来获得Fragment,并将其add到容器里,而不用的时候直接remove掉,这样就不会一直占用Fragment,可以实现无限滑动,唯一关心的就是在getItem中创建一个Fragment就可以了。
以上就是我对三种PagerAdapter的简单理解。在使用ViewPager的过程中,除了adapter的选择,还需要到了下面的问题。
2.常见的两个问题小析 1.notifyDataSetChanged()后页面没有刷新的问题 这个问题主要是由于当数据更新时,ViewPager会调用dataSetChanged
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 void dataSetChanged () { ... for (int i = 0 ; i < mItems.size(); i++) { final ItemInfo ii = mItems.get(i); final int newPos = mAdapter.getItemPosition(ii.object); if (newPos == PagerAdapter.POSITION_UNCHANGED) { continue ; } 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) { newCurrItem = Math.max(0 , Math.min(mCurItem, adapterCount - 1 )); needPopulate = true ; } continue ; } ... } ... }
这个方法会调用adapter的getItemPosition()来获得object的位置情况是否发生变化,如果不发生变化就不更新了,如果而这个方法默认是返回的就是POSITION_UNCHANGED:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public int getItemPosition (Object object) { return POSITION_UNCHANGED; }
所以要想每次刷新都让页面更新,需要在adapter中重写getItemPosition方法,并返回POSITION_NONE就可以了:
1 2 3 4 5 @Override public int getItemPosition (Object object) { return POSITION_NONE; }
2.初始化数据时不会调用OnPageChangeListener 相信大家都遇到过这种情况,就是ViewPager初始化时并不会调用OnPageChangeListener,所以很多时候我们都是手动调用第一页的onPageSelected方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private ViewPager.OnPageChangeListener onPageChangeListener = new ViewPager .OnPageChangeListener() { @Override public void onPageScrolled (int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected (int position) { } @Override public void onPageScrollStateChanged (int state) { } }; public void OnCreate () { ... mViewPager.setOnPageChangeListener(onPageChangeListener); onPageChangeListener.onPageSelected(0 ); ... }
但是如果如果你使用的是FragmentPagerAdapter时,会发现在onPageSelected(0)时,会发现Fragment还没有创建成功,这时候会出现NPE(NullPointerException)。所以这时候可以这样写:
1 2 3 4 5 6 mViewPager.post(new Runnable (){ @Override public void run () { onPageChangeListener.onPageSelected(0 ); } });
这样就会在viewpager的UI事件队列完成后处理onPageSelected方法的内容,这个时候Fragment已经好了。
但是,虽然这样,讲要进行的工作交给post会打乱同步时序,让要做的事充满了不确定性,我不知道它什么时候能调用onPageSelected方法。而且这样做也存在另外一个问题,当使用notifyDataSetChanged()刷新数据时,还是不会调用当前页的onPageSelected来更新当前页面,所以我更倾向于下面这种方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class MyPagerAdapter extends FragmentStatePagerAdapter { ... private Object lastPrimaryItem; private OnPageSwitchListener onPageSwitchListener; public MyPagerAdapter (FragmentManager fm, OnPageSwitchListener onPageSwitchListener) { super (fm); this .onPageSwitchListener = onPageSwitchListener; } @Override public void setPrimaryItem (ViewGroup container, int position, Object object) { super .finishUpdate(container); super .setPrimaryItem(container, position, object); if (object == lastPrimaryItem) { return ; } lastPrimaryItem = object; MyFragment fragment = (MyFragment)object; if (onPageSwitchListener != null && fragment != null ) { onPageSwitchListener.onPageSwitch(position, fragment); } } @Override public void finishUpdate (ViewGroup container) { } public interface OnPageSwitchListener { void onPageSwitch (int position, MyFragment fragment) ; } } class MyActivity extends Activity implements MyPagerAdapter .OnPageSwitchListener{ @Override public void onPageSwitch (int position, MyFragment fragment) { } }
简单说就是,PagerAdapter中有一个重要的方法叫setPrimaryItem
,当一个页面显示的时候都会调用这个方法(多次),通过一些简单的处理,可以实现与mViewPager.setOnPageChangeListener()类似的功能,这样就不用每次调用onPageChangeListener.onPageSelected(0)或者使用Post操作了。
以上分析较为肤浅,有问题欢迎指正、补充,谢谢🙏
参考
[1] https://stackoverflow.com/questions/16074058/onpageselected-doesnt-work-for-first-page
[2] https://stackoverflow.com/questions/11794269/onpageselected-isnt-triggered-when-calling-setcurrentitem0