您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關Android 中怎么利用Fragment實現懶加載,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
多層嵌套后的 Fragment 懶加載
印象中從 Feed 流應用流行開始,Fragment
懶加載變成了一個大家都需要關注的開發知識,關于 Fragment
的懶加載,網上有很多例子,GitHub 上也有很多例子,就連我自己在一年前也寫過相關的文章。但是之前的應用可能最多的是一層 Activity + ViewPager
的 UI 層次,但是隨著頁面越來越復雜,越來越多的應用首頁一個頁面外層是一個 ViewPager
內部可能還嵌套著一層 ViewPager
,這是之前的懶加載就可能不那么好用了。本文對于多層 ViewPager
的嵌套使用過程中,Fragment
主要的三個狀態:第一次可見,每次可見,每次不可見,提供解決方案。
為什么要使用懶加載
在我們開發中經常會使用 ViewPager + Fragment
來創建多 tab 的頁面,此時在 ViewPager
內部默認會幫我們緩存當頁面前后兩個頁面的 Fragment
內容,如果使用了 setOffscreenPageLimit
方法,那么 ViewPager
初始化的時候將會緩存對應參數個 Fragment。為了增加用戶體驗我們往往會使用該方法來保證加載過的頁面不被銷毀,并留離開 tab 之前的狀態(列表滑動距離等),而我們在使用 Fragment
的時候往往在創建完 View
后,就會開始網絡請求等操作,如果存在上述的需求時,懶加載就顯得尤為重要了,不僅可以節省用戶流量,還可以在提高應用性能,給用戶帶來更加的體驗。
ViewPager + Fragment
的懶加載實質上我們就在做三件事,就可以將上邊所說的效果實現,那么就是找到每個 Fragment
第一對用戶可見的時機,和每次 Fragment 對用戶可見時機,以及每次 Framgment
對用戶不可見的時機,來暴露給實現實現類做對應的網絡請求或者網絡請求中斷時機。下面我們就來從常見的幾種 UI 結構上一步步實現無論嵌套多少層,無論開發者使用的 hide show
還是 ViewPager
嵌套都能準確獲取這三種狀態的時機的一種懶加載實現方案。
單層 ViewPager + Fragment 懶加載
我們都知道 Fragment 生命周期按先后順序有
onAttach -> onCreate -> onCreatedView -> onActivityCreated -> onStart -> onResume -> onPause -> onStop -> onDestroyView -> onDestroy -> onDetach
對于 ViewPager + Fragment 的實現我們需要關注的幾個生命周期有:
onCreatedView + onActivityCreated + onResume + onPause + onDestroyView
以及非生命周期函數:
setUserVisibleHint + onHiddenChanged
對于單層 ViewPager + Fragment 可能是我們最常用的頁面結構了,如網易云音樂的首頁頂部的是三個 tab ,我們那網易云音樂作為例子:
對于這種 ViewPager + Fragment 結構,我們使用的過程中一般只包含是 4 種情況分別是:
1、使用 FragmentPagerAdapter
,FragmentPagerStateAdapter
不設置 setOffscreenPageLimit
左右滑動頁面,每次只緩存下一個 Pager ,和上一個 Pager
間隔的點擊 tab 如從位于 tab1 的時候直接選擇 tab3 或 tab4
2、使用 FragmentPagerAdapter
,FragmentPagerStateAdapter
設置 setOffscreenPageLimit
為 tab 總數
左右滑動頁面,每次只緩存下一個 Pager ,和上一個 Pager
間隔的點擊 tab 如從位于 tab1 的時候直接選擇 tab3 或 tab4
3、進入其他頁面或者用戶按 home 鍵回到桌面,當前 ViewPager 頁面變成不見狀態。
對于 FragmentPagerAdapter
和 FragmentPagerStateAdapter
的區別在于在于,前者在 Fragment
不見的時候將不會 detach
,而后者將會銷毀 Fragment
并 detach
掉。
實際上這也是所有 ViewPager
的操作情況。
第一種情況不設置 setOffscreenPageLimit
左右滑動頁面/或者每次選擇相鄰 tab 的情況 FragmentPagerAdapter
和 FragmentPagerStateAdapter
有所區別
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 可見狀態的把握其實已經很明顯了。下面給出我的解決方案:
對于 Fragment 可見狀態的判斷需要設置兩個標志位 ,Fragment View 創建完成的標志位 isViewCreated
和 Fragment
第一次創建的標志位 mIsFirstVisible
為了獲得 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 直接 return 掉 if (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 直接 return 掉 if (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() { LazyLoadBaseFragment fragment = (LazyLoadBaseFragment) getParentFragment(); return fragment != null && !fragment.isSupportVisible(); } 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); }
上述就是小編為大家分享的Android 中怎么利用Fragment實現懶加載了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。