preface
This article mainly provides a scheme for monitoring Fragment visibility. It is perfect for a variety of case s. You can see it if you are interested. Without much nonsense, I began to enter the text.
fragment is often used in development. In many application scenarios, we need to listen to the display and hiding of fragments for some operations. For example, count the dwell time of the page and stop playing the video when the page is hidden.
Some students may say that it's not easy to directly monitor the onResume and onPause of the Fragment. I can only say, brother, too young, too simple.
Next, let's implement fragment monitoring. It is mainly divided into several case s
- If a page has only one fragment, use replace
- Hide and Show operations
- ViewPager nested Fragment
- Host fragments nest fragments, such as ViewPager, ViewPager, and Fragment
Replace operation
The replace operation is relatively simple because it will normally call onResume and onPause methods. We only need to check onResume and onPause
override fun onResume() { info("onResume") super.onResume() onActivityVisibilityChanged(true) } override fun onPause() { info("onPause") super.onPause() onActivityVisibilityChanged(false) }
Hide and Show operations
Hide and show operations will trigger callbacks in the life cycle, but hide and show operations will not. What methods can we use to listen? In fact, it is very simple. You can use the onHiddenChanged method
/** * This method is called back when fragment show hide is called */ override fun onHiddenChanged(hidden: Boolean) { super.onHiddenChanged(hidden) checkVisibility(hidden) }
ViewPager nested Fragment
ViewPager nested Fragment is also a very common structure. Because of the preload mechanism of ViewPager, listening on onResume is inaccurate.
At this time, we can listen through the setUserVisibleHint method. When the value passed in by the method is true, it indicates that the Fragment is visible, and when it is false, it indicates that the Fragment has been cut away
public void setUserVisibleHint(boolean isVisibleToUser)
It should be noted that a method may be called before the life cycle of the Fragment (in the FragmentPagerAdapter, this method is called before the Fragment is add ed). Therefore, before operating in this method, you may need to judge whether the life cycle has been executed.
/** * Tab This method is called back when switching. For pages without tabs, [Fragment.getUserVisibleHint] defaults to true. */ @Suppress("DEPRECATION") override fun setUserVisibleHint(isVisibleToUser: Boolean) { info("setUserVisibleHint = $isVisibleToUser") super.setUserVisibleHint(isVisibleToUser) checkVisibility(isVisibleToUser) } /** * Check for changes in visibility * * @param expected Visibility is the expected value. Only when the current value is different from expected, you need to make a judgment */ 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) } }
Android x adaptation (also a pit)
In Android x, the construction methods of FragmentAdapter and FragmentStatePagerAdapter are implemented by adding a behavior parameter.
If we specify different behavior s, there will be different performances
- When behavior is behavior_ RESUME_ ONLY_ CURRENT_ When fragment, When the Fragment is switched in the ViewPager, the setUserVisibleHint method will no longer be called. It will ensure the correct call timing of onResume
- When behavior is BEHAVIOR_SET_USER_VISIBLE_HINT is the same as the previous method. We can listen through setUserVisibleHint combined with the life cycle of fragment
//FragmentStatePagerAdapter construction method public FragmentStatePagerAdapter(@NonNull FragmentManager fm, @Behavior int behavior) { mFragmentManager = fm; mBehavior = behavior; } //FragmentPagerAdapter construction method 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 { }
Since this is the case, we are well suited to it. We call the checkVisibility method directly in onResume to determine whether the current Fragment is visible.
Looking back, how does Behavior come true?
FragmentStatePagerAdapter has been taken as an example. Let's open the source code together
@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) { //Fragment is currently displayed mCurrentPrimaryItem.setMenuVisibility(false); if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } //The maximum life cycle is set to STARTED, and the life cycle falls back to onPause mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED); } else { //Visibility is set to false mCurrentPrimaryItem.setUserVisibleHint(false); } } //The Fragment to display fragment.setMenuVisibility(true); if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } //The maximum lifecycle is set to RESUMED mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED); } else { //Visibility set to true fragment.se tUserVisibleHint(true); } //assignment mCurrentPrimaryItem = fragment; } }
The code is relatively simple and easy to understand
- When mBehavior is set to BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT will modify the status of the current fragment and the fragment to be displayed through setMaxLifecycle, so that only the fragment t being displayed will be executed to the onResume() method, and other fragments will only be executed to the onStart() method, and the onPause() method will be triggered when the fragment is switched to the non display state.
- When mBehavior is set to behavior_ SET_ USER_ VISIBLE_ When hint, setUserVisibleHint() will be called when the visibility of the frame changes, which is the same as the first lazy loading implementation mentioned above.
For more details, please refer to this blog Lazy loading implementation of Android Fragment + ViewPager
Host fragments and then nest fragments
This kind of case is also quite common. For example, ViewPager nested ViewPager and then nested Fragment.
The host Fragment will be distributed to the child Fragment when the life cycle is executed, but setUserVisibleHint and onHiddenChanged do not make corresponding callbacks. Imagine that a ViewPager has a FragmentA tab, and FragmentA has a child FragmentB. FragmentA is slid away. FragmentB cannot receive the setUserVisibleHint event, and the onHiddenChange event is the same.
Is there any way to listen to the setUserVisibleHint and onHiddenChange events of the host?
There must be a way.
- In the first method, the host Fragment provides a callback for visibility, and the child Fragment Listens to the callback, which is a bit similar to the observer mode. The difficulty lies in how the sub Fragment gets the host Fragment
- In the second case, when the visibility of the host Fragment changes, take the initiative to traverse all the sub fragments and call the corresponding methods of the sub fragments
The first method
The general idea is as follows: the host Fragment provides visibility callbacks, and the sub Fragment Listens and changes callbacks, which is somewhat similar to the observer mode. It is also a bit similar to the middle and lower reaches of Rxjava
First, we define an interface
interface OnFragmentVisibilityChangedListener { fun onFragmentVisibilityChanged(visible: Boolean) }
The second step is to provide addOnVisibilityChangedListener and removeOnVisibilityChangedListener methods in BaseVisibilityFragment. It should be noted here that we need to use an ArrayList to save all listeners, because a host Fragment may have multiple sub fragments.
When the Fragment visibility changes, it will traverse the List and call the onFragmentVisibilityChanged method of OnFragmentVisibilityChangedListener **
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) return val parentVisible = if (localParentFragment == null) parentActivityVisible else localParentFragment?.isFragmentVisible() ?: false val superVisible = super.isVisible() val hintVisible = userVisibleHint val visible = parentVisible && superVisible && hintVisible if (visible != this.visible) { this.visible = visible listeners.forEach { it -> it.onFragmentVisibilityChanged(visible) } onVisibilityChanged(this.visible) } }
Step 3: in the Fragment attach, we get the host Fragment through the getParentFragment method and listen. In this way, when the visibility of the host Fragment changes, the child Fragment can sense it.
override fun onAttach(context: Context) { super.onAttach(context) val parentFragment = parentFragment if (parentFragment != null && parentFragment is BaseVisibilityFragment) { this.localParentFragment = parentFragment info("onAttach, localParentFragment is $localParentFragment") localParentFragment?.addOnVisibilityChangedListener(this) } checkVisibility(true) }
The second method
The second method is implemented in such a way that when the life cycle of the host Fragment changes, it traverses the sub Fragment and calls the corresponding method to notify the life cycle of the change
//Call this method to notify the child Fragment when its display and hiding state changes 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); } }
For the specific implementation scheme, you can see this blog. Get and listen for Fragment visibility
Complete code
/** * Created by jun xu on 2020/11/26. */ interface OnFragmentVisibilityChangedListener { fun onFragmentVisibilityChanged(visible: Boolean) } /** * Created by jun xu on 2020/11/26. * * The following four case s are supported * 1. viewPager nested fragment s are supported, mainly through setUserVisibleHint compatibility, * FragmentStatePagerAdapter BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT Because the setUserVisibleHint method will not be called at this time, it can be compatible with onResume check * 2. Direct fragment, direct add and hide are mainly through onHiddenChanged * 3. Direct fragment and direct replace are mainly used to judge onResume * 4. Fragment ViewPager is used in it. If there are multiple fragments in ViewPager, it is compatible through setOnVisibilityChangedListener. The premise is that both primary and secondary fragments must inherit BaseVisibilityFragment, and must use FragmentPagerAdapter or FragmentStatePagerAdapter * The first level ViewPager adapter in the project is special. It is neither FragmentPagerAdapter nor FragmentStatePagerAdapter, which makes this method unusable */ open class BaseVisibilityFragment : Fragment(), View.OnAttachStateChangeListener, OnFragmentVisibilityChangedListener { companion object { const val TAG = "BaseVisibilityFragment" } /** * ParentActivity Visible */ private var parentActivityVisible = false /** * Whether it is visible (Activity is in the foreground, Tab is selected, Fragment is added, Fragment is not hidden, and Fragment.View has been attached) */ 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 Visibility change */ protected fun onActivityVisibilityChanged(visible: Boolean) { parentActivityVisible = visible checkVisibility(visible) } /** * ParentFragment Visibility change */ 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) // Handle the case of direct replace view.addOnAttachStateChangeListener(this) } /** * This method is called back when fragment add hide is called */ override fun onHiddenChanged(hidden: Boolean) { super.onHiddenChanged(hidden) checkVisibility(hidden) } /** * Tab This method is called back when switching. For pages without tabs, [Fragment.getUserVisibleHint] defaults to 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) } /** * Check for changes in visibility * * @param expected Visibility is the expected value. Only when the current value is different from expected, you need to make a judgment */ 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) } } /** * Visibility change */ protected fun onVisibilityChanged(visible: Boolean) { info("==> onVisibilityChanged = $visible") listeners.forEach { it.onFragmentVisibilityChanged(visible) } } /** * Whether it is visible (Activity is in the foreground, Tab is selected, Fragment is added, Fragment is not hidden, and Fragment.View has been attached) */ fun isFragmentVisible(): Boolean { return visible } private fun info(s: String) { Log.i(TAG, "${this.javaClass.simpleName} ; $s ; this is $this") } }
Digression
I haven't updated the technology blog for a long time this year, mainly because I'm busy. Drag, drag, don't bother to update.
In fact, the technical content of blogs here is not high, mainly due to adaptation.
- Adaptation of AndroidX FragmentAdapter behavior
- Host fragments and nested fragments provide two solutions, one is top-down and the other is top-down. Based on the design idea of Rxjava, the downstream holds the upstream reference, so as to control the callback thread of Obverable. The Observer will have a reference to the downstream Observer to perform some conversion operations, such as map and FlatMap operators
- If you encounter a pit in use, you are also welcome to call me at any time and we will solve it together. If you have a better plan, you are also welcome to communicate with me at any time