Fragment 可見性監聽方案 - 完美兼容多種 case

前言

本篇文章主要提供一種監聽 Fragment 可見性監聽的方案,完美多種 case,有興趣的能夠看看。廢話很少說,開始進入正文。java

在開發當中, fragment 常用到。在不少應用場景中,咱們須要監聽到 fragment 的顯示與隱藏,來進行一些操做。好比,統計頁面的停留時長,頁面隱藏的時候中止播放視頻。面試

有些同窗可能會說了,這還不容易,直接監聽 Fragment 的 onResume,onPause。我只能說,兄弟,too young,too simple。算法

下面,讓咱們一塊兒來實現 fragment 的監聽。主要分爲幾種 case緩存

  • 一個頁面只有一個 fragment 的,使用 replace微信

  • Hide 和 Show 操做app

  • ViewPager 嵌套 Fragment框架

  • 宿主 Fragment 再嵌套 Fragment,好比 ViewPager 嵌套 ViewPager,再嵌套 Fragmentide

Replace 操做

replace 操做這種比較簡單,由於他會正常調用 onResume 和 onPause 方法,咱們只須要在 onResume 和 onPause 作 check 操做便可this

 1    override fun onResume() {
2        info("onResume")
3        super.onResume()
4        onActivityVisibilityChanged(true)
5    }
6
7
8    override fun onPause() {
9        info("onPause")
10        super.onPause()
11        onActivityVisibilityChanged(false)
12    }

Hide 和 Show 操做

Hide 和 show 操做,會促發生命週期的回調,可是 hide 和 show 操做並不會,那麼咱們能夠經過什麼方法來監聽呢?其實很簡單,能夠經過 onHiddenChanged 方法spa

1    /**
2     * 調用 fragment show hide 的時候回調用這個方法
3     */

4    override fun onHiddenChanged(hidden: Boolean) {
5        super.onHiddenChanged(hidden)
6        checkVisibility(hidden)
7    }

ViewPager 嵌套 Fragment

ViewPager 嵌套 Fragment,這種也是很常見的一種結構。由於 ViewPager 的預加載機制,在 onResume 監聽是不許確的。

這時候,咱們能夠經過 setUserVisibleHint 方法來監聽,當方法傳入值爲true的時候,說明Fragment可見,爲false的時候說明Fragment被切走了

1public void setUserVisibleHint(boolean isVisibleToUser)
2

有一點須要注意的是,個方法可能先於Fragment的生命週期被調用(在FragmentPagerAdapter中,在Fragment被add以前這個方法就被調用了),因此在這個方法中進行操做以前,可能須要先判斷一下生命週期是否執行了。

 1    /**
2     * Tab切換時會回調此方法。對於沒有Tab的頁面,[Fragment.getUserVisibleHint]默認爲true。
3     */

4    @Suppress("DEPRECATION")
5    override fun setUserVisibleHint(isVisibleToUser: Boolean) {
6        info("setUserVisibleHint = $isVisibleToUser")
7        super.setUserVisibleHint(isVisibleToUser)
8        checkVisibility(isVisibleToUser)
9    }
10
11    /**
12     * 檢查可見性是否變化
13     *
14     * @param expected 可見性指望的值。只有當前值和expected不一樣,才須要作判斷
15     */

16    private fun checkVisibility(expected: Boolean) {
17        if (expected == visible) return
18        val parentVisible = if (localParentFragment == null) {
19            parentActivityVisible
20        } else {
21            localParentFragment?.isFragmentVisible() ?: false
22        }
23        val superVisible = super.isVisible()
24        val hintVisible = userVisibleHint
25        val visible = parentVisible && superVisible && hintVisible
26        info(
27                String.format(
28                        "==> checkVisibility = %s  ( parent = %s, super = %s, hint = %s )",
29                        visible, parentVisible, superVisible, hintVisible
30                )
31        )
32        if (visible != this.visible) {
33            this.visible = visible
34            onVisibilityChanged(this.visible)
35        }
36    }

AndroidX 的適配(也是一個坑)

在 AndroidX 當中,FragmentAdapter 和 FragmentStatePagerAdapter 的構造方法,添加一個 behavior 參數實現的。

若是咱們指定不一樣的 behavior,會有不一樣的表現

  1. 當 behavior 爲 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 時,
    ViewPager 中切換 Fragment,setUserVisibleHint 方法將再也不被調用,他會確保 onResume 的正確調用時機

  2. 當 behavior 爲 BEHAVIOR_SET_USER_VISIBLE_HINT,跟以前的方式是一致的,咱們能夠經過 setUserVisibleHint 結合 fragment 的生命週期來監聽

 1//FragmentStatePagerAdapter構造方法
2public FragmentStatePagerAdapter(@NonNull FragmentManager fm,
3        @Behavior int behavior) {
4    mFragmentManager = fm;
5    mBehavior = behavior;
6}
7
8//FragmentPagerAdapter構造方法
9public FragmentPagerAdapter(@NonNull FragmentManager fm,
10        @Behavior int behavior) {
11    mFragmentManager = fm;
12    mBehavior = behavior;
13}
14
15@IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})
16private @interface Behavior { }

既然是這樣,咱們就很好適配呢,直接在 onResume 中調用 checkVisibility 方法,判斷當前 Fragment 是否可見。

回過頭,Behavior 是如何實現的呢?

已 FragmentStatePagerAdapter 爲例,咱們一塊兒開看看源碼

 1@SuppressWarnings({"ReferenceEquality""deprecation"})
2@Override
3public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
4    Fragment fragment = (Fragment)object;
5    if (fragment != mCurrentPrimaryItem) {
6        if (mCurrentPrimaryItem != null) {
7            //當前顯示Fragment
8            mCurrentPrimaryItem.setMenuVisibility(false);
9            if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
10                if (mCurTransaction == null) {
11                    mCurTransaction = mFragmentManager.beginTransaction();
12                }
13                //最大生命週期設置爲STARTED,生命週期回退到onPause
14                mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
15            } else {
16                //可見性設置爲false
17                mCurrentPrimaryItem.setUserVisibleHint(false);
18            }
19        }
20
21        //將要顯示的Fragment
22        fragment.setMenuVisibility(true);
23        if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
24            if (mCurTransaction == null) {
25                mCurTransaction = mFragmentManager.beginTransaction();
26            }
27            //最大 生命週期設置爲RESUMED
28            mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
29        } else {
30            //可見性設置爲true
31            fragment.se tUserVisibleHint(true);
32        }
33
34        //賦值
35        mCurrentPrimaryItem = fragment;
36    }
37}

代碼比較簡單很好理解

  • 當 mBehavior 設置爲 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 會經過 setMaxLifecycle 來修改當前Fragment和將要顯示的Fragment的狀態,使得只有正在顯示的 Fragmen t執行到 onResume() 方法,其餘 Fragment 只會執行到 onStart() 方法,而且當 Fragment 切換到不顯示狀態時觸發 onPause() 方法。

  • 當 mBehavior 設置爲 BEHAVIOR_SET_USER_VISIBLE_HINT 時,會當 frament 可見性發生變化時調用 setUserVisibleHint() ,也就是跟咱們上面提到的第一種懶加載實現方式同樣。

更多詳情,能夠參考這一篇博客Android Fragment + ViewPager的懶加載實現

宿主 Fragment 再嵌套 Fragment

這種 case 也是比較常見的,好比 ViewPager 嵌套 ViewPager,再嵌套 Fragment。

宿主Fragment在生命週期執行的時候會相應的分發到子Fragment中,可是setUserVisibleHint和onHiddenChanged卻沒有進行相應的回調。試想一下,一個ViewPager中有一個FragmentA的tab,而FragmentA中有一個子FragmentB,FragmentA被滑走了,FragmentB並不能接收到setUserVisibleHint事件,onHiddenChange事件也是同樣的。

那有沒有辦法監聽到宿主的 setUserVisibleHint 和 ,onHiddenChange 事件呢?

方法確定是有的。

  1. 第一種方法,宿主 Fragment 提供可見性的回調,子 Fragment 監聽改回調,有點相似於觀察者模式。難點在於子 Fragment 要怎麼拿到宿主 Fragment

  2. 第二種 case,宿主 Fragment 可見性變化的時候,主動去遍歷全部的 子 Fragment,調用 子 Fragment 的相應方法

第一種方法

整體思路是這樣的,宿主 Fragment 提供可見性的回調,子 Fragment 監聽改回調,有點相似於觀察者模式。也有點相似於 Rxjava 中下游持有

第一,咱們先定義一個接口

1interface OnFragmentVisibilityChangedListener {
2    fun onFragmentVisibilityChanged(visible: Boolean)
3}

第二步,在 BaseVisibilityFragment 中提供 addOnVisibilityChangedListener 和 removeOnVisibilityChangedListener 方法,這裏須要注意的是,咱們須要用一個 ArrayList 來保存全部的 listener,由於一個宿主 Fragment 可能有多個子 Fragment。

當 Fragment 可見性變化的時候,會遍歷 List 調用 OnFragmentVisibilityChangedListener 的 onFragmentVisibilityChanged 方法
**

 1open class BaseVisibilityFragment : Fragment(), View.OnAttachStateChangeListener,
2        OnFragmentVisibilityChangedListener {
3
4
5    private val listeners = ArrayList<OnFragmentVisibilityChangedListener>()
6
7    fun addOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) {
8        listener?.apply {
9            listeners.add(this)
10        }
11    }
12
13    fun removeOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) {
14        listener?.apply {
15            listeners.remove(this)
16        }
17    }
18
19    private fun checkVisibility(expected: Boolean) {
20        if (expected == visible) return
21        val parentVisible =
22            if (localParentFragment == null) parentActivityVisible
23            else localParentFragment?.isFragmentVisible() ?: false
24        val superVisible = super.isVisible()
25        val hintVisible = userVisibleHint
26        val visible = parentVisible && superVisible && hintVisible
27
28        if (visible != this.visible) {
29            this.visible = visible
30            listeners.forEach { it ->
31                it.onFragmentVisibilityChanged(visible)
32            }
33            onVisibilityChanged(this.visible)
34        }
35    }

第三步,在 Fragment attach 的時候,咱們經過 getParentFragment 方法,拿到宿主 Fragment,進行監聽。這樣,當宿主 Fragment 可見性變化的時候,子 Fragment 能感應到。

 1override fun onAttach(context: Context) {
2        super.onAttach(context)
3        val parentFragment = parentFragment
4        if (parentFragment != null && parentFragment is BaseVisibilityFragment) {
5            this.localParentFragment = parentFragment
6            info("onAttach, localParentFragment is $localParentFragment")
7            localParentFragment?.addOnVisibilityChangedListener(this)
8        }
9        checkVisibility(true)
10    }

第二種方法

第二種方法,它的實現思路是這樣的,宿主 Fragment 生命週期發生變化的時候,遍歷子 Fragment,調用相應的方法,通知生命週期發生變化

 1//當本身的顯示隱藏狀態改變時,調用這個方法通知子Fragment
2private void notifyChildHiddenChange(boolean hidden) {
3    if (isDetached() || !isAdded()) {
4        return;
5    }
6    FragmentManager fragmentManager = getChildFragmentManager();
7    List<Fragment> fragments = fragmentManager.getFragments();
8    if (fragments == null || fragments.isEmpty()) {
9        return;
10    }
11    for (Fragment fragment : fragments) {
12        if (!(fragment instanceof IPareVisibilityObserver)) {
13            continue;
14        }
15        ((IPareVisibilityObserver) fragment).onParentFragmentHiddenChanged(hidden);
16    }
17}

具體的實現方案,能夠看這一篇博客。獲取和監聽Fragment的可見性

完整代碼

 1/**
2 * Created by jun xu on 2020/11/26.
3 */

4interface OnFragmentVisibilityChangedListener {
5    fun onFragmentVisibilityChanged(visible: Boolean)
6}
7
8
9/**
10 * Created by jun xu on 2020/11/26.
11 *
12 * 支持如下四種 case
13 * 1. 支持 viewPager 嵌套 fragment,主要是經過 setUserVisibleHint 兼容,
14 *  FragmentStatePagerAdapter BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 的 case,由於這時候不會調用 setUserVisibleHint 方法,在 onResume check 能夠兼容
15 * 2. 直接 fragment 直接 add, hide 主要是經過 onHiddenChanged
16 * 3. 直接 fragment 直接 replace ,主要是在 onResume 作判斷
17 * 4. Fragment 裏面用 ViewPager, ViewPager 裏面有多個 Fragment 的,經過 setOnVisibilityChangedListener 兼容,前提是一級 Fragment 和 二級 Fragment 都必須繼承  BaseVisibilityFragment, 且必須用 FragmentPagerAdapter 或者 FragmentStatePagerAdapter
18 * 項目當中一級 ViewPager adapter 比較特殊,不是 FragmentPagerAdapter,也不是 FragmentStatePagerAdapter,致使這種方式用不了
19 */

20open class BaseVisibilityFragment : Fragment(), View.OnAttachStateChangeListener,
21    OnFragmentVisibilityChangedListener {
22
23
24    companion object {
25        const val TAG = "BaseVisibilityFragment"
26    }
27
28    /**
29     * ParentActivity是否可見
30     */

31    private var parentActivityVisible = false
32
33    /**
34     * 是否可見(Activity處於前臺、Tab被選中、Fragment被添加、Fragment沒有隱藏、Fragment.View已經Attach)
35     */

36    private var visible = false
37
38    private var localParentFragment: BaseVisibilityFragment? =
39        null
40    private val listeners = ArrayList<OnFragmentVisibilityChangedListener>()
41
42    fun addOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) {
43        listener?.apply {
44            listeners.add(this)
45        }
46    }
47
48    fun removeOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) {
49        listener?.apply {
50            listeners.remove(this)
51        }
52
53    }
54
55    override fun onAttach(context: Context) {
56        info("onAttach")
57        super.onAttach(context)
58        val parentFragment = parentFragment
59        if (parentFragment != null && parentFragment is BaseVisibilityFragment) {
60            this.localParentFragment = parentFragment
61            localParentFragment?.addOnVisibilityChangedListener(this)
62        }
63        checkVisibility(true)
64    }
65
66    override fun onDetach() {
67        info("onDetach")
68        localParentFragment?.removeOnVisibilityChangedListener(this)
69        super.onDetach()
70        checkVisibility(false)
71        localParentFragment = null
72    }
73
74    override fun onResume() {
75        info("onResume")
76        super.onResume()
77        onActivityVisibilityChanged(true)
78    }
79
80
81    override fun onPause() {
82        info("onPause")
83        super.onPause()
84        onActivityVisibilityChanged(false)
85    }
86
87    /**
88     * ParentActivity可見性改變
89     */

90    protected fun onActivityVisibilityChanged(visible: Boolean) {
91        parentActivityVisible = visible
92        checkVisibility(visible)
93    }
94
95    /**
96     * ParentFragment可見性改變
97     */

98    override fun onFragmentVisibilityChanged(visible: Boolean) {
99        checkVisibility(visible)
100    }
101
102    override fun onCreate(savedInstanceState: Bundle?) {
103        info("onCreate")
104        super.onCreate(savedInstanceState)
105    }
106
107    override fun onViewCreated(
108        view: View,
109        savedInstanceState: Bundle?
110    )
 {
111        super.onViewCreated(view, savedInstanceState)
112        // 處理直接 replace 的 case
113        view.addOnAttachStateChangeListener(this)
114    }
115
116    /**
117     * 調用 fragment add hide 的時候回調用這個方法
118     */

119    override fun onHiddenChanged(hidden: Boolean) {
120        super.onHiddenChanged(hidden)
121        checkVisibility(hidden)
122    }
123
124    /**
125     * Tab切換時會回調此方法。對於沒有Tab的頁面,[Fragment.getUserVisibleHint]默認爲true。
126     */

127    override fun setUserVisibleHint(isVisibleToUser: Boolean) {
128        info("setUserVisibleHint = $isVisibleToUser")
129        super.setUserVisibleHint(isVisibleToUser)
130        checkVisibility(isVisibleToUser)
131    }
132
133    override fun onViewAttachedToWindow(v: View?) {
134        info("onViewAttachedToWindow")
135        checkVisibility(true)
136    }
137
138    override fun onViewDetachedFromWindow(v: View) {
139        info("onViewDetachedFromWindow")
140        v.removeOnAttachStateChangeListener(this)
141        checkVisibility(false)
142    }
143
144    /**
145     * 檢查可見性是否變化
146     *
147     * @param expected 可見性指望的值。只有當前值和expected不一樣,才須要作判斷
148     */

149    private fun checkVisibility(expected: Boolean) {
150        if (expected == visible) return
151        val parentVisible =
152            if (localParentFragment == null) parentActivityVisible
153            else localParentFragment?.isFragmentVisible() ?: false
154        val superVisible = super.isVisible()
155        val hintVisible = userVisibleHint
156        val visible = parentVisible && superVisible && hintVisible
157        info(
158            String.format(
159                "==> checkVisibility = %s  ( parent = %s, super = %s, hint = %s )",
160                visible, parentVisible, superVisible, hintVisible
161            )
162        )
163        if (visible != this.visible) {
164            this.visible = visible
165            onVisibilityChanged(this.visible)
166        }
167    }
168
169    /**
170     * 可見性改變
171     */

172    protected fun onVisibilityChanged(visible: Boolean) {
173        info("==> onVisibilityChanged = $visible")
174        listeners.forEach {
175            it.onFragmentVisibilityChanged(visible)
176        }
177    }
178
179    /**
180     * 是否可見(Activity處於前臺、Tab被選中、Fragment被添加、Fragment沒有隱藏、Fragment.View已經Attach)
181     */

182    fun isFragmentVisible()Boolean {
183        return visible
184    }
185
186    private fun info(s: String) {
187        Log.i(TAG, "${this.javaClass.simpleName} ; $s ; this is $this")
188    }
189
190
191}

題外話

今年有好長時間沒有更新技術博客了,主要是比較忙。拖着拖着,就懶得更新了。

這邊博客的技術含量其實不高,主要是適配。

  1. AndroidX FragmentAdapter behavior 的適配

  2. 宿主 Fragment 嵌套 Fragment,提供了兩種方式解決,一種是自上而下的,一種是自上而下的。借鑑了 Rxjava 的設計思想,下游持有上游的引用,從而控制 Obverable 的回調線程。Obsever 會有下游 Observer 的引用,從而進行一些轉換操做,好比 map,FlatMap 操做符

  3. 若是你使用中遇到坑,也歡迎隨時 call 我,咱們一塊兒解決。若是你有更好的方案,也歡迎隨時跟我交流

往期文章

面試官:簡歷上最好不要寫Glide,不是問源碼那麼簡單

常見的鏈表翻轉,字節跳動加了個條件,面試者高呼「我太難了」| 圖解算法

面試官,怎樣實現 Router 框架?

面試 Google, 我失敗了 | Google 面經分享

面試官系列- 你真的瞭解 http 嗎

java 版劍指offer集錦

面試官系列 - LeetCode鏈表知識點&題型總結

RecyclerView緩存機制(咋複用?)

徐公自敘


若是你以爲對你有所幫助的話,能夠關注個人公衆號 徐公碼字(stormjun94),第一時間會在上面更新




本文分享自微信公衆號 - 徐公碼字(stormjun94)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索