1. 程式人生 > >ViewPager中使用Fragment時防止資料預載入

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。

因此,也就不難理解為什麼開頭的程式碼可以解決防止資料預載入了。