ViewPager中使用Fragment时防止数据预加载

  我们知道ViewPager是具有预加载页面的特性的,默认会提前加载左右各1页的View, 如果在ViewPager中使用Fragment,那么Fragment也会被预加载,如果你是在Fragment生命周期中写请求网络加载数据的方法,就会遇到页面未展示,但是数据会被提前加载的问题,有时我们不想要这个效果,我们想滑动到哪一页时再去加载哪一页的数据,怎么办呢?

先上最终解决问题的代码:

/**
 * 在ViewPager中使用,可以防止数据预加载, 只预加载View,滑动到哪一页才会加载哪一页的数据
 */
public abstract class BaseViewPageFragment extends BaseFragment {
    private boolean mIsDataInited;

    @Override
    protected void onViewCreated() {
        initView();
        initListener();
        if (!mIsDataInited) {
            if (getUserVisibleHint()) {
                initData();
                mIsDataInited = true;
            }
        }
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        //防止数据预加载, 只预加载View,不预加载数据
        if (isVisibleToUser && isVisible() && !mIsDataInited) {
            initData();
            mIsDataInited = true;
        }
    }

    @Override
    public abstract void initView();

    @Override
    public abstract void initListener();

    @Override
    public abstract void initData();
}

其中BaseFragment是我在项目中使用的一个基类就是继承了v4包中的Fragment,代码不贴了,你的项目中应该也会自己的基类,继承你自己的基类就好了。上面代码中主要在两个方法中进行了控制,onViewCreated()setUserVisibleHint, 另外这里我还使用了标志位,多重条件保证Fragment创建时数据不会被预加载且Fragment可见时只加载一次。

为什么这样可以在ViewPager中防止数据预加载呢,先来看一下setUserVisibleHint这个方法:

    /**
     * Set a hint to the system about whether this fragment's UI is currently visible
     * to the user. This hint defaults to true and is persistent across fragment instance
     * state save and restore.
     *
     * <p>An app may set this to false to indicate that the fragment's UI is
     * scrolled out of visibility or is otherwise not directly visible to the user.
     * This may be used by the system to prioritize operations such as fragment lifecycle updates
     * or loader ordering behavior.</p>
     *
     * <p><strong>Note:</strong> This method may be called outside of the fragment lifecycle.
     * and thus has no ordering guarantees with regard to fragment lifecycle method calls.</p>
     *
     * @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
     *                        false if it is not.
     */
    public void setUserVisibleHint(boolean isVisibleToUser) {
        if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
                && mFragmentManager != null && isAdded()) {
            mFragmentManager.performPendingDeferredStart(this);
        }
        mUserVisibleHint = isVisibleToUser;
        mDeferStart = mState < STARTED && !isVisibleToUser;
    }

以上是来自support v4包中的Fragment源码, 大概的意思是调用这个方法来设置Fragment当前是否是对用户可见的,这个方法只有一个参数,visible就传true,否则就传false。可是在Fragment源码搜索发现并没有地方调用这个方法,那这个方法可能是给用户来调用的。

由此,我们可以猜想肯定是在ViewPager使用的过程中的某个地方调用了这个方法,我们看下ViewPager使用Fragment的流程一般是:

mAdapter = new BaseFragmentPagerAdapter(getSupportFragmentManager(), mFragmentList);
mViewPager.setAdapter(mAdapter);

于是,我先到FragmentPagerAdapter的源码中搜索了下,果不其然,发现了踪迹:

@SuppressWarnings("ReferenceEquality")
@Override
public Object instantiateItem(ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    final long itemId = getItemId(position);

    // Do we already have this fragment?
    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));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}

在instantiateItem方法的最后我们发现了调用的地方:

if (fragment != mCurrentPrimaryItem) {
    fragment.setMenuVisibility(false);
    fragment.setUserVisibleHint(false);
}

这里有个判断fragment != mCurrentPrimaryItem这个时候会把Fragment的Visible设为false, 那这个mCurrentPrimaryItem又是什么呢,继续搜索源码:

@SuppressWarnings("ReferenceEquality")
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
    Fragment fragment = (Fragment)object;
    if (fragment != mCurrentPrimaryItem) {
        if (mCurrentPrimaryItem != null) {
            mCurrentPrimaryItem.setMenuVisibility(false);
            mCurrentPrimaryItem.setUserVisibleHint(false);
        }
        if (fragment != null) {
            fragment.setMenuVisibility(true);
            fragment.setUserVisibleHint(true);
        }
        mCurrentPrimaryItem = fragment;
    }
}

在一个setPrimaryItem的方法中找到这个变量的赋值,这个方法将传进来的fragment的visible设置为true, 同时会更新mCurrentPrimaryItem变量的值。我们再继续搜索setPrimaryItem这个方法的调用,结果在当前FragmentPagerAdapter的源码中没有找到,但是在它的父类PagerAdapter的源码中找到了它的定义:

    /**
     * Called to inform the adapter of which item is currently considered to
     * be the "primary", that is the one show to the user as the current page.
     *
     * @param container The containing View from which the page will be removed.
     * @param position The page position that is now the primary.
     * @param object The same object that was returned by
     * {@link #instantiateItem(View, int)}.
     */
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        setPrimaryItem((View) container, position, object);
    }

看注释大概明白了这个方法的含义是设置为ViewPager中当前展示给用户的那一页。
继续到ViewPager的源码中搜索,找到了调用它的地方:

这里写图片描述

是在一个populate()的方法中调用的,搜索发现好多地方调用了它,但是发现了有两个关键的地方:

    /**
     * Set a PagerAdapter that will supply views for this pager as needed.
     *
     * @param adapter Adapter to use
     */
    public void setAdapter(PagerAdapter adapter) {
        //...忽略部分源码

        if (mAdapter != null) {
            //...忽略部分源码
            if (mRestoredCurItem >= 0) {
                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
                setCurrentItemInternal(mRestoredCurItem, false, true);
                mRestoredCurItem = -1;
                mRestoredAdapterState = null;
                mRestoredClassLoader = null;
            } else if (!wasFirstLayout) {
                populate();
            } else {
                requestLayout();
            }
        }

        //...忽略部分源码
    }
/**
  * Set the currently selected page.
  *
  * @param item Item index to select
  * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately
  */
 public void setCurrentItem(int item, boolean smoothScroll) {
     mPopulatePending = false;
     setCurrentItemInternal(item, smoothScroll, false);
 }

 void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
     setCurrentItemInternal(item, smoothScroll, always, 0);
 }

 void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {

     //...忽略部分源码

     if (mFirstLayout) {
         // We don't have any idea how big we are yet and shouldn't have any pages either.
         // Just set things up and let the pending layout handle things.
         mCurItem = item;
         if (dispatchSelected) {
             dispatchOnPageSelected(item);
         }
         requestLayout();
     } else {
         populate(item);
         scrollToItem(item, smoothScroll, velocity, dispatchSelected);
     }
 }

看到这里就不陌生了,这两个地方分别是在我们使用viewpager的时候设置mViewPager.setAdapter(mAdapter)mViewPager.setCurrentItem(1)时调用的,也就是说这个时候会最终调用到ViewPager里的Fragment的setUserVisibleHint方法,初始化的时候会默认设置为第一页可见,滑动切换的时候,滑动到哪一页就会设置哪一页的Fragment的setUserVisibleHint为true, 而其他页为false。

因此,也就不难理解为什么开头的代码可以解决防止数据预加载了。

猜你喜欢

转载自blog.csdn.net/lyabc123456/article/details/81975246
今日推荐