多層嵌套後的 Fragment 懶加載實現

多層嵌套後的 Fragment 懶加載

印象中從 Feed 流應用流行開始,Fragment 懶加載變成了一個你們都須要關注的開發知識,關於 Fragment 的懶加載,網上有不少例子,GitHub 上也有不少例子,就連我本身在一年前也寫過相關的文章。可是以前的應用可能最多的是一層 Activity + ViewPager 的 UI 層次,可是隨着頁面愈來愈複雜,愈來愈多的應用首頁一個頁面外層是一個 ViewPager 內部可能還嵌套着一層 ViewPager,這是以前的懶加載就可能不那麼好用了。本文對於多層 ViewPager 的嵌套使用過程當中,Fragment 主要的三個狀態:第一次可見,每次可見,每次不可見,提供解決方案。java

爲何要使用懶加載

在咱們開發中常常會使用 ViewPager + Fragment 來建立多 tab 的頁面,此時在 ViewPager 內部默認會幫咱們緩存當頁面先後兩個頁面的 Fragment 內容,若是使用了 setOffscreenPageLimit 方法,那麼 ViewPager 初始化的時候將會緩存對應參數個 Fragment。爲了增長用戶體驗咱們每每會使用該方法來保證加載過的頁面不被銷燬,並留離開 tab 以前的狀態(列表滑動距離等),而咱們在使用 Fragment 的時候每每在建立完 View 後,就會開始網絡請求等操做,若是存在上述的需求時,懶加載就顯得尤其重要了,不只能夠節省用戶流量,還能夠在提升應用性能,給用戶帶來更加的體驗。android

ViewPager + Fragment 的懶加載實質上咱們就在作三件事,就能夠將上邊所說的效果實現,那麼就是找到每一個 Fragment 第一對用戶可見的時機,和每次 Fragment 對用戶可見時機,以及每次 Framgment 對用戶不可見的時機,來暴露給實現實現類作對應的網絡請求或者網絡請求中斷時機。下面咱們就來從常見的幾種 UI 結構上一步步實現不管嵌套多少層,不管開發者使用的 hide show 仍是 ViewPager 嵌套都能準確獲取這三種狀態的時機的一種懶加載實現方案。git

單層 ViewPager + Fragment 懶加載

咱們都知道 Fragment 生命週期按前後順序有github

onAttach -> onCreate -> onCreatedView -> onActivityCreated -> onStart -> onResume -> onPause -> onStop -> onDestroyView -> onDestroy -> onDetach緩存

對於 ViewPager + Fragment 的實現咱們須要關注的幾個生命週期有:bash

onCreatedView + onActivityCreated + onResume + onPause + onDestroyView微信

以及非生命週期函數:網絡

setUserVisibleHint + onHiddenChangedapp

對於單層 ViewPager + Fragment 多是咱們最經常使用的頁面結構了,如網易雲音樂的首頁頂部的是三個 tab ,咱們那網易雲音樂做爲例子:ide

對於這種 ViewPager + Fragment 結構,咱們使用的過程當中通常只包含是 3 種狀況分別是:

  1. 使用 FragmentPagerAdapterFragmentPagerStateAdapter不設置 setOffscreenPageLimit

    • 左右滑動頁面,每次只緩存下一個 Pager ,和上一個 Pager
    • 間隔的點擊 tab 如從位於 tab1 的時候直接選擇 tab3 或 tab4 ,tab1將會被銷燬
  2. 使用 FragmentPagerAdapterFragmentPagerStateAdapter 設置 setOffscreenPageLimit 爲 tab 總數

    • 建立 ViewPager 的時候全部頁面都將建立完成,生命週期走到 onResume
    • 間隔的點擊 tab 如從位於 tab1 的時候直接選擇 tab3 或 tab4, tab1不會被銷燬
  3. 進入其餘頁面或者用戶按 home 鍵回到桌面,當前 ViewPager 頁面變成不見狀態。

對於 FragmentPagerAdapterFragmentPagerStateAdapter 的區別在於在於,前者在 Fragment 不見的時候將不會 detach ,然後者將會銷燬 Fragmentdetach 掉。

實際上這也是全部 ViewPager 的操做狀況。

  • 第一種狀況不設置 setOffscreenPageLimit 左右滑動頁面/或者每次選擇相鄰 tab 的狀況 FragmentPagerAdapterFragmentPagerStateAdapter 有所區別
BottomTabFragment1  setUserVisibleHint false
 BottomTabFragment2  setUserVisibleHint false
 BottomTabFragment1  setUserVisibleHint true
 
 BottomTabFragment1  onCreateView
 BottomTabFragment1  onActivityCreated 
 BottomTabFragment1  onResume  

 BottomTabFragment2  onCreateView
 BottomTabFragment2  onActivityCreated 
 BottomTabFragment2  onResume   
 
 //滑動到 Tab 2
 BottomTabFragment3  setUserVisibleHint false
 BottomTabFragment1  setUserVisibleHint false
 BottomTabFragment2  setUserVisibleHint true

 BottomTabFragment3  onCreateView
 BottomTabFragment3  onActivityCreated 
 BottomTabFragment3 onResume  
 
 //跳過 Tab3 直接選擇 Tab4
 BottomTabFragment4  setUserVisibleHint false
 BottomTabFragment2  setUserVisibleHint false
 BottomTabFragment4  setUserVisibleHint true
 
 BottomTabFragment4  onCreateView
 BottomTabFragment4  onActivityCreated 
 BottomTabFragment4  onResume   
 
 BottomTabFragment2  onPause 
 BottomTabFragment2  onDestroyView
 
 // FragmentPagerStateAdapter 會走一下兩個生命週期方法
 BottomTabFragment2  onDestroy 
 BottomTabFragment2  onDetach  

 BottomTabFragment1  onPause 
 BottomTabFragment1  onDestroyView
 
 // FragmentPagerStateAdapter 會走一下兩個生命週期方法
 BottomTabFragment1  onDestroy 
 BottomTabFragment1  onDetach 
 
 // 用戶回到桌面 再回到當前 APP 打開其餘頁面當前頁面的生命週期也是這樣的
 BottomTabFragment3  onPause 
 BottomTabFragment4  onPause 
 BottomTabFragment3  onStop 
 BottomTabFragment4  onStop 
 
 BottomTabFragment3 onResume  
 BottomTabFragment4 onResume  
 

複製代碼
  • 第二種狀況設置 setOffscreenPageLimit 爲 Pager 的個數時候,左右滑動頁面/或者每次選擇相鄰 tab 的狀況 FragmentPagerAdapter 和 FragmentPagerStateAdapter 沒有區別
BottomTabFragment1  setUserVisibleHint false
  BottomTabFragment2  setUserVisibleHint false
  BottomTabFragment3  setUserVisibleHint false
  BottomTabFragment4  setUserVisibleHint false

  BottomTabFragment1  setUserVisibleHint true
  BottomTabFragment1  onCreateView
  BottomTabFragment1  onActivityCreated 
  BottomTabFragment1 onResume  

  BottomTabFragment2  onCreateView
  BottomTabFragment2  onActivityCreated 

  BottomTabFragment3  onCreateView
  BottomTabFragment3  onActivityCreated 

  BottomTabFragment4  onCreateView
  BottomTabFragment4  onActivityCreated 
  
  BottomTabFragment2 onResume  
  BottomTabFragment3 onResume  
  BottomTabFragment4 onResume 
  
  //選擇 Tab2
  BottomTabFragment1  setUserVisibleHint false
  BottomTabFragment2  setUserVisibleHint true

 //跳過 Tab3 直接選擇 Tab4
  BottomTabFragment2  setUserVisibleHint false
  BottomTabFragment4  setUserVisibleHint true
  
  // 用戶回到桌面 再回到當前 APP 打開其餘頁面當前頁面的生命週期也是這樣的
 BottomTabFragment1  onPause 
 BottomTabFragment2  onPause 
 BottomTabFragment3  onPause 
 BottomTabFragment4  onPause  
 
 BottomTabFragment1 onResume  
 BottomTabFragment2 onResume  
 BottomTabFragment3 onResume  
 BottomTabFragment4 onResume
   
複製代碼

能夠看出第一次執行 setUserVisibleHint(boolean isVisibleToUser) 除了可見的 Fragment 外都爲 false,還能夠看出除了這一點不一樣之外,全部的 Fragment 都走到了生命週期 onResume 階段。而選擇相鄰 tab 的時候已經初始化完成的Fragment 並再也不從新走生命週期方法,只是 setUserVisibleHint(boolean isVisibleToUser) 爲 true。當用戶進入其餘頁面的時候全部 ViewPager 緩存的 Fragment 都會調用 onPause 生命週期函數,當再次回到當前頁面的時候都會調用 onResume。

能發現這一點,其實對於單層 ViewPager 嵌套 Fragment 可見狀態的把握其實已經很明顯了。下面給出個人解決方案:

  1. 對於 Fragment 可見狀態的判斷須要設置兩個標誌位 ,Fragment View 建立完成的標誌位 isViewCreatedFragment 第一次建立的標誌位 mIsFirstVisible

  2. 爲了得到 Fragment 不可見的狀態,和再次回到可見狀態的判斷,咱們還須要增長一個 currentVisibleState 標誌位,該標誌位在 onResume 中和 onPause 中結合 getUserVisibleHint 的返回值來決定是否應該回調可見和不可見狀態函數。

整個可見過程判斷邏輯以下圖所示

接下來咱們就來看下具體實現:

public abstract class LazyLoadBaseFragment extends BaseLifeCircleFragment {

    protected View rootView = null;


    private boolean mIsFirstVisible = true;

    private boolean isViewCreated = false;

    private boolean currentVisibleState = false;


    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        //走這裏分發可見狀態狀況有兩種,1. 已緩存的 Fragment 被展現的時候 2. 當前 Fragment 由可見變成不可見的狀態時
        // 對於默認 tab 和 間隔 checked tab 須要等到 isViewCreated = true 後才能夠經過此通知用戶可見,
        // 這種狀況下第一次可見不是在這裏通知 由於 isViewCreated = false 成立,可見狀態在 onActivityCreated 中分發
        // 對於非默認 tab,View 建立完成  isViewCreated =  true 成立,走這裏分發可見狀態,mIsFirstVisible 此時還爲 false  因此第一次可見狀態也將經過這裏分發
        if (isViewCreated){
            if (isVisibleToUser && !currentVisibleState) {
                dispatchUserVisibleHint(true);
            }else if (!isVisibleToUser && currentVisibleState){
                dispatchUserVisibleHint(false);
            }
        }
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        // 將 View 建立完成標誌位設爲 true
        isViewCreated = true;
        // 默認 Tab getUserVisibleHint() = true !isHidden() = true
        // 對於非默認 tab 或者非默認顯示的 Fragment 在該生命週期中只作了 isViewCreated 標誌位設置 可見狀態將不會在這裏分發
        if (!isHidden() && getUserVisibleHint()){
            dispatchUserVisibleHint(true);
        }

    }


    /**
     * 統一處理 顯示隱藏  作兩件事
     * 設置當前 Fragment 可見狀態 負責在對應的狀態調用第一次可見和可見狀態,不可見狀態函數 
     * @param visible
     */
    private void dispatchUserVisibleHint(boolean visible) {

        currentVisibleState = visible;

        if (visible) {
            if (mIsFirstVisible) {
                mIsFirstVisible = false;
                onFragmentFirstVisible();
            }
            onFragmentResume();
        }else {
            onFragmentPause();
        }
    }

    /**
     * 該方法與 setUserVisibleHint 對應,調用時機是 show,hide 控制 Fragment 隱藏的時候,
     * 注意的是,只有當 Fragment 被建立後再次隱藏顯示的時候纔會調用,第一次 show 的時候是不會回調的。
     */
    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        if (hidden){
            dispatchUserVisibleHint(false);
        }else {
            dispatchUserVisibleHint(true);
        }
    }

    /**
     * 須要再 onResume 中通知用戶可見狀態的狀況是在當前頁面再次可見的狀態 !mIsFirstVisible 能夠保證這一點,
     * 而當前頁面 Activity 可見時全部緩存的 Fragment 都會回調 onResume 
     * 因此咱們須要區分那個Fragment 位於可見狀態 
     * (!isHidden() && !currentVisibleState && getUserVisibleHint())可條件能夠斷定哪一個 Fragment 位於可見狀態
     */
    @Override
    public void onResume() {
        super.onResume();
        if (!mIsFirstVisible){
            if (!isHidden() && !currentVisibleState && getUserVisibleHint()){
                dispatchUserVisibleHint(true);
            }
        }
    }

    /** 
     * 當用戶進入其餘界面的時候全部的緩存的 Fragment 都會 onPause
     * 可是咱們想要知道只是當前可見的的 Fragment 不可見狀態,
     * currentVisibleState && getUserVisibleHint() 可以限定是當前可見的 Fragment
     */
    @Override
    public void onPause() {
        super.onPause();

        if (currentVisibleState && getUserVisibleHint()){
            dispatchUserVisibleHint(false);
        }
    }
    
    
    @Override
    public void onDestroyView() {
        super.onDestroyView();
        //當 View 被銷燬的時候咱們須要從新設置 isViewCreated mIsFirstVisible 的狀態
        isViewCreated = false;
        mIsFirstVisible = true;
    }

    /**
     * 對用戶第一次可見
     */
    public void onFragmentFirstVisible(){
        LogUtils.e(getClass().getSimpleName() + " ");
    }
    
    /**
     *   對用戶可見
     */
    public void onFragmentResume(){
        LogUtils.e(getClass().getSimpleName() + " 對用戶可見");
    }
    
   /**
     *  對用戶不可見
     */
    public void onFragmentPause(){
        LogUtils.e(getClass().getSimpleName() + " 對用戶不可見");
    }


    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater,container,savedInstanceState);

        if (rootView == null) {
            rootView = inflater.inflate(getLayoutRes(), container, false);
        }

        initView(rootView);

        return rootView;
    }

    /**
     * 返回佈局 resId
     *
     * @return layoutId
     */
    protected abstract int getLayoutRes();


    /**
     * 初始化view
     *
     * @param rootView
     */
    protected abstract void initView(View rootView);
}
複製代碼

咱們使以前的 Fragment 改成繼承 LazyLoadBaseFragment 打印 log 能夠看出:

//默認選中第一 Tab
 BottomTabFragment1  setUserVisibleHint false
 BottomTabFragment2  setUserVisibleHint false
 BottomTabFragment3  setUserVisibleHint false
 BottomTabFragment4  setUserVisibleHint false
 BottomTabFragment1  setUserVisibleHint true

 BottomTabFragment1  onCreateView
 BottomTabFragment1  onActivityCreated 
 BottomTabFragment1  對用戶第一次可見
 BottomTabFragment1  對用戶可見
 BottomTabFragment1  onResume  

 BottomTabFragment2  onCreateView
 BottomTabFragment2  onActivityCreated 

 BottomTabFragment3  onCreateView
 BottomTabFragment3  onActivityCreated 

 BottomTabFragment4  onCreateView
 BottomTabFragment4  onActivityCreated 

 BottomTabFragment2 onResume  
 BottomTabFragment3 onResume  
 BottomTabFragment4 onResume  

 //滑動選中 Tab2
 BottomTabFragment1  setUserVisibleHint false
 BottomTabFragment1  對用戶不可見

 BottomTabFragment2  setUserVisibleHint true
 BottomTabFragment2  對用戶第一次可見
 BottomTabFragment2  對用戶可見

 //間隔選中 Tab4
 BottomTabFragment2  setUserVisibleHint false
 BottomTabFragment2  對用戶不可見

 BottomTabFragment4  setUserVisibleHint true
 BottomTabFragment4  對用戶第一次可見
 BottomTabFragment4  對用戶可見


 // 回退到桌面
 BottomTabFragment1  onPause 
 BottomTabFragment2  onPause 
 BottomTabFragment3  onPause 
 BottomTabFragment4  onPause 
 BottomTabFragment4  對用戶不可見

 BottomTabFragment1  onStop 
 BottomTabFragment2  onStop 
 BottomTabFragment3  onStop 
 BottomTabFragment4  onStop 

 // 再次進入 APP
 BottomTabFragment1 onResume  
 BottomTabFragment2 onResume  
 BottomTabFragment3 onResume  
 BottomTabFragment4 onResume  
 BottomTabFragment4  對用戶可見
複製代碼

上述 log 只演示瞭如何 ViewPager 中的函數打印,因爲 hide show 方法顯示隱藏的 Fragment 有人可能認爲不須要懶加載這個東西,若是說從建立來講的確是這樣的,可是若是說全部的 Fragment 已經 add 進 Activity 中,此時 Activity 退到後臺,全部的 Fragment 都會調用 onPause ,而且在其進入前臺的前臺統一會回調 onResume, 若是咱們在 Resume 中作了某些操做,那麼不可見的 Fragment 也會執行,勢必也是個浪費。因此這裏的懶加載吧 hide show 的展現方法也考慮進去。

對於無嵌套的 ViewPager ,懶加載仍是相對簡單的。可是對於ViewPager 嵌套 ViewPager 的狀況可能就出現一些咱們意料不到的狀況。

雙層 ViewPager 嵌套的懶加載實現

對於雙層 ViewPager 嵌套咱們也拿網易雲來舉例:

能夠看出頂層的第二 tab 內部又是一個 ViewPager ,那麼咱們試着按照咱們以前的方案打印一下生命週期過程:

BottomTabFragment1  setUserVisibleHint false
 BottomTabFragment2  setUserVisibleHint false
 
 BottomTabFragment1  setUserVisibleHint true
 BottomTabFragment1  onCreateView
 BottomTabFragment1  onActivityCreated 
 BottomTabFragment1  對用戶第一次可見
 BottomTabFragment1  對用戶可見

 BottomTabFragment1 onResume  
 BottomTabFragment2  onCreateView
 BottomTabFragment2  onActivityCreated 
 BottomTabFragment2 onResume  

 Bottom2InnerFragment1  setUserVisibleHint false
 Bottom2InnerFragment2  setUserVisibleHint false
 Bottom2InnerFragment1  setUserVisibleHint true
 
 //注意這裏 位於第二個Tab 中的 ViewPager 中的第一個 Tab 也走了可見,而它自己並不可見
 Bottom2InnerFragment1  onCreateView
 Bottom2InnerFragment1  onActivityCreated 
 Bottom2InnerFragment1  對用戶第一次可見
 Bottom2InnerFragment1  對用戶可見
 Bottom2InnerFragment1  onResume  

 Bottom2InnerFragment2  onCreateView
 Bottom2InnerFragment2  onActivityCreated 
 Bottom2InnerFragment2 onResume  
複製代碼

咦奇怪的事情發生了,對於外層 ViewPager 的第二個 tab 默認是不顯示的,爲何內部 ViewPager 中的 Bottom2InnerFragment1 卻走了可見了狀態回調。是否是 onActivityCreated 中的寫法有問題,!isHidden() && getUserVisibleHint() getUserVisibleHint() 方法經過 log 打印發如今 Bottom2InnerFragment1 onActivityCreated 時候, Bottom2InnerFragment1 setUserVisibleHint true的確是 true。因此纔會走到分發可見事件中。

咱們再回頭看下上述的生命週期的打印,能夠發現,事實上做爲父 Fragment 的 BottomTabFragment2 並無分發可見事件,他經過 getUserVisibleHint() 獲得的結果爲 false,首先我想到的是能在負責分發事件的方法中判斷一下當前父 fragment 是否可見,若是父 fragment 不可見咱們就不進行可見事件的分發,咱們試着修改 dispatchUserVisibleHint 以下面所示:

private void dispatchUserVisibleHint(boolean visible) {
        //當前 Fragment 是 child 時候 做爲緩存 Fragment 的子 fragment getUserVisibleHint = true
        //但當父 fragment 不可見因此 currentVisibleState = false 直接 returnif (visible && isParentInvisible()) return;
        
        currentVisibleState = visible;

        if (visible) {
            if (mIsFirstVisible) {
                mIsFirstVisible = false;
                onFragmentFirstVisible();
            }
            onFragmentResume();
        } else {
            onFragmentPause();
        }
    }
    
 /**
  * 用於分發可見時間的時候父獲取 fragment 是否隱藏
  * @return true fragment 不可見, false 父 fragment 可見
  */
 private boolean isParentInvisible() {
    LazyLoadBaseFragment fragment = (LazyLoadBaseFragment) getParentFragment();
    return fragment != null && !fragment.isSupportVisible();
   
 }
 
private boolean isSupportVisible() {
    return currentVisibleState;
}
複製代碼

經過日誌打印咱們發現這彷佛起做用了:

BottomTabFragment1  setUserVisibleHint false
 BottomTabFragment2  setUserVisibleHint false
 
 BottomTabFragment1  setUserVisibleHint true
 BottomTabFragment1  onCreateView
 BottomTabFragment1  onActivityCreated 
 BottomTabFragment1  對用戶第一次可見
 BottomTabFragment1  對用戶可見

 BottomTabFragment1 onResume  
 BottomTabFragment2  onCreateView
 BottomTabFragment2  onActivityCreated 
 BottomTabFragment2 onResume  

 Bottom2InnerFragment1  setUserVisibleHint false
 Bottom2InnerFragment2  setUserVisibleHint false
 Bottom2InnerFragment1  setUserVisibleHint true
 
 Bottom2InnerFragment1  onCreateView
 Bottom2InnerFragment1  onActivityCreated 
 Bottom2InnerFragment1  onResume  

 Bottom2InnerFragment2  onCreateView
 Bottom2InnerFragment2  onActivityCreated 
 Bottom2InnerFragment2 onResume  
 
 //滑動到第二個 tab
 BottomTabFragment3  setUserVisibleHint false
 BottomTabFragment1  setUserVisibleHint false
 BottomTabFragment1  對用戶不可見
 
 BottomTabFragment2  setUserVisibleHint true
 BottomTabFragment2  對用戶第一次可見
 BottomTabFragment2  對用戶可見
 
 BottomTabFragment3  onCreateView
 BottomTabFragment3  onActivityCreated 
 BottomTabFragment3 onResume  
複製代碼

可是咱們又發現了新的問題,當咱們滑動到第二個 tab 時候,無疑咱們指望獲得第二個 tab 中內層 ViewPager 第一個 tab 中 fragment 狀態的可見狀態,可是從上邊的 log 能夠發現咱們並無得到其可見狀態的打印,問題出當外層 ViewPager 初始化的時候咱們已經經歷了 Bottom2InnerFragment1 的初始化,而咱們在 dispatchUserVisibleHint 作了攔截,致使其沒法分發可見事件,當其真正可見的時候卻發現事件函數並不會再次被調用了。

本着堅信一切困難都是紙老虎的社會主義光榮理念,我404了一下,發現網上極少的嵌套 fragment 懶加載的文章中,大多都採用了,在父 Fragment 可見的時候,分發本身可見狀態的同時,把本身的可見狀態通知子 Fragment,對於可見狀態的 生命週期調用順序,父 Fragment老是優先於子 Fragment,因此咱們在 Fragment 分發事件的時候,能夠在上述攔截子 Fragment 事件分發後,當在父 Fragment 第一可見的時候,通知子 Fragment 你也可見了。因此我再次修改 dispatchUserVisibleHint,在父 Fragment 分發完成本身的可見事件後,讓子 Fragment 再次調用本身的可見事件分發方法,此次 isParentInvisible() 將會返回 false ,也就是可見狀態將會正確分發。

private void dispatchUserVisibleHint(boolean visible) {
   //當前 Fragment 是 child 時候 做爲緩存 Fragment 的子 fragment getUserVisibleHint = true
   //但當父 fragment 不可見因此 currentVisibleState = false 直接 returnif (visible && isParentInvisible()) return;
   
   currentVisibleState = visible;

   if (visible) {
       if (mIsFirstVisible) {
           mIsFirstVisible = false;
           onFragmentFirstVisible();
       }
       onFragmentResume();
       //可見狀態的時候內層 fragment 生命週期晚於外層 因此在 onFragmentResume 後分發
       dispatchChildVisibleState(true);
   } else {
       onFragmentPause();
       dispatchChildVisibleState(false);

   }
}

 private void dispatchChildVisibleState(boolean visible) {
       FragmentManager childFragmentManager = getChildFragmentManager();
       List<Fragment> fragments = childFragmentManager.getFragments();
       if (!fragments.isEmpty()) {
           for (Fragment child : fragments) {
               // 若是隻有當前 子 fragment getUserVisibleHint() = true 時候分發事件,並將 也就是咱們上面說的 Bottom2InnerFragment1
               if (child instanceof LazyLoadBaseFragment && !child.isHidden() && child.getUserVisibleHint()) {
                   ((LazyLoadBaseFragment) child).dispatchUserVisibleHint(visible);
               }
           }
       }
    }
複製代碼

dispatchChildVisibleState 方法經過 childFragmentManager 獲取當前 Fragment 中全部的子 Fragment 並經過判斷 child.getUserVisibleHint() 的返回值,判斷是否應該通知子 Fragment 不可見,同理在父 Fragment 真正可見的時候,咱們也會經過該方法,通知child.getUserVisibleHint() = true 的子 Fragment 你可見。

咱們再次打印能夠看出通過此次調整內層 Fragment 已經能夠準確地拿到本身第一次可見狀態了。

BottomTabFragment3  setUserVisibleHint false
   BottomTabFragment1  setUserVisibleHint false
   BottomTabFragment1  對用戶不可見
   
   BottomTabFragment2  setUserVisibleHint true
   BottomTabFragment2  對用戶第一次可見
   BottomTabFragment2  對用戶可見
   
   Bottom2InnerFragment1  對用戶第一次可見
   Bottom2InnerFragment1  對用戶可見
   
   BottomTabFragment3  onCreateView
   BottomTabFragment3  onActivityCreated  
   BottomTabFragment3 onResume  
複製代碼

當我覺得紙老虎一進被我大戰勝的時候,我按了下 home 鍵看了條微信,而後發現 log 打印以下:

BottomTabFragment1  onPause 
 
 //Bottom2InnerFragment1 第一不可見回調
 Bottom2InnerFragment1  onPause 
 Bottom2InnerFragment1  對用戶不可見
 
 Bottom2InnerFragment2  onPause 
 BottomTabFragment2  onPause 
 
 BottomTabFragment2  對用戶不可見
 //Bottom2InnerFragment1 第二次不可見回調
 Bottom2InnerFragment1  對用戶不可見
 BottomTabFragment3  onPause 
 BottomTabFragment1  onStop 

 Bottom2InnerFragment1  onStop 
 Bottom2InnerFragment2  onStop 

 BottomTabFragment2  onStop 
 BottomTabFragment3  onStop  
複製代碼

這又是啥狀況? 爲啥回調了兩次,我連微信都忘了回就開始回憶以前分發可見事件的代碼,可見的時候時候沒問題,爲何不可見會回調兩次?後來發現問題出如今事件分發的順序上。

經過日誌打印咱們也能夠看出,對於可見狀態的生命週期調用順序,父 Fragment老是優先於子 Fragment,而對於不可見事件,內部的 Fragment 生命週期老是先於外層 Fragment。因此第一的時候 Bottom2InnerFragment1 調用自身的 dispatchUserVisibleHint 方法分發了不可見事件,做爲父 Fragment 的BottomTabFragment2 分發不可見的時候,又會再次調用 dispatchChildVisibleState ,致使子 Fragment 再次調用本身的 dispatchUserVisibleHint 再次調用了一次 onFragmentPause();

解決辦法也很簡單,還記得 currentVisibleState 這個變量麼? 表示當前 Fragment 的可見狀態,若是當前的 Fragment 要分發的狀態與 currentVisibleState 相同咱們就沒有必要去作分發了。

咱們知道子 Fragment 優於父 Fragment回調本方法 currentVisibleState 置位 false,當前不可見,咱們能夠當父 dispatchChildVisibleState 的時候第二次回調本方法 visible = false 因此此處 visible 將直接返回。

private void dispatchUserVisibleHint(boolean visible) {

   if (visible && isParentInvisible()) 
     return;
     
    // 此處是對子 Fragment 不可見的限制,由於 子 Fragment 先於父 Fragment回調本方法 currentVisibleState 置位 false
   // 當父 dispatchChildVisibleState 的時候第二次回調本方法 visible = false 因此此處 visible 將直接返回
   if (currentVisibleState == visible) {
       return;
   }
        
   currentVisibleState = visible;

   if (visible) {
       if (mIsFirstVisible) {
           mIsFirstVisible = false;
           onFragmentFirstVisible();
       }
       onFragmentResume();
       //可見狀態的時候內層 fragment 生命週期晚於外層 因此在 onFragmentResume 後分發
       dispatchChildVisibleState(true);
   } else {
       onFragmentPause();
       dispatchChildVisibleState(false);
   }
}
複製代碼

對於 Hide And show 方法顯示的 Fragment 驗證這裏講不在過多贅述,上文也說了,對這種 Fragment 展現方法,咱們更須要關注的是 hide 的時候, onPause 和 onResume 再次隱藏顯示的的時候。改方法的驗證能夠經過下載 Demo 查看 log。Demo 地址

最終的實現方案

下面是完整 LazyLoadBaseFragment 實現方案:也能夠直接戳此下載文件 LazyLoadBaseFragment.java

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.List;

/**
 * @author wangshijia
 * @date 2018/2/2
 * Fragment 第一次可見狀態應該在哪裏通知用戶 在 onResume 之後?
 */
public abstract class LazyLoadBaseFragment extends BaseLifeCircleFragment {

    protected View rootView = null;


    private boolean mIsFirstVisible = true;

    private boolean isViewCreated = false;

    private boolean currentVisibleState = false;

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);

        if (rootView == null) {
            rootView = inflater.inflate(getLayoutRes(), container, false);
        }
        initView(rootView);
        return rootView;
    }


    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        // 對於默認 tab 和 間隔 checked tab 須要等到 isViewCreated = true 後才能夠經過此通知用戶可見
        // 這種狀況下第一次可見不是在這裏通知 由於 isViewCreated = false 成立,等從別的界面回到這裏後會使用 onFragmentResume 通知可見
        // 對於非默認 tab mIsFirstVisible = true 會一直保持到選擇則這個 tab 的時候,由於在 onActivityCreated 會返回 false
        if (isViewCreated) {
            if (isVisibleToUser && !currentVisibleState) {
                dispatchUserVisibleHint(true);
            } else if (!isVisibleToUser && currentVisibleState) {
                dispatchUserVisibleHint(false);
            }
        }
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        isViewCreated = true;
        // !isHidden() 默認爲 true  在調用 hide show 的時候可使用
        if (!isHidden() && getUserVisibleHint()) {
            dispatchUserVisibleHint(true);
        }

    }

    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        LogUtils.e(getClass().getSimpleName() + " onHiddenChanged dispatchChildVisibleState hidden " + hidden);

        if (hidden) {
            dispatchUserVisibleHint(false);
        } else {
            dispatchUserVisibleHint(true);
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        if (!mIsFirstVisible) {
            if (!isHidden() && !currentVisibleState && getUserVisibleHint()) {
                dispatchUserVisibleHint(true);
            }
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        // 當前 Fragment 包含子 Fragment 的時候 dispatchUserVisibleHint 內部自己就會通知子 Fragment 不可見
        // 子 fragment 走到這裏的時候自身又會調用一遍 ?
        if (currentVisibleState && getUserVisibleHint()) {
            dispatchUserVisibleHint(false);
        }
    }


    /**
     * 統一處理 顯示隱藏
     *
     * @param visible
     */
    private void dispatchUserVisibleHint(boolean visible) {
        //當前 Fragment 是 child 時候 做爲緩存 Fragment 的子 fragment getUserVisibleHint = true
        //但當父 fragment 不可見因此 currentVisibleState = false 直接 return 掉
        // 這裏限制則能夠限制多層嵌套的時候子 Fragment 的分發
        if (visible && isParentInvisible()) return;

       //此處是對子 Fragment 不可見的限制,由於 子 Fragment 先於父 Fragment回調本方法 currentVisibleState 置位 false
       // 當父 dispatchChildVisibleState 的時候第二次回調本方法 visible = false 因此此處 visible 將直接返回
        if (currentVisibleState == visible) {
            return;
        }

        currentVisibleState = visible;

        if (visible) {
            if (mIsFirstVisible) {
                mIsFirstVisible = false;
                onFragmentFirstVisible();
            }
            onFragmentResume();
            dispatchChildVisibleState(true);
        } else {
            dispatchChildVisibleState(false);
            onFragmentPause();
        }
    }

    /**
     * 用於分發可見時間的時候父獲取 fragment 是否隱藏
     *
     * @return true fragment 不可見, false 父 fragment 可見
     */
    private boolean isParentInvisible() {
        Fragment parentFragment = getParentFragment();
        if (parentFragment instanceof LazyLoadBaseFragment ) {
            LazyLoadBaseFragment fragment = (LazyLoadBaseFragment) parentFragment;
            return !fragment.isSupportVisible();
        }else {
            return false;
        }
    }

    private boolean isSupportVisible() {
        return currentVisibleState;
    }

    /**
     * 當前 Fragment 是 child 時候 做爲緩存 Fragment 的子 fragment 的惟一或者嵌套 VP 的第一 fragment 時 getUserVisibleHint = true
     * 可是因爲父 Fragment 還進入可見狀態因此自身也是不可見的, 這個方法能夠存在是由於慶幸的是 父 fragment 的生命週期回調老是先於子 Fragment
     * 因此在父 fragment 設置完成當前不可見狀態後,須要通知子 Fragment 我不可見,你也不可見,
     * <p>
     * 由於 dispatchUserVisibleHint 中判斷了 isParentInvisible 因此當 子 fragment 走到了 onActivityCreated 的時候直接 return 掉了
     * <p>
     * 當真正的外部 Fragment 可見的時候,走 setVisibleHint (VP 中)或者 onActivityCreated (hide show) 的時候
     * 從對應的生命週期入口調用 dispatchChildVisibleState 通知子 Fragment 可見狀態
     *
     * @param visible
     */
    private void dispatchChildVisibleState(boolean visible) {
        FragmentManager childFragmentManager = getChildFragmentManager();
        List<Fragment> fragments = childFragmentManager.getFragments();
        if (!fragments.isEmpty()) {
            for (Fragment child : fragments) {
                if (child instanceof LazyLoadBaseFragment && !child.isHidden() && child.getUserVisibleHint()) {
                    ((LazyLoadBaseFragment) child).dispatchUserVisibleHint(visible);
                }
            }
        }
    }

    public void onFragmentFirstVisible() {
        LogUtils.e(getClass().getSimpleName() + " 對用戶第一次可見");

    }

    public void onFragmentResume() {
        LogUtils.e(getClass().getSimpleName() + " 對用戶可見");
    }

    public void onFragmentPause() {
        LogUtils.e(getClass().getSimpleName() + " 對用戶不可見");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        isViewCreated = false;
        mIsFirstVisible = true;
    }


    /**
     * 返回佈局 resId
     *
     * @return layoutId
     */
    protected abstract int getLayoutRes();


    /**
     * 初始化view
     *
     * @param rootView
     */
    protected abstract void initView(View rootView);
}
複製代碼

總結

對於 ViewPager Fragment 懶加載網上文章可能已經不少了,可是對於多層 ViewPager + Fragment 嵌套的文章並非不少,上文還原了我本身對 Fragment 懶加載的探索過程,目前該基類已經應用於公司項目中,相信隨着業務的複雜可能有的地方還有可能該方法存在缺陷,若是你們在使用過程當中有問題也請給我留言。

最後感謝 YoKeyword 大神的Fragmentation 提供的思路,一個很好的單 Activity 多 Fragment 庫。

相關文章
相關標籤/搜索