Android x 來襲,F(xiàn)ragment 可見性監(jiān)聽方案 - 完美兼容多種 case

我的公眾號徐公,四年中大廠工作經驗,回復黑馬,領取 Android 學習視頻一份,回復徐公666,可以獲得我精心整理的簡歷模板,帶你走近大廠。
前言
本篇文章主要提供一種監(jiān)聽 Fragment 可見性監(jiān)聽的方案,完美多種 case,有興趣的可以看看。廢話不多說,開始進入正文。
在開發(fā)當中, fragment 經常使用到。在很多應用場景中,我們需要監(jiān)聽到 fragment 的顯示與隱藏,來進行一些操作。比如,統(tǒng)計頁面的停留時長,頁面隱藏的時候停止播放視頻。
有些同學可能會說了,這還不容易,直接監(jiān)聽 Fragment 的 onResume,onPause。我只能說,兄弟,too young,too simple。
下面,讓我們一起來實現(xiàn) fragment 的監(jiān)聽。主要分為幾種 case
一個頁面只有一個 fragment 的,使用 replace
Hide 和 Show 操作
ViewPager 嵌套 Fragment
宿主 Fragment 再嵌套 Fragment,比如 ViewPager 嵌套 ViewPager,再嵌套 Fragment
Replace 操作
replace 操作這種比較簡單,因為他會正常調用 onResume 和 onPause 方法,我們只需要在 onResume 和 onPause 做 check 操作即可
????override?fun?onResume()?{
????????info("onResume")
????????super.onResume()
????????onActivityVisibilityChanged(true)
????}
????override?fun?onPause()?{
????????info("onPause")
????????super.onPause()
????????onActivityVisibilityChanged(false)
????}
Hide 和 Show 操作
Hide 和 show 操作,會促發(fā)生命周期的回調,但是 hide 和 show 操作并不會,那么我們可以通過什么方法來監(jiān)聽呢?其實很簡單,可以通過 onHiddenChanged 方法
????/**
?????*?調用?fragment?show?hide?的時候回調用這個方法
?????*/
????override?fun?onHiddenChanged(hidden:?Boolean)?{
????????super.onHiddenChanged(hidden)
????????checkVisibility(hidden)
????}
ViewPager 嵌套 Fragment
ViewPager 嵌套 Fragment,這種也是很常見的一種結構。因為 ViewPager 的預加載機制,在 onResume 監(jiān)聽是不準確的。
這時候,我們可以通過 setUserVisibleHint 方法來監(jiān)聽,當方法傳入值為true的時候,說明Fragment可見,為false的時候說明Fragment被切走了
public?void?setUserVisibleHint(boolean?isVisibleToUser)
有一點需要注意的是,個方法可能先于Fragment的生命周期被調用(在FragmentPagerAdapter中,在Fragment被add之前這個方法就被調用了),所以在這個方法中進行操作之前,可能需要先判斷一下生命周期是否執(zhí)行了。
????/**
?????*?Tab切換時會回調此方法。對于沒有Tab的頁面,[Fragment.getUserVisibleHint]默認為true。
?????*/
????
????override?fun?setUserVisibleHint(isVisibleToUser:?Boolean)?{
????????info("setUserVisibleHint?=?$isVisibleToUser")
????????super.setUserVisibleHint(isVisibleToUser)
????????checkVisibility(isVisibleToUser)
????}
????/**
?????*?檢查可見性是否變化
?????*
?????*?@param?expected?可見性期望的值。只有當前值和expected不同,才需要做判斷
?????*/
????private?fun?checkVisibility(expected:?Boolean)?{
????????if?(expected?==?visible)?return
????????val?parentVisible?=?if?(localParentFragment?==?null)?{
????????????parentActivityVisible
????????}?else?{
????????????localParentFragment?.isFragmentVisible()??:?false
????????}
????????val?superVisible?=?super.isVisible()
????????val?hintVisible?=?userVisibleHint
????????val?visible?=?parentVisible?&&?superVisible?&&?hintVisible
????????info(
????????????????String.format(
????????????????????????"==>?checkVisibility?=?%s??(?parent?=?%s,?super?=?%s,?hint?=?%s?)",
????????????????????????visible,?parentVisible,?superVisible,?hintVisible
????????????????)
????????)
????????if?(visible?!=?this.visible)?{
????????????this.visible?=?visible
????????????onVisibilityChanged(this.visible)
????????}
????}
AndroidX 的適配(也是一個坑)
在 AndroidX 當中,F(xiàn)ragmentAdapter 和 FragmentStatePagerAdapter 的構造方法,添加一個 behavior 參數(shù)實現(xiàn)的。
如果我們指定不同的 behavior,會有不同的表現(xiàn)
當 behavior 為 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 時,ViewPager 中切換 Fragment,setUserVisibleHint 方法將不再被調用,他會確保 onResume 的正確調用時機
當 behavior 為 BEHAVIOR_SET_USER_VISIBLE_HINT,跟之前的方式是一致的,我們可以通過 setUserVisibleHint 結合 fragment 的生命周期來監(jiān)聽
//FragmentStatePagerAdapter構造方法
public?FragmentStatePagerAdapter( ?FragmentManager?fm,
???????? ?int?behavior)?{
????mFragmentManager?=?fm;
????mBehavior?=?behavior;
}
//FragmentPagerAdapter構造方法
public?FragmentPagerAdapter( ?FragmentManager?fm,
???????? ?int?behavior)?{
????mFragmentManager?=?fm;
????mBehavior?=?behavior;
}
private? ?Behavior?{?}
既然是這樣,我們就很好適配呢,直接在 onResume 中調用 checkVisibility 方法,判斷當前 Fragment 是否可見。
回過頭,Behavior 是如何實現(xiàn)的呢?
已 FragmentStatePagerAdapter 為例,我們一起開看看源碼?
public?void?setPrimaryItem( ?ViewGroup?container,?int?position,? ?Object?object)?{
????Fragment?fragment?=?(Fragment)object;
????if?(fragment?!=?mCurrentPrimaryItem)?{
????????if?(mCurrentPrimaryItem?!=?null)?{
????????????//當前顯示Fragment
????????????mCurrentPrimaryItem.setMenuVisibility(false);
????????????if?(mBehavior?==?BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)?{
????????????????if?(mCurTransaction?==?null)?{
????????????????????mCurTransaction?=?mFragmentManager.beginTransaction();
????????????????}
????????????????//最大生命周期設置為STARTED,生命周期回退到onPause
????????????????mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem,?Lifecycle.State.STARTED);
????????????}?else?{
????????????????//可見性設置為false
????????????????mCurrentPrimaryItem.setUserVisibleHint(false);
????????????}
????????}
????????//將要顯示的Fragment
????????fragment.setMenuVisibility(true);
????????if?(mBehavior?==?BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)?{
????????????if?(mCurTransaction?==?null)?{
????????????????mCurTransaction?=?mFragmentManager.beginTransaction();
????????????}
????????????//最大?生命周期設置為RESUMED
????????????mCurTransaction.setMaxLifecycle(fragment,?Lifecycle.State.RESUMED);
????????}?else?{
????????????//可見性設置為true
????????????fragment.se?tUserVisibleHint(true);
????????}
????????//賦值
????????mCurrentPrimaryItem?=?fragment;
????}
}
代碼比較簡單很好理解
當 mBehavior 設置為 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 會通過 setMaxLifecycle 來修改當前Fragment和將要顯示的Fragment的狀態(tài),使得只有正在顯示的 Fragmen t執(zhí)行到 onResume() 方法,其他 Fragment 只會執(zhí)行到 onStart() 方法,并且當 Fragment 切換到不顯示狀態(tài)時觸發(fā) onPause() 方法。
當 mBehavior 設置為 BEHAVIOR_SET_USER_VISIBLE_HINT 時,會當 frament 可見性發(fā)生變化時調用 setUserVisibleHint() ,也就是跟我們上面提到的第一種懶加載實現(xiàn)方式一樣。
更多詳情,可以參考這一篇博客Android Fragment + ViewPager的懶加載實現(xiàn)
宿主 Fragment 再嵌套 Fragment
這種 case 也是比較常見的,比如 ViewPager 嵌套 ViewPager,再嵌套 Fragment。
宿主Fragment在生命周期執(zhí)行的時候會相應的分發(fā)到子Fragment中,但是setUserVisibleHint和onHiddenChanged卻沒有進行相應的回調。試想一下,一個ViewPager中有一個FragmentA的tab,而FragmentA中有一個子FragmentB,F(xiàn)ragmentA被滑走了,F(xiàn)ragmentB并不能接收到setUserVisibleHint事件,onHiddenChange事件也是一樣的。
那有沒有辦法監(jiān)聽到宿主的 setUserVisibleHint 和 ,onHiddenChange 事件呢?
方法肯定是有的。
第一種方法,宿主 Fragment 提供可見性的回調,子 Fragment 監(jiān)聽改回調,有點類似于觀察者模式。難點在于子 Fragment 要怎么拿到宿主 Fragment
第二種 case,宿主 Fragment 可見性變化的時候,主動去遍歷所有的 子 Fragment,調用 子 Fragment 的相應方法
第一種方法
總體思路是這樣的,宿主 Fragment 提供可見性的回調,子 Fragment 監(jiān)聽改回調,有點類似于觀察者模式。也有點類似于 Rxjava 中下游持有
第一,我們先定義一個接口
interface?OnFragmentVisibilityChangedListener?{
????fun?onFragmentVisibilityChanged(visible:?Boolean)
}
第二步,在 BaseVisibilityFragment 中提供 addOnVisibilityChangedListener 和 removeOnVisibilityChangedListener 方法,這里需要注意的是,我們需要用一個 ArrayList 來保存所有的 listener,因為一個宿主 Fragment 可能有多個子 Fragment。
當 Fragment 可見性變化的時候,會遍歷 List 調用 OnFragmentVisibilityChangedListener 的 onFragmentVisibilityChanged 方法
**
open?class?BaseVisibilityFragment?:?Fragment(),?View.OnAttachStateChangeListener,
????????OnFragmentVisibilityChangedListener?{
????private?val?listeners?=?ArrayList<OnFragmentVisibilityChangedListener>()
????fun?addOnVisibilityChangedListener(listener:?OnFragmentVisibilityChangedListener?)?{
????????listener?.apply?{
????????????listeners.add(this)
????????}
????}
????fun?removeOnVisibilityChangedListener(listener:?OnFragmentVisibilityChangedListener?)?{
????????listener?.apply?{
????????????listeners.remove(this)
????????}
????}
????private?fun?checkVisibility(expected:?Boolean)?{
????????if?(expected?==?visible)?return
????????val?parentVisible?=
????????????if?(localParentFragment?==?null)?parentActivityVisible
????????????else?localParentFragment?.isFragmentVisible()??:?false
????????val?superVisible?=?super.isVisible()
????????val?hintVisible?=?userVisibleHint
????????val?visible?=?parentVisible?&&?superVisible?&&?hintVisible
????????if?(visible?!=?this.visible)?{
????????????this.visible?=?visible
????????????listeners.forEach?{?it?->
????????????????it.onFragmentVisibilityChanged(visible)
????????????}
????????????onVisibilityChanged(this.visible)
????????}
????}
第三步,在 Fragment attach 的時候,我們通過 getParentFragment 方法,拿到宿主 Fragment,進行監(jiān)聽。這樣,當宿主 Fragment 可見性變化的時候,子 Fragment 能感應到。
override?fun?onAttach(context:?Context)?{
????????super.onAttach(context)
????????val?parentFragment?=?parentFragment
????????if?(parentFragment?!=?null?&&?parentFragment?is?BaseVisibilityFragment)?{
????????????this.localParentFragment?=?parentFragment
????????????info("onAttach,?localParentFragment?is?$localParentFragment")
????????????localParentFragment?.addOnVisibilityChangedListener(this)
????????}
????????checkVisibility(true)
????}
第二種方法
第二種方法,它的實現(xiàn)思路是這樣的,宿主 Fragment 生命周期發(fā)生變化的時候,遍歷子 Fragment,調用相應的方法,通知生命周期發(fā)生變化
//當自己的顯示隱藏狀態(tài)改變時,調用這個方法通知子Fragment
private?void?notifyChildHiddenChange(boolean?hidden)?{
????if?(isDetached()?||?!isAdded())?{
????????return;
????}
????FragmentManager?fragmentManager?=?getChildFragmentManager();
????List<Fragment>?fragments?=?fragmentManager.getFragments();
????if?(fragments?==?null?||?fragments.isEmpty())?{
????????return;
????}
????for?(Fragment?fragment?:?fragments)?{
????????if?(!(fragment?instanceof?IPareVisibilityObserver))?{
????????????continue;
????????}
????????((IPareVisibilityObserver)?fragment).onParentFragmentHiddenChanged(hidden);
????}
}
具體的實現(xiàn)方案,可以看這一篇博客。獲取和監(jiān)聽Fragment的可見性
完整代碼
/**
?*?Created?by?jun?xu?on?2020/11/26.
?*/
interface?OnFragmentVisibilityChangedListener?{
????fun?onFragmentVisibilityChanged(visible:?Boolean)
}
/**
?*?Created?by?jun?xu?on?2020/11/26.
?*
?*?支持以下四種?case
?*?1.?支持?viewPager?嵌套?fragment,主要是通過?setUserVisibleHint?兼容,
?*??FragmentStatePagerAdapter?BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT?的?case,因為這時候不會調用?setUserVisibleHint?方法,在?onResume?check?可以兼容
?*?2.?直接?fragment?直接?add,?hide?主要是通過?onHiddenChanged
?*?3.?直接?fragment?直接?replace?,主要是在?onResume?做判斷
?*?4.?Fragment?里面用?ViewPager,?ViewPager?里面有多個?Fragment?的,通過?setOnVisibilityChangedListener?兼容,前提是一級?Fragment?和?二級?Fragment?都必須繼承??BaseVisibilityFragment,?且必須用?FragmentPagerAdapter?或者?FragmentStatePagerAdapter
?*?項目當中一級?ViewPager?adapter?比較特殊,不是?FragmentPagerAdapter,也不是?FragmentStatePagerAdapter,導致這種方式用不了
?*/
open?class?BaseVisibilityFragment?:?Fragment(),?View.OnAttachStateChangeListener,
????OnFragmentVisibilityChangedListener?{
????companion?object?{
????????const?val?TAG?=?"BaseVisibilityFragment"
????}
????/**
?????*?ParentActivity是否可見
?????*/
????private?var?parentActivityVisible?=?false
????/**
?????*?是否可見(Activity處于前臺、Tab被選中、Fragment被添加、Fragment沒有隱藏、Fragment.View已經Attach)
?????*/
????private?var?visible?=?false
????private?var?localParentFragment:?BaseVisibilityFragment??=
????????null
????private?val?listeners?=?ArrayList<OnFragmentVisibilityChangedListener>()
????fun?addOnVisibilityChangedListener(listener:?OnFragmentVisibilityChangedListener?)?{
????????listener?.apply?{
????????????listeners.add(this)
????????}
????}
????fun?removeOnVisibilityChangedListener(listener:?OnFragmentVisibilityChangedListener?)?{
????????listener?.apply?{
????????????listeners.remove(this)
????????}
????}
????override?fun?onAttach(context:?Context)?{
????????info("onAttach")
????????super.onAttach(context)
????????val?parentFragment?=?parentFragment
????????if?(parentFragment?!=?null?&&?parentFragment?is?BaseVisibilityFragment)?{
????????????this.localParentFragment?=?parentFragment
????????????localParentFragment?.addOnVisibilityChangedListener(this)
????????}
????????checkVisibility(true)
????}
????override?fun?onDetach()?{
????????info("onDetach")
????????localParentFragment?.removeOnVisibilityChangedListener(this)
????????super.onDetach()
????????checkVisibility(false)
????????localParentFragment?=?null
????}
????override?fun?onResume()?{
????????info("onResume")
????????super.onResume()
????????onActivityVisibilityChanged(true)
????}
????override?fun?onPause()?{
????????info("onPause")
????????super.onPause()
????????onActivityVisibilityChanged(false)
????}
????/**
?????*?ParentActivity可見性改變
?????*/
????protected?fun?onActivityVisibilityChanged(visible:?Boolean)?{
????????parentActivityVisible?=?visible
????????checkVisibility(visible)
????}
????/**
?????*?ParentFragment可見性改變
?????*/
????override?fun?onFragmentVisibilityChanged(visible:?Boolean)?{
????????checkVisibility(visible)
????}
????override?fun?onCreate(savedInstanceState:?Bundle?)?{
????????info("onCreate")
????????super.onCreate(savedInstanceState)
????}
????override?fun?onViewCreated(
????????view:?View,
????????savedInstanceState:?Bundle?
????)?{
????????super.onViewCreated(view,?savedInstanceState)
????????//?處理直接?replace?的?case
????????view.addOnAttachStateChangeListener(this)
????}
????/**
?????*?調用?fragment?add?hide?的時候回調用這個方法
?????*/
????override?fun?onHiddenChanged(hidden:?Boolean)?{
????????super.onHiddenChanged(hidden)
????????checkVisibility(hidden)
????}
????/**
?????*?Tab切換時會回調此方法。對于沒有Tab的頁面,[Fragment.getUserVisibleHint]默認為true。
?????*/
????override?fun?setUserVisibleHint(isVisibleToUser:?Boolean)?{
????????info("setUserVisibleHint?=?$isVisibleToUser")
????????super.setUserVisibleHint(isVisibleToUser)
????????checkVisibility(isVisibleToUser)
????}
????override?fun?onViewAttachedToWindow(v:?View?)?{
????????info("onViewAttachedToWindow")
????????checkVisibility(true)
????}
????override?fun?onViewDetachedFromWindow(v:?View)?{
????????info("onViewDetachedFromWindow")
????????v.removeOnAttachStateChangeListener(this)
????????checkVisibility(false)
????}
????/**
?????*?檢查可見性是否變化
?????*
?????*?@param?expected?可見性期望的值。只有當前值和expected不同,才需要做判斷
?????*/
????private?fun?checkVisibility(expected:?Boolean)?{
????????if?(expected?==?visible)?return
????????val?parentVisible?=
????????????if?(localParentFragment?==?null)?parentActivityVisible
????????????else?localParentFragment?.isFragmentVisible()??:?false
????????val?superVisible?=?super.isVisible()
????????val?hintVisible?=?userVisibleHint
????????val?visible?=?parentVisible?&&?superVisible?&&?hintVisible
????????info(
????????????String.format(
????????????????"==>?checkVisibility?=?%s??(?parent?=?%s,?super?=?%s,?hint?=?%s?)",
????????????????visible,?parentVisible,?superVisible,?hintVisible
????????????)
????????)
????????if?(visible?!=?this.visible)?{
????????????this.visible?=?visible
????????????onVisibilityChanged(this.visible)
????????}
????}
????/**
?????*?可見性改變
?????*/
????protected?fun?onVisibilityChanged(visible:?Boolean)?{
????????info("==>?onVisibilityChanged?=?$visible")
????????listeners.forEach?{
????????????it.onFragmentVisibilityChanged(visible)
????????}
????}
????/**
?????*?是否可見(Activity處于前臺、Tab被選中、Fragment被添加、Fragment沒有隱藏、Fragment.View已經Attach)
?????*/
????fun?isFragmentVisible():?Boolean?{
????????return?visible
????}
????private?fun?info(s:?String)?{
????????Log.i(TAG,?"${this.javaClass.simpleName}?;?$s?;?this?is?$this")
????}
}
題外話
今年有好長時間沒有更新技術博客了,主要是比較忙。拖著拖著,就懶得更新了。
這邊博客的技術含量其實不高,主要是適配。
AndroidX FragmentAdapter behavior 的適配
宿主 Fragment 嵌套 Fragment,提供了兩種方式解決,一種是自上而下的,一種是自上而下的。借鑒了 Rxjava 的設計思想,下游持有上游的引用,從而控制 Obverable 的回調線程。Obsever 會有下游 Observer 的引用,從而進行一些轉換操作,比如 map,F(xiàn)latMap 操作符
如果你使用中遇到坑,也歡迎隨時 call 我,我們一起解決。如果你有更好的方案,也歡迎隨時跟我交流