背景
前一段时间,运营同事发现首页tab pv数据异常,希望我们可以修改下。先介绍下app首页架构:首先底部四个按钮,点击切换tab(fragment),第二个fragment中使用viewPager加载了三个fragment(后续成为内部fragment),而这三个fragment的pv埋点原先都是在setUserVisibleHint()中埋的,而此方法只有在内部fragment切换中才会触发,导致pv极为的少。就此希望调整下fragment可见与隐藏的实现方案,不试不知道,一试发现里面的水很深。。。
fragment基础
思考下activity的pv埋点方式,就可以很轻松知道fragment的pv埋点方式。activity pv是在onResume()和onPause()方法中记录。而我们只需要在BaseFragment中实现类似的方法,命名为onVisible()和onHidden()分别对应onResume()和onPause()。
而想要实现onVisible和onHidden,必须得了解两个方法,
onHiddenChange()和setUserVisibleHint(),源码注释:
/**
* 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) {
}
/**
* 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) {
if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
&& mFragmentManager != null && isAdded()) {
mFragmentManager.performPendingDeferredStart(this);
}
mUserVisibleHint = isVisibleToUser;
mDeferStart = mState < STARTED && !isVisibleToUser;
}
简单来说
onHiddenChange(): 触发时机从显示后开始,fragment状态发生改变后执行。使用FragmentTransaction执行add,show,hide方法时触发。
/**
* fragment 切换
*
* @param from from
* @param to to
* @param tag tag
*/
private synchronized void switchFragment(Fragment from, Fragment to, String tag) {
if (from == to) {
return;
}
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
if (from == null || !from.isAdded()) {
if (!to.isAdded() && null == manager.findFragmentByTag(tag)) {
transaction.add(R.id.rly_content_home, to, tag).commitAllowingStateLoss();
} else {
transaction.show(to).commitAllowingStateLoss();
}
} else {
if (!to.isAdded() && null == manager.findFragmentByTag(tag)) {
transaction.hide(from).add(R.id.rly_content_home, to, tag).commitAllowingStateLoss();
} else {
transaction.hide(from).show(to).commitAllowingStateLoss();
}
}
mSelectedFragment = to;
}
setUserVisibleHint(): 当fragment中的UI对用户可见时触发,可能会在fragment生命周期之外被调用。(乍听此方法,是可以用于pv的显示时埋点,但后续实验发现仍有问题,因为注释后还有一句话,不保证和生命周期关联)
使用viewPager加载fragment,切换时,执行此方法。
实验分析
目前可以把fragment显示,隐藏分为三种方式,
- 使用add,show,hide方式
- 使用viewPager加载
- 使用viewPager预加载(看后面就知为何单独列出来)
先看图
activity切换对于前两个:在当前fragment,跳转到其他activity后再返回,fragment所经历的生命周期。
tab切换:同级fragment切换
外部tab切换:外部fragment切换时,内部fragment所经历的生命周期。
终极解决方案如下:
private boolean mIsHidden = false;
private boolean mIsUserVisibleHint = true;
//viewPager内第一次加载fragment,会执行setUserVisibleHint和onResume,字段防止onVisible()执行两遍
private boolean mIsExecuteOnVisible = false;
//viewPager 预加载默认会执行一次setUserVisibleHint,初始第一次都为false,避免执行onHidden方法
private boolean mIsFirstUserVisibleHint = true;
/**
* 目前只有在底部tab切换到viewPager内fragment时, onVisible()不会执行,其余时机皆可
*/
protected void onVisible(){
if (TextUtils.isEmpty(TAG)) {
setTag();
}
LogUtils.log("======="+TAG+"===onVisible====");
}
protected void onHidden() {
if (TextUtils.isEmpty(TAG)) {
setTag();
}
LogUtils.log("======="+TAG+"===onHidden====");
}
@Override
public void onResume() {
if (getUserVisibleHint() && !mIsHidden && !mIsExecuteOnVisible) {
onVisible();
}
if (mIsExecuteOnVisible) {
mIsExecuteOnVisible = false;
}
super.onResume();
}
@Override
public void onPause() {
super.onPause();
if (!mIsHidden && mIsUserVisibleHint) {
onHidden();
}
mIsExecuteOnVisible = false;
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (hidden) {
onHidden();
} else {
onVisible();
}
mIsHidden = hidden;
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mIsUserVisibleHint = isVisibleToUser;
if (TextUtils.isEmpty(TAG)) {
setTag();
}
if (isVisibleToUser) {
if (!mIsExecuteOnVisible) {
mIsExecuteOnVisible = true;
}
onVisible();
} else {
if (!mIsFirstUserVisibleHint) {
onHidden();
}
mIsFirstUserVisibleHint = false;
}
}
当然上述方法也不是完全没有问题,目前外部fragment切换时,只会触发外部fragment onVisible()方法,是无法触发到内部fragment的。解决方案也是有的,在触发到外部fragment onVisible()方法时,做逻辑判断,触发内部fragment的onVisible()方法。暂时就没有做这一步逻辑了,感兴趣的,可以自己实现
总结
fragment可见与不可见涉及方法有4个:onResume(), onPause(), onHiddenChange(boolean isHidden), setUserVisibleHint(boolean isVisibleToUser)。因为加载方式的不同,导致fragment加载的生命周期不如activity明确,需要较多逻辑判断,才可完善此功能!