Fragment 可见性监听方案

1.问题

公司app有banner 展示,后台同学有反应banner 埋点数据上报次数异常多,多达亿级别,差点把kafaka 多给干爆了😅。 banner是在Fragment中展示的,所以我们需要在Fragment可见的时候上报埋点数据,Fragment不可见的时候不能上报埋点数据。

2.场景

下面,让我们一起来实现 fragment 的监听。主要分为几种 case:
1、一个页面只有一个 fragment 的,使用 replace。
2、Hide 和 Show 操作。
3、ViewPager 嵌套 Fragment。
4、宿主 Fragment 再嵌套 Fragment,比如 ViewPager 嵌套 ViewPager,再嵌套Fragment。

3.Replace操作

replace 操作这种比较简单,因为他会正常调用 onResume 和 onPause 方法,我们只需要在onResume 和 onPause 做 check 操作即可。

override fun onResume() {
    info("onResume")
    super.onResume()
    checkVisibility(true)
}


override fun onPause() {
    info("onPause")
    super.onPause()
    checkVisibility(false)
}

4.Hide和Show操作

add 和 replace 操作,会触发生命周期的回调,但是 hide 和 show 操作并不会,那么我们可以通过什么方法来监听呢?其实很简单,可以通过 onHiddenChanged 方法。

/**
 * 调用 fragment show hide 的时候回调用这个方法
 */
override fun onHiddenChanged(hidden: Boolean) {
    super.onHiddenChanged(hidden)
    checkVisibility(hidden)
}

5.ViewPager嵌套Fragment

ViewPager 嵌套 Fragment,这种也是很常见的一种结构。因为 ViewPager 的预加载机制,在onResume监听是不准确的。
这时候,我们可以通过 setUserVisibleHint 方法来监听,当方法传入值为true的时候,说明Fragment可见,为false的时候说明Fragment被切走了。

public void setUserVisibleHint(boolean isVisibleToUser)

有一点需要注意的是,这个方法可能先于Fragment的生命周期被调用(在FragmentPagerAdapter中,在Fragment被add之前这个方法就被调用了),所以在这个方法中进行操作之前,可能需要先判断一下生命周期是否执行了。


/**
 * Tab切换时会回调此方法。对于没有Tab的页面,[Fragment.getUserVisibleHint]默认为true。
 */
@Suppress("DEPRECATION")
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 当中,FragmentAdapterFragmentStatePagerAdapter 的构造方法,添加一个 behavior 参数实现的。

如果我们指定不同的behavior,会有不同的表现。

1、当 behaviorBEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 时,ViewPager中切换 FragmentsetUserVisibleHint 方法将不再被调用,他会确保 onResume 的正确调用时机。

2、当 behaviorBEHAVIOR_SET_USER_VISIBLE_HINT,跟之前的方式是一致的,我们可以通过 setUserVisibleHint结合fragment的生命周期来监听。

既然是这样,我们就很好适配呢,直接在 onResume 中调用 checkVisibility 方法,判断当前Fragment 是否可见。

6.宿主Fragment再嵌套Fragment

这种 case 也是比较常见的,比如 ViewPager 嵌套 ViewPager,再嵌套 Fragment。
宿主Fragment在生命周期执行的时候会相应的分发到子Fragment中,但是setUserVisibleHint和onHiddenChanged却没有进行相应的回调。试想一下,一个ViewPager中有一个FragmentA的tab,而FragmentA中有一个子FragmentB,FragmentA被滑走了,FragmentB并不能接收到setUserVisibleHint事件,onHiddenChange事件也是一样的。

那有没有办法监听到宿主的 setUserVisibleHint 和 ,onHiddenChange 事件呢?

方法肯定是有的。

宿主 Fragment 生命周期发生变化的时候,遍历子Fragment,调用相应的方法,通知生命周期发生变化。

//当自己的显示隐藏状态改变时,调用这个方法通知子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);
    }
}

7.完整代码

interface OnFragmentVisibilityChangedListener {
    fun onFragmentVisibilityChanged(visible: Boolean)
}


/**
 * 支持以下四种 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")
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容