1.問題
公司app有banner 展示,后臺同學(xué)有反應(yīng)banner 埋點數(shù)據(jù)上報次數(shù)異常多,多達(dá)億級別,差點把kafaka 多給干爆了??。 banner是在Fragment中展示的,所以我們需要在Fragment可見的時候上報埋點數(shù)據(jù),F(xiàn)ragment不可見的時候不能上報埋點數(shù)據(jù)。
2.場景
下面,讓我們一起來實現(xiàn) fragment 的監(jiān)聽。主要分為幾種 case:
1、一個頁面只有一個 fragment 的,使用 replace。
2、Hide 和 Show 操作。
3、ViewPager 嵌套 Fragment。
4、宿主 Fragment 再嵌套 Fragment,比如 ViewPager 嵌套 ViewPager,再嵌套Fragment。
3.Replace操作
replace 操作這種比較簡單,因為他會正常調(diào)用 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 操作,會觸發(fā)生命周期的回調(diào),但是 hide 和 show 操作并不會,那么我們可以通過什么方法來監(jiān)聽呢?其實很簡單,可以通過 onHiddenChanged 方法。
/**
* 調(diào)用 fragment show hide 的時候回調(diào)用這個方法
*/
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
checkVisibility(hidden)
}
5.ViewPager嵌套Fragment
ViewPager 嵌套 Fragment,這種也是很常見的一種結(jié)構(gòu)。因為 ViewPager 的預(yù)加載機(jī)制,在onResume監(jiān)聽是不準(zhǔn)確的。
這時候,我們可以通過 setUserVisibleHint 方法來監(jiān)聽,當(dāng)方法傳入值為true的時候,說明Fragment可見,為false的時候說明Fragment被切走了。
public void setUserVisibleHint(boolean isVisibleToUser)
有一點需要注意的是,這個方法可能先于Fragment的生命周期被調(diào)用(在FragmentPagerAdapter中,在Fragment被add之前這個方法就被調(diào)用了),所以在這個方法中進(jìn)行操作之前,可能需要先判斷一下生命周期是否執(zhí)行了。
/**
* Tab切換時會回調(diào)此方法。對于沒有Tab的頁面,[Fragment.getUserVisibleHint]默認(rèn)為true。
*/
@Suppress("DEPRECATION")
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
info("setUserVisibleHint = $isVisibleToUser")
super.setUserVisibleHint(isVisibleToUser)
checkVisibility(isVisibleToUser)
}
/**
* 檢查可見性是否變化
*
* @param expected 可見性期望的值。只有當(dāng)前值和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 當(dāng)中,FragmentAdapter 和 FragmentStatePagerAdapter 的構(gòu)造方法,添加一個 behavior 參數(shù)實現(xiàn)的。
如果我們指定不同的behavior,會有不同的表現(xiàn)。
1、當(dāng) behavior 為 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 時,ViewPager中切換 Fragment,setUserVisibleHint 方法將不再被調(diào)用,他會確保 onResume 的正確調(diào)用時機(jī)。
2、當(dāng) behavior 為 BEHAVIOR_SET_USER_VISIBLE_HINT,跟之前的方式是一致的,我們可以通過 setUserVisibleHint結(jié)合fragment的生命周期來監(jiān)聽。
既然是這樣,我們就很好適配呢,直接在 onResume 中調(diào)用 checkVisibility 方法,判斷當(dāng)前Fragment 是否可見。
6.宿主Fragment再嵌套Fragment
這種 case 也是比較常見的,比如 ViewPager 嵌套 ViewPager,再嵌套 Fragment。
宿主Fragment在生命周期執(zhí)行的時候會相應(yīng)的分發(fā)到子Fragment中,但是setUserVisibleHint和onHiddenChanged卻沒有進(jìn)行相應(yīng)的回調(diào)。試想一下,一個ViewPager中有一個FragmentA的tab,而FragmentA中有一個子FragmentB,F(xiàn)ragmentA被滑走了,F(xiàn)ragmentB并不能接收到setUserVisibleHint事件,onHiddenChange事件也是一樣的。
那有沒有辦法監(jiān)聽到宿主的 setUserVisibleHint 和 ,onHiddenChange 事件呢?
方法肯定是有的。
宿主 Fragment 生命周期發(fā)生變化的時候,遍歷子Fragment,調(diào)用相應(yīng)的方法,通知生命周期發(fā)生變化。
//當(dāng)自己的顯示隱藏狀態(tài)改變時,調(diào)用這個方法通知子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,因為這時候不會調(diào)用 setUserVisibleHint 方法,在 onResume check 可以兼容
* 2. 直接 fragment 直接 add, hide 主要是通過 onHiddenChanged
* 3. 直接 fragment 直接 replace ,主要是在 onResume 做判斷
* 4. Fragment 里面用 ViewPager, ViewPager 里面有多個 Fragment 的,通過 setOnVisibilityChangedListener 兼容,前提是一級 Fragment 和 二級 Fragment 都必須繼承 BaseVisibilityFragment, 且必須用 FragmentPagerAdapter 或者 FragmentStatePagerAdapter
* 項目當(dāng)中一級 ViewPager adapter 比較特殊,不是 FragmentPagerAdapter,也不是 FragmentStatePagerAdapter,導(dǎo)致這種方式用不了
*/
open class BaseVisibilityFragment : Fragment(), View.OnAttachStateChangeListener,
OnFragmentVisibilityChangedListener {
companion object {
const val TAG = "BaseVisibilityFragment"
}
/**
* ParentActivity是否可見
*/
private var parentActivityVisible = false
/**
* 是否可見(Activity處于前臺、Tab被選中、Fragment被添加、Fragment沒有隱藏、Fragment.View已經(jīng)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)
}
/**
* 調(diào)用 fragment add hide 的時候回調(diào)用這個方法
*/
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
checkVisibility(hidden)
}
/**
* Tab切換時會回調(diào)此方法。對于沒有Tab的頁面,[Fragment.getUserVisibleHint]默認(rèn)為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 可見性期望的值。只有當(dāng)前值和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已經(jīng)Attach)
*/
fun isFragmentVisible(): Boolean {
return visible
}
private fun info(s: String) {
Log.i(TAG, "${this.javaClass.simpleName} ; $s ; this is $this")
}
}