獲取和監聽Fragment的可見性

  在不少應用場景中,須要監聽頁面的顯示或者隱藏,好比產品想統計用戶在頁面的停留時間,須要在頁面隱藏的時候上報。又或者在頁面隱藏的時候須要中止頁面上正在播放的視頻。咱們的界面中有一些是Activity實現的,有一些是Fragment實現的。不一樣於Activity,Fragment能夠嵌套使用,能夠靈活地顯示隱藏,這就給開發者帶來一個難題,就是咱們如何有效地監聽Fragment的顯示隱藏?筆者在開發過程當中對這個問題也研究了一番,分享出來但願對你們有用。首先會分四種狀況下如何監聽Fragment的顯示隱藏。而後會介紹一下咱們是如何實現統一的監聽的。最後簡單說下在Fragment嵌套中正確的使用姿式。java

1、Fragment顯示在屏幕上的幾種狀況

  不一樣的狀況下監聽Fragment顯示隱藏的方法不同,下面咱們就分四種狀況進行說明,分別是:生命週期引發的顯示隱藏、ViewPager滑動引發的Fragment顯示隱藏、監聽Hide/Show操做、宿主Fragment的顯示隱藏。android

一、生命週期。

  生命週期的狀況比較明確,那就是監聽onPause和onResume這兩個生命週期。這裏稍微提一下這兩個生命週期在何時會被觸發。通常而言,有兩種狀況會執行這兩個生命週期:git

  • 宿主Activity/Fragment的生命週期變化 若是Fragment直接嵌入在Activity,那麼Activity會在生命週期中調用FragmentController分發相應的生命週期變化,FragmentController再調用FragmentManager的方法進行分發
/** * Moves all Fragments managed by the controller's FragmentManager * into the pause state. * <p>Call when Fragments should be paused. * * @see Fragment#onPause() */
public void dispatchPause() {
    mHost.mFragmentManager.dispatchPause();
}
複製代碼

若是Fragment是嵌套在其餘Fragment中,那麼宿主Fragment會在生命週期中調用ChildFragmentManager的方法進行分發:github

void performPause() {
    if (mView != null) {
        mViewLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
    }
    mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
    if (mChildFragmentManager != null) {
        mChildFragmentManager.dispatchPause();
    }
    mState = STARTED;
    mCalled = false;
    onPause();
    if (!mCalled) {
        throw new SuperNotCalledException("Fragment " + this
                    + " did not call through to super.onPause()");
    }
}
複製代碼
  • 執行了Remove、Relace、Detach/Attach的操做 比較須要注意的是Detach和Attach操做的生命週期,執行Detach和Attach後生命週期變化爲
cn.cocoder.fragmentvisibility I/FragmentVisibility: onPause
cn.cocoder.fragmentvisibility I/FragmentVisibility: onStop
cn.cocoder.fragmentvisibility I/FragmentVisibility: onDestroyView

cn.cocoder.fragmentvisibility I/FragmentVisibility: onCreateView
cn.cocoder.fragmentvisibility I/FragmentVisibility: onActivityCreated
cn.cocoder.fragmentvisibility I/FragmentVisibility: onStart
cn.cocoder.fragmentvisibility I/FragmentVisibility: onResume
複製代碼

  以上幾種操做,只須要在對應的生命週期onPause/onResume進行監聽就能夠了。api

二、ViewPager切換

  當前Fragment被ViewPager切走時,主要是經過setUserVisibleHint方法監聽可見性的變化。當方法傳入值爲true的時候,說明Fragment可見,爲false的時候說明Fragment被切走了。bash

/** * 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) 複製代碼

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

update:setUserVisibleHint在androidx 1.1.0以後的api中被廢棄了,取而代之是使用setMaxLifeCycle:app

public void setUserVisibleHint (boolean isVisibleToUser)
This method is deprecated. If you are manually calling this method, use FragmentTransaction.setMaxLifecycle(Fragment, Lifecycle.State) instead. If overriding this method, behavior implemented when passing in true should be moved to onResume(), and behavior implemented when passing in false should be moved to onPause().ide

原先在setUserVisibleHint中isVisibleToUser爲true的代碼應該挪到onResume中,爲false的代碼挪到onPause中,這樣就把切tab的行爲與生命週期統一了oop

三、Hide和Show操做

  前面咱們介紹過,Attach和add操做會觸發一些生命週期的回調,可是Hide和show操做並不會,Fragment中也提供了相應的方法監聽Hide和Show的狀態

/** * Called when the hidden state (as returned by {@link #isHidden()} of * the fragment has changed. Fragments start out not hidden; this will * be called whenever the fragment changes state from that. * @param hidden True if the fragment is now hidden, false otherwise. */
public void onHiddenChanged(boolean hidden) {
}
/** * Return true if the fragment has been hidden. By default fragments * are shown. You can find out about changes to this state with * {@link #onHiddenChanged}. Note that the hidden state is orthogonal * to other states -- that is, to be visible to the user, a fragment * must be both started and not hidden. */
final public boolean isHidden() {
        return mHidden;
}
複製代碼
四、宿主Fragment的顯示隱藏

  這是一種特殊的狀況,存在於Fragment中嵌套了Fragment的狀況。宿主Fragment在生命週期執行的時候會相應的分發到子Fragment中,可是setUserVisibleHint和onHiddenChanged卻沒有進行相應的回調。試想一下,一個ViewPager中有一個FragmentA的tab,而FragmentA中有一個子FragmentB,FragmentA被滑走了,FragmentB並不能接收到setUserVisibleHint事件,onHiddenChange事件也是同樣的。因此,咱們必需要進行特殊的處理,在這兩個事件回調的時候相應的分發到子Fragment中。當前Fragment的子Fragment能夠從getChildFragmentManager中得到:

FragmentManager fragmentManager = getChildFragmentManager();
        List<Fragment> fragments = fragmentManager.getFragments();
複製代碼

2、自主監聽Fragment顯示隱藏

  在瞭解了以上方法以後,可能有的同窗就要躍躍欲試了,可是,直接使用以上的方法可能會致使邏輯複雜,怎麼說呢,好比你要在隱藏的時候中止視頻,難道你要在上面每一個回調方法中都調一遍中止視頻的方法?顯然這樣是不合適也很差維護,最好的辦法是將這些狀態收斂一下,最好是有一個統一的監聽,筆者就作了這樣的嘗試,下面跟你們分享一下。 首先根據四種顯示隱藏狀態定義四個狀態值,他們位於一個Integer的不一樣比特位上:

//Hide狀態位第一位
public static final int FRAGMENT_HIDDEN_STATE = 0x01;
//setUserVisibilityHint的狀態爲第二位
public static final int USER_INVISIBLE_STATE = 0x02;
//宿主Fragment被隱藏的狀態爲第三位
public static final int PARENT_INVISIBLE_STATE = 0x04;
//生命週期Pause的狀態爲第四位
public static final int LIFE_CIRCLE_PAUSE_STATE = 0x08;
複製代碼

再定義一個LiveData監聽:

protected MutableLiveData<Integer> mFragmentVisibleState = new MutableLiveData<>();
複製代碼

  狀態值和狀態變量都定義好以後,咱們須要處理一下如何將宿主Fragment的狀態變化傳遞到子Fragment的,首先定義一個接口,Fragment實現這個接口表示須要監聽宿主Fragment的顯示隱藏狀態:

public interface IPareVisibilityObserver {
    public void onParentFragmentHiddenChanged(boolean hidden);
}
複製代碼

  再來看看狀態如何傳遞:

//當本身的顯示隱藏狀態改變時,調用這個方法通知子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);
    }
}
    
//子Fragment從這裏接收父Fragment的顯示隱藏狀態。因爲Fragment可能嵌套多個,因此這裏須要依次傳遞下去
@CallSuper
@Override
public void onParentFragmentHiddenChanged(boolean hidden) {
    int value = mFragmentVisibleState.getValue() == null ? LIFE_CIRCLE_PAUSE_STATE : mFragmentVisibleState.getValue();
    if (hidden) {
        mFragmentVisibleState.setValue(value | PARENT_INVISIBLE_STATE);
    } else {
        mFragmentVisibleState.setValue(value & ~PARENT_INVISIBLE_STATE);
    }
    notifyChildHiddenChange(mFragmentVisibleState.getValue() != 0);
}
複製代碼

  接下來咱們以onHidenChange爲例,看下如何設置和傳遞狀態值的:

@Override
@CallSuper
public void onHiddenChanged(boolean hidden) {
    super.onHiddenChanged(hidden);
    Integer value = mFragmentVisibleState.getValue();
    if (value == null) {
        return;
    }
    if (hidden) {
    //或操做,將相應位置的狀態值設置爲1
        mFragmentVisibleState.setValue(value | FRAGMENT_HIDDEN_STATE);
    } else {
    //與非操做,將相應位置的狀態值設置爲0
        mFragmentVisibleState.setValue(value & ~FRAGMENT_HIDDEN_STATE);
    }
    //通知子Fragment本身的狀態發生改變
    notifyChildHiddenChange(value != 0);
}
複製代碼

  在setUserVisibleHint也是相應的處理,完整的代碼你們能夠參考Demo中的代碼。 經過以上的方法,咱們就能監聽Fragment的顯示和隱藏狀態,只須要給LiveData設置一個監聽就能夠了:

mFragmentVisibleState.observe(this, new Observer<Integer>() {
    @Override
    public void onChanged(Integer integer) {
        
    }
});
複製代碼

3、Fragment嵌套時請使用getChildFragmentManager

  筆者在實現Fragment顯示隱藏監聽時,發現代碼中Fragment的嵌套使用時,有的地方使用getFragmentManager,有的地方使用的是getChildFragmentManager獲取到的FragmentManager,那麼這兩個方法有什麼不同呢,顯然,第一個獲取到的是「管理本身的FragmentManager」,第二個獲取到的是「本身管理的FragmentManager」,那麼這兩個能夠隨便用嗎?並不能隨便用,最好是使用getChildFragmentManager。
  筆者發現,若是在FragmentA中,使用getFragmentManager去添加一個FragmentB,那麼在FragmentA被銷燬時,並不會去銷燬FragmentB,由於這兩個Fragment是屬於同一個級別的FragmentManager,而FragmentManager認爲這兩個Fragment並無什麼聯繫。這就會致使當FragmentA被銷燬後,若是沒有調用FragmentManager去銷燬FragmentB,FragmentB會超過預期地存在,致使內存泄露的風險。
  若是是使用getChildFragmentManager,在1.1中生命週期的分發過程當中咱們講過,Fragment會把本身的生命週期經過ChildFragmentManager傳遞,從而能順利地銷燬子Fragement。 因此,除非你清楚地知道本身在作什麼,不然,在Fragment中添加Fragment的時候,最好使用getChildFragmentManager。

本文demo github.com/txlbupt/Vis…


時間有限,本文準備比較倉促,恐有遺漏,有任何問題歡迎交流,個人郵箱txlbupt@gmail.com 【本文做者】塗曉龍,曾就任於網易和美圖,現擔任懂球帝安卓研發工程師,致力於爲足球迷們打造一款更好用的App

相關文章
相關標籤/搜索