获取和监听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