获取和监听Fragment的可见性

9,495 阅读8分钟

  在很多应用场景中,需要监听页面的显示或者隐藏,比如产品想统计用户在页面的停留时间,需要在页面隐藏的时候上报。又或者在页面隐藏的时候需要停止页面上正在播放的视频。我们的界面中有一些是Activity实现的,有一些是Fragment实现的。不同于Activity,Fragment可以嵌套使用,可以灵活地显示隐藏,这就给开发者带来一个难题,就是我们如何有效地监听Fragment的显示隐藏?笔者在开发过程中对这个问题也研究了一番,分享出来希望对大家有用。首先会分四种情况下如何监听Fragment的显示隐藏。然后会介绍一下我们是如何实现统一的监听的。最后简单说下在Fragment嵌套中正确的使用姿势。

一、Fragment显示在屏幕上的几种情况

  不同的情况下监听Fragment显示隐藏的方法不一样,下面我们就分四种情况进行说明,分别是:生命周期引起的显示隐藏、ViewPager滑动引起的Fragment显示隐藏、监听Hide/Show操作、宿主Fragment的显示隐藏。

1、生命周期。

  生命周期的情况比较明确,那就是监听onPause和onResume这两个生命周期。这里稍微提一下这两个生命周期在什么时候会被触发。一般而言,有两种情况会执行这两个生命周期:

  • 宿主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的方法进行分发:

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进行监听就可以了。

2、ViewPager切换

  当前Fragment被ViewPager切走时,主要是通过setUserVisibleHint方法监听可见性的变化。当方法传入值为true的时候,说明Fragment可见,为false的时候说明Fragment被切走了。

    /**
     * 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之前这个方法就被调用了),所以在这个方法中进行操作之前,可能需要先判断一下生命周期是否执行了。

update:setUserVisibleHint在androidx 1.1.0之后的api中被废弃了,取而代之是使用setMaxLifeCycle:

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().

原先在setUserVisibleHint中isVisibleToUser为true的代码应该挪到onResume中,为false的代码挪到onPause中,这样就把切tab的行为与生命周期统一了

3、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;
}
4、宿主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();

二、自主监听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) {
        
    }
});

三、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