前言

本篇文章主要提供一种监听 Fragment 可见性监听的方案,完美多种 case,有兴趣的可以看看。废话不多说,开始进入正文。

在开发当中, fragment 经常使用到。在很多应用场景中,我们需要监听到 fragment 的显示与隐藏,来进行一些操作。比如,统计页面的停留时长,页面隐藏的时候停止播放视频。

有些同学可能会说了,这还不容易,直接监听 Fragment 的 onResume,onPause。我只能说,兄弟,too young,too simple。

下面,让我们一起来实现 fragment 的监听。主要分为几种 case

  • 一个页面只有一个 fragment 的,使用 replace
  • Hide 和 Show 操作
  • ViewPager 嵌套 Fragment
  • 宿主 Fragment 再嵌套 Fragment,比如 ViewPager 嵌套 ViewPager,再嵌套 Fragment

Replace 操作

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

    override fun onResume() {info("onResume")super.onResume()onActivityVisibilityChanged(true)}override fun onPause() {info("onPause")super.onPause()onActivityVisibilityChanged(false)}

Hide 和 Show 操作

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

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

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) returnval parentVisible = if (localParentFragment == null) {parentActivityVisible} else {localParentFragment?.isFragmentVisible() ?: false}val superVisible = super.isVisible()val hintVisible = userVisibleHintval visible = parentVisible && superVisible && hintVisibleinfo(String.format("==> checkVisibility = %s  ( parent = %s, super = %s, hint = %s )",visible, parentVisible, superVisible, hintVisible))if (visible != this.visible) {this.visible = visibleonVisibilityChanged(this.visible)}}

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 的生命周期来监听
//FragmentStatePagerAdapter构造方法
public FragmentStatePagerAdapter(@NonNull FragmentManager fm,@Behavior int behavior) {mFragmentManager = fm;mBehavior = behavior;
}//FragmentPagerAdapter构造方法
public FragmentPagerAdapter(@NonNull FragmentManager fm,@Behavior int behavior) {mFragmentManager = fm;mBehavior = behavior;
}@IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})
private @interface Behavior { }

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

回过头,Behavior 是如何实现的呢?

已 FragmentStatePagerAdapter 为例,我们一起开看看源码

@SuppressWarnings({"ReferenceEquality", "deprecation"})
@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {Fragment fragment = (Fragment)object;if (fragment != mCurrentPrimaryItem) {if (mCurrentPrimaryItem != null) {//当前显示FragmentmCurrentPrimaryItem.setMenuVisibility(false);if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {if (mCurTransaction == null) {mCurTransaction = mFragmentManager.beginTransaction();}//最大生命周期设置为STARTED,生命周期回退到onPausemCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);} else {//可见性设置为falsemCurrentPrimaryItem.setUserVisibleHint(false);}}//将要显示的Fragmentfragment.setMenuVisibility(true);if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {if (mCurTransaction == null) {mCurTransaction = mFragmentManager.beginTransaction();}//最大 生命周期设置为RESUMEDmCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);} else {//可见性设置为truefragment.se tUserVisibleHint(true);}//赋值mCurrentPrimaryItem = fragment;}
}

代码比较简单很好理解

  • 当 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 中下游持有

第一,我们先定义一个接口

interface OnFragmentVisibilityChangedListener {fun onFragmentVisibilityChanged(visible: Boolean)
}

第二步,在 BaseVisibilityFragment 中提供 addOnVisibilityChangedListener 和 removeOnVisibilityChangedListener 方法,这里需要注意的是,我们需要用一个 ArrayList 来保存所有的 listener,因为一个宿主 Fragment 可能有多个子 Fragment。

当 Fragment 可见性变化的时候,会遍历 List 调用 OnFragmentVisibilityChangedListener 的 onFragmentVisibilityChanged 方法
**

open class BaseVisibilityFragment : Fragment(), View.OnAttachStateChangeListener,OnFragmentVisibilityChangedListener {private val listeners = ArrayList<OnFragmentVisibilityChangedListener>()fun addOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) {listener?.apply {listeners.add(this)}}fun removeOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) {listener?.apply {listeners.remove(this)}}private fun checkVisibility(expected: Boolean) {if (expected == visible) returnval parentVisible =if (localParentFragment == null) parentActivityVisibleelse localParentFragment?.isFragmentVisible() ?: falseval superVisible = super.isVisible()val hintVisible = userVisibleHintval visible = parentVisible && superVisible && hintVisibleif (visible != this.visible) {this.visible = visiblelisteners.forEach { it ->it.onFragmentVisibilityChanged(visible)}onVisibilityChanged(this.visible)}}

第三步,在 Fragment attach 的时候,我们通过 getParentFragment 方法,拿到宿主 Fragment,进行监听。这样,当宿主 Fragment 可见性变化的时候,子 Fragment 能感应到。

override fun onAttach(context: Context) {super.onAttach(context)val parentFragment = parentFragmentif (parentFragment != null && parentFragment is BaseVisibilityFragment) {this.localParentFragment = parentFragmentinfo("onAttach, localParentFragment is $localParentFragment")localParentFragment?.addOnVisibilityChangedListener(this)}checkVisibility(true)}

第二种方法

第二种方法,它的实现思路是这样的,宿主 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);}
}

具体的实现方案,可以看这一篇博客。获取和监听Fragment的可见性

完整代码

/*** Created by jun xu on 2020/11/26.*/
interface OnFragmentVisibilityChangedListener {fun onFragmentVisibilityChanged(visible: Boolean)
}/*** Created by jun xu on 2020/11/26.** 支持以下四种 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 = falseprivate var localParentFragment: BaseVisibilityFragment? =nullprivate 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 = parentFragmentif (parentFragment != null && parentFragment is BaseVisibilityFragment) {this.localParentFragment = parentFragmentlocalParentFragment?.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 = visiblecheckVisibility(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 的 caseview.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) returnval parentVisible =if (localParentFragment == null) parentActivityVisibleelse localParentFragment?.isFragmentVisible() ?: falseval superVisible = super.isVisible()val hintVisible = userVisibleHintval visible = parentVisible && superVisible && hintVisibleinfo(String.format("==> checkVisibility = %s  ( parent = %s, super = %s, hint = %s )",visible, parentVisible, superVisible, hintVisible))if (visible != this.visible) {this.visible = visibleonVisibilityChanged(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")}}

题外话

今年有好长时间没有更新技术博客了,主要是比较忙。拖着拖着,就懒得更新了。

这边博客的技术含量其实不高,主要是适配。

  1. AndroidX FragmentAdapter behavior 的适配
  2. 宿主 Fragment 嵌套 Fragment,提供了两种方式解决,一种是自上而下的,一种是自上而下的。借鉴了 Rxjava 的设计思想,下游持有上游的引用,从而控制 Obverable 的回调线程。Obsever 会有下游 Observer 的引用,从而进行一些转换操作,比如 map,FlatMap 操作符
  3. 如果你使用中遇到坑,也欢迎随时 call 我,我们一起解决。如果你有更好的方案,也欢迎随时跟我交流

往期文章

Android 面试必备 - http 与 https 协议

Android 面试必备 - 计算机网络基本知识(TCP,UDP,Http,https)

Android 面试必备 - 线程

Android 面试必备 - JVM 及 类加载机制

Android 面试必备 - 系统、App、Activity 启动过程

面试官系列- 你真的了解 http 吗

面试官问, https 真的安全吗,可以抓包吗,如何防止抓包吗

java 版剑指offer算法集锦

Android_interview github 地址 帮忙 star

如果你觉得对你有所帮助的话,可以关注我的公众号 徐公码字(stormjun94),第一时间会在上面更新

Fragment 可见性监听方案 - 完美兼容多种 case相关推荐

  1. android view可见性监听,view 可见性 监听探究

    view 可见性监听 今天产品有个需求,当一个view任何又不可见->k可见时,上报这个view的特定信息.任何由不可见->可见,包括进入一个页面:从其他页面返回到该页面:在页面内view ...

  2. android view可见性监听,Android检测View的可见性

    Android中我们经常会用到判断View的可见行,当然有人会说View.VISIBLE就可以了,但是有时候这个真是满足不了,有时候我们为了优化,在View滚到得不可见的时候或者由于滚到只显示了部分内 ...

  3. ANR系列(二)——ANR监听方案之WatchDog

    前言 ANR的监控在Android6.0之前可以通过监听文件data/anr/trace读取trace信息来分析,但从6.0之后就被禁止了.随着Android的发展,手机里的ANR越来越多,对ANR的 ...

  4. android全局监听onkeydown,在Fragment中监听onKeyDown事件

    在Activity中可以很轻监听到onKeyDown事件,但大部分场景我们的操作是在Fragment中完成的,此时要获取到onKeyDown事件需要多做点事 1.首先在Fragment的宿主Activ ...

  5. Fragment监听touch事件

    1. 在MainActivity中添加方法 /** * 以下的几个方法用来,让fragment能够监听touch事件 */ private ArrayList<MyOnTouchListener ...

  6. Glide 源码解析之监听生命周期

    code小生 一个专注大前端领域的技术平台公众号回复Android加入安卓技术群 作者:断了谁的弦 链接:https://www.jianshu.com/p/1169a91342a9 声明:本文已获断 ...

  7. Android VoLTE 视频通话是否可用状态读取与监听

    展讯volte视频通话 初始化状态读取 TelephonyManager.isImsRegistered() 在远程服务端中对应的接口实际上为: frameworks\opt\telephony\sr ...

  8. 实时监听输入框值变化的完美方案:oninput onpropertychange

    实时监听输入框值变化的完美方案:oninput & onpropertychange 原文:实时监听输入框值变化的完美方案:oninput & onpropertychange 在 W ...

  9. uni-app - 头像图片裁剪组件(支持多种裁剪,手势控制旋转或缩放、内外部控制图片移动、提供上传后端接口方案、头像图片美化)全端完美兼容 H5 App 小程序,最好用的图片上传后裁剪插件教程源代码

    前言 网上的教程代码非常乱且都有 BUG 存在,非常难移植到自己的项目中,而且很难. 实现了 完美兼容 H5 App 小程序,选取手机本地相册或拍照,图片上传裁切内置多种方案,样式随便改, 本文代码干 ...

最新文章

  1. Oracle-01033错误处理
  2. DAG情况下如何移动数据库路径
  3. Boost:字符串Predicate的测试实例
  4. SAP 电商云 Spartacus UI ROUTING_FEATURE 的使用场景
  5. Windows 10通过本地镜像离线安装.NET 3.5
  6. 利用物联网技术为市民打造“无忧”生活
  7. shell历史命令记录功能
  8. shell一周学习心得
  9. 微信资源混淆AndResGuard原理
  10. multisim仪表运放_Multisim仿真---三运放仪表放大器
  11. 电脑护眼,老司机教你电脑护眼设置怎么开
  12. android 视频录制锐化,从录制到剪辑,用的同款APP,为什么你录制的游戏视频画面会远不如别人?...
  13. c语言编程中句柄无效怎么解决,句柄无效,小编教你句柄无效怎么解决
  14. 软件测试实验-决策表
  15. Xshell下载安装(解决评估过期问题)
  16. 小米VR一体机、Oculus Go投屏到PC、TV教程
  17. 【算法设计与分析】C++回溯法求全排列
  18. 街霸5服务器链接已中断,《街头霸王5》常见问题解决方法
  19. 批量识别图片文字并存为Excel,几行Python轻松实现!
  20. linux内核是如何实现分页机制的

热门文章

  1. [软考]项目管理之十二大项目管理输入输出、工具技术、作用及内容总结
  2. java from space to space_快速定位Java 内存OOM的问题
  3. 【AWS】一、如何在AWS免费撸一年的服务器
  4. windows的由来与详细介绍
  5. android os仿ios,安卓仿ios12桌面全套仿安卓完美版
  6. java笔试题分类集锦
  7. C# WAV音乐多音轨合并
  8. CNN结构演变总结(二)轻量化模型
  9. CIsco思科三层交换配置DHCP,客户端动态获取
  10. Spring Boot 学习之路之 Spring Security(二)加入mybatis