对viewpager熟悉的同学都知道,viewpager有2个弊端:一是不能关闭预加载,二是PageAdapter.notifyDataSetChanged()无效问题

其中第一个弊端,不能关闭预加载相信很多人都知道原因了,所以这里不在进行解释,直接将源码放出来估计也能看得懂:

private static final int DEFAULT_OFFSCREEN_PAGES = 1;
public void setOffscreenPageLimit(int limit) {         if (limit < DEFAULT_OFFSCREEN_PAGES) {             Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "                + DEFAULT_OFFSCREEN_PAGES);        limit = DEFAULT_OFFSCREEN_PAGES;    }    if (limit != mOffscreenPageLimit) {             mOffscreenPageLimit = limit;        populate();    }}

简单解释下,就是即使我们认为的设置setOffscreenPageLimit(0)也没有用,这个方法就是设置viewpager的预加载数量,当我们设置数量为0时,小于默认数量1,所以设置无效,意味着viewpager的最小预加载数量就是1个。

那么第一个问题的解决方案我知道的就有两种,文末我会给出其中一种我觉得比较好的方案,先看第二个弊端。

另外一个弊端就是调用PageAdapter.notifyDataSetChanged()无法刷新viewpager布局的问题,具体的问题也是可以从ViewPager的源码看出来。

当我们调用adapter.notifyDataSetChanged()的时候,PageAdapter内部会调用ViewPager的dataSetChanged()方法,我们截取该方法的片段:

void dataSetChanged() {           ...    boolean isUpdating = false;    for (int i = 0; i < mItems.size(); i++) {             final ItemInfo ii = mItems.get(i);        // 从mAdapter获得子布局的位置pos        final int newPos = mAdapter.getItemPosition(ii.object);        // 如果子View的pos位置没变,则直接返回,不刷新布局        if (newPos == PagerAdapter.POSITION_UNCHANGED) {                 continue;        }        // 如果newPos等于PagerAdapter.POSITION_NONE        // 则进入重新布局的逻辑        if (newPos == PagerAdapter.POSITION_NONE) {                 mItems.remove(i);            i--;           ...    }}

从上面的代码片段以及简略的注释就可以明白第二个问题的来源,知道了问题的来源,那么解决问题就有了思路,其实我们只需要强制PageAdaper的getItemPosition()返回POSTION_UNCHENGED的就可以了,也就是重写PageAdapter的getItemPosition()方法即可:

// 解决Adapter.notifyDataSetChanged()无效问题@Overridepublic int getItemPosition(@NonNull Object object) {         return POSITION_NONE;}

第二个弊端解决比较容易,那么如何解决第一个弊端,让ViewPager能够实现懒加载呢?

ViewPager无论如何都是会帮我们预先加载至少一个page,这个设计的初衷是为了能够让我们能提前加载好下一页的数据,这样在page之间滑动的时候能够更加丝滑,视图的滑动不会因为加载数据而卡顿,从而达到一定程度的视觉优化效果,只不过是以牺牲内存为前提的。

试想一下,如果page很多呢,那我们可以设置预加载数量为1,只缓存一页的数据和布局就行,但是如果用户在page间快速滑动呢?page还没缓存及时,就产生了滑动,那么一样还是会造成滑动卡动的情况。

这就有了懒加载的应用场景。

那么要实现viewpager中fragment的懒加载,如何实现呢?

我们希望达到的效果就是能尽量减少内存的消耗,同时在page间滑动不会卡顿。

我们无法避免viewpager的预加载特性,至少都会帮我们预加载1个视图的数据,如果加载的数据量大,滑动的时候还没有加载完成,那么也会出现滑动卡顿的情况。

既然无法避免viewpager的预加载,那么我们就让它预加载我们的视图,只不过这是个空的视图,视图中的数据是等到这个视图真正可见的时候再加载。

这样即使预加载所有的视图(空视图),占用的内存也会很低,同时因为视图已经提前被初始化,在page间滑动的时候也不会造成卡顿,这种思路简直完美。接下来就让我们来实际操作下,实验是检验真理的唯一标准,代码亦如此。

首先新建一个视图:

下面一个RadioGroup,里面放着三个RadioButton,上面部分则是ViewPager

然后新建一个Activity,并设置引用这个视图(setContentView()),获取控件

为ViewPager准备PageAdapter,这里我们自定义一个PageAdapter:

为了清晰的达到我们的实验效果,所以其他逻辑尽量简单。

pageAdapter的关键部分就是getItem(),由这个方法创建我们的fragments,要实现fragment的懒加载,那我们fragment也需要自定义。

先定义一个接口,当我们fragment由不可见变为可见时就会调用这个接口的方法:

public interface OnFirstShowListener {         void onFirstShow();}

然后创建一个ShellFragment类,继承自Fragment,然后在onAttach()和setUserVisibleHint()中,按视图是否可见来调用接口的方法:

在我们的视图不可见的时候,我们只希望让viewpager加载我们的空视图,可见后再加载我们具体的视图和数据,这部分的逻辑就在onFirstShow()中。

我们可以直接在ShellFragment中更新加载后的数据,但这样类的复用率就太低了,一个ShellFragment就对应着一个page,而一个ViewPager一般至少都是2个page以上。

所以我们要让这个Fragment有更高的复用率,同时耦合度降低,可以在当前这个Fragment的基础上在加载一个包含具体业务视图的Fragment。当然,是在ShellFragment可见的时候才加载我们具体的业务Fragment。接着完善我们的ShellFragment吧

@Nullable@Overridepublic View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {         return inflater.inflate(R.layout.fragment_content, container, false);}

给ShellFragment设置一个空布局,布局文件中只是一个FramLayout,除了给这个FrameLayout设置ID为root之外,里面没有任何逻辑。

private Fragment mContentFragment;private Bundle mExtraBundle;
public void setContentFragment(Fragment fragment) {         if (fragment == null || mContentFragment != null || getActivity() == null) {             return;    }    Bundle srcArgument = fragment.getArguments();    if (srcArgument == null) {             srcArgument = new Bundle();    }    srcArgument.putAll(mExtraBundle);    // add之后会调用onCreate和onCreateView()    if (!fragment.isAdded()) {             fragment.setArguments(srcArgument);    }    mContentFragment = fragment;    FragmentManager manager = getChildFragmentManager();    if (manager != null) {             manager.beginTransaction().replace(R.id.root, mContentFragment).commitAllowingStateLoss();    }}

setContentFragment()就是实现懒加载主要的逻辑了,我们将contentFragment传进来,并把参数(如果有的话)设置进去,最后赋值给我们的成员属性mContentFragment,开启ChildFragmentManager事务让这个mContentFragment显示在R.id.root,也就是我们FrameLayout上面就行了。

最终setContentFragment()方法会在OnFirstShowListener接口方法调用。

接下来分别准备3个Fragment,我创建了三个,分别是OneFragment,TwoFragment,ThreeFragment,他们的视图有区分度就行。

fragment准备好了,还剩pageAdapter待完善,那就是上面空出来的getItem()方法,看看这里怎么实现:

其他未给出的逻辑也类似,只是相应的fragment不是OneFragment,而是TwoFragment,ThreeFragment。这里就省略了。

接下来看看效果:

这是当页面还没滑动,只显示fragment1视图的时候,当前的视图结构:

因为我们没有给ViewPager设置offsetLimitCount,默认会预加载一个page,所以这时父Fragment中有两个ShellFragment,分别对应fragment1和fragment2的page。

而因为fragment1已经可见,所以会调用onFirstShow()方法,将具体的业务Fragment加载出来,也就是OneFragment。

当我们滑动到fragment2时,视图结构又是怎么样的呢?来看看


只有显示fragment2时才加载的TwoFragment并显示,可见懒加载已实现。

同时,也在实验的过程中印证了ViewPager.setOffsetLimitCount()的真正意义,就是规定了当前显示页,左右两边会被缓存(预先加载)的页数.

例如,上例设置的setOffsetLimitCount = 1,且当前显示在fragment2,那么就会缓存(预先加载)左右两边各一页,如果当前显示的是fragment3,因为右边已经没有新的一页了,所以只会缓存(预先加载)左边一页fragment2。

兄dei,如果觉得我写的还不错,麻烦帮个忙呗

  1. 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#.#)
  2. 不用点收藏,诶别点啊,你怎么点了?这多不好意思!

拜托拜托,我让我好朋友给您磕头了!

更多相关文章

  1. Android加载通话记录流程分析
  2. Android两行代码搞定ViewPager的过渡动画
  3. Firmware加载原理分析
  4. Android之LayoutInflater了解(20190109)
  5. [置顶] android scrollview 滑动到顶端或者指定位置
  6. Android(安卓)动态加载menu
  7. Android(安卓)API:自定义ViewGroup
  8. Android(安卓)ViewFlipper实现多个布局手势切换的效果
  9. Android(安卓)布局优化之include与merge,最后有ViewStub

随机推荐

  1. Android实体类序列化
  2. 关于android的设备管理器-DevicePolicyMa
  3. Android(安卓)Gradle 学习之二:重命名APK
  4. Android(安卓)添加数据到本地Excel表中
  5. Android(安卓)框架层为IMountService 增
  6. android进入页面时焦点不在首页问题
  7. Dex2Oat源码流程(1)——Android6.0
  8. Android(安卓)判断手机厂商rom
  9. android View移动总结
  10. Android(安卓)NDK 异常 Error:No toolcha