序言
最近在埋点,需要记录fragment的显示时长。想法很简单,使用我们熟知的fragment的生命周期即可。主需要在onResume记录开始时间,在onPase记录结束时间。相减就可以完成fragment显示时间的记录。但是这是最理想的情况,是在一个activity中只有一个fragment。 fragment不涉及hide和show等方法的情况下。
然后我们实际情况却很复杂,涉及到ViewPager中使用fragment会预加载fragment,此时会有多个fragment处于onResume状态但是真正显示的fragment只有一个,也涉及到hide和show方法的影响,hide是通过将Fragment的View设置为GONE来实现的。会调用onHiddenChanged方法。而不会调用onPause方法。
因此要正确记录Fragment的显示时长,还是很烦的。需要处理各种情况。下面分情况讨论。
android.support.v4.app.Fragment
下面介绍使用了android.support.v4.app.Fragment事的方法。至于Android X有更好的处理办法。
ViewPager的影响
测试代码如下
package com.vincent.testfragment;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
/**
* Created by zhuguohui
* Date: 2021/6/30
* Time: 10:16
* Desc:
*/
public class TestFragment extends Fragment {
private String title;
public void setTitle(String title) {
this.title = title;
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.i("zzz",title+"执行 setUserVisibleHint:isVisibleToUser="+isVisibleToUser);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i("zzz",title+"执行 onCreate");
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.i("zzz",title+"执行 onCreateView");
View view= inflater.inflate(R.layout.test_fragment,container,false);
TextView textView= view.findViewById(R.id.tv_title);
textView.setText(title);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Log.i("zzz",title+"执行 onViewCreated");
}
@Override
public void onStart() {
super.onStart();
Log.i("zzz",title+"执行 onStart");
}
@Override
public void onResume() {
super.onResume();
Log.i("zzz",title+"执行 onResume");
}
@Override
public void onPause() {
super.onPause();
Log.i("zzz",title+"执行 onPause");
}
@Override
public void onStop() {
super.onStop();
Log.i("zzz",title+"执行 onStop");
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
Log.i("zzz",title+"执行 onHiddenChanged");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i("zzz",title+"执行 onDestroy");
}
}
package com.vincent.testfragment.adapter;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.util.Log;
import com.vincent.testfragment.TestFragment;
import java.util.List;
/**
* Created by zhuguohui
* Date: 2021/6/30
* Time: 10:16
* Desc:
*/
public class TestAdapter extends FragmentStatePagerAdapter {
List<String> titles;
public TestAdapter(FragmentManager fm, List<String> titles) {
super(fm);
this.titles = titles;
}
@Override
public Fragment getItem(int position) {
Log.i("zzz", "new " + titles.get(position));
TestFragment testFragment = new TestFragment();
testFragment.setTitle(titles.get(position));
return testFragment;
}
@Override
public int getCount() {
return titles.size();
}
@Override
public CharSequence getPageTitle(int position) {
return titles.get(position);
}
}
使用TabLayout+ViewPager测试,代码很简单就不贴出来。
测试日志
初始化ViewPager 打印的日志
2021-06-30 11:05:58.162 21249-21249/? W/nt.testfragmen: Accessing hidden method Landroid/app/LoadedApk;-><init>(Landroid/app/ActivityThread;Landroid/content/pm/ApplicationInfo;Landroid/content/res/CompatibilityInfo;Ljava/lang/ClassLoader;ZZZ)V (greylist-max-o, linking, denied)
2021-06-30 11:05:58.486 21249-21249/com.vincent.testfragment I/zzz: new 第1个fragment
2021-06-30 11:05:58.486 21249-21249/com.vincent.testfragment I/zzz: 第1个fragment执行 setUserVisibleHint:isVisibleToUser=false
2021-06-30 11:05:58.486 21249-21249/com.vincent.testfragment I/zzz: new 第2个fragment
2021-06-30 11:05:58.486 21249-21249/com.vincent.testfragment I/zzz: 第2个fragment执行 setUserVisibleHint:isVisibleToUser=false
2021-06-30 11:05:58.486 21249-21249/com.vincent.testfragment I/zzz: 第1个fragment执行 setUserVisibleHint:isVisibleToUser=true
2021-06-30 11:05:58.486 21249-21249/com.vincent.testfragment I/zzz: 第1个fragment执行 onCreate
2021-06-30 11:05:58.486 21249-21249/com.vincent.testfragment I/zzz: 第2个fragment执行 onCreate
2021-06-30 11:05:58.487 21249-21249/com.vincent.testfragment I/zzz: 第1个fragment执行 onCreateView
2021-06-30 11:05:58.490 21249-21249/com.vincent.testfragment I/zzz: 第1个fragment执行 onViewCreated
2021-06-30 11:05:58.491 21249-21249/com.vincent.testfragment I/zzz: 第1个fragment执行 onStart
2021-06-30 11:05:58.491 21249-21249/com.vincent.testfragment I/zzz: 第1个fragment执行 onResume
2021-06-30 11:05:58.491 21249-21249/com.vincent.testfragment I/zzz: 第2个fragment执行 onCreateView
2021-06-30 11:05:58.493 21249-21249/com.vincent.testfragment I/zzz: 第2个fragment执行 onViewCreated
2021-06-30 11:05:58.493 21249-21249/com.vincent.testfragment I/zzz: 第2个fragment执行 onStart
2021-06-30 11:05:58.493 21249-21249/com.vincent.testfragment I/zzz: 第2个fragment执行 onResume
点击第6个fragment执行的日志
2021-06-30 11:07:32.429 21249-21249/com.vincent.testfragment I/zzz: new 第6个fragment
2021-06-30 11:07:32.430 21249-21249/com.vincent.testfragment I/zzz: 第6个fragment执行 setUserVisibleHint:isVisibleToUser=false
2021-06-30 11:07:32.430 21249-21249/com.vincent.testfragment I/zzz: new 第5个fragment
2021-06-30 11:07:32.430 21249-21249/com.vincent.testfragment I/zzz: 第5个fragment执行 setUserVisibleHint:isVisibleToUser=false
2021-06-30 11:07:32.430 21249-21249/com.vincent.testfragment I/zzz: new 第7个fragment
2021-06-30 11:07:32.430 21249-21249/com.vincent.testfragment I/zzz: 第7个fragment执行 setUserVisibleHint:isVisibleToUser=false
2021-06-30 11:07:32.430 21249-21249/com.vincent.testfragment I/zzz: 第1个fragment执行 setUserVisibleHint:isVisibleToUser=false
2021-06-30 11:07:32.430 21249-21249/com.vincent.testfragment I/zzz: 第6个fragment执行 setUserVisibleHint:isVisibleToUser=true
2021-06-30 11:07:32.430 21249-21249/com.vincent.testfragment I/zzz: 第6个fragment执行 onCreate
2021-06-30 11:07:32.431 21249-21249/com.vincent.testfragment I/zzz: 第5个fragment执行 onCreate
2021-06-30 11:07:32.431 21249-21249/com.vincent.testfragment I/zzz: 第7个fragment执行 onCreate
2021-06-30 11:07:32.432 21249-21249/com.vincent.testfragment I/zzz: 第6个fragment执行 onCreateView
2021-06-30 11:07:32.437 21249-21249/com.vincent.testfragment I/zzz: 第6个fragment执行 onViewCreated
2021-06-30 11:07:32.437 21249-21249/com.vincent.testfragment I/zzz: 第6个fragment执行 onStart
2021-06-30 11:07:32.437 21249-21249/com.vincent.testfragment I/zzz: 第6个fragment执行 onResume
2021-06-30 11:07:32.437 21249-21249/com.vincent.testfragment I/zzz: 第5个fragment执行 onCreateView
2021-06-30 11:07:32.440 21249-21249/com.vincent.testfragment I/zzz: 第5个fragment执行 onViewCreated
2021-06-30 11:07:32.440 21249-21249/com.vincent.testfragment I/zzz: 第7个fragment执行 onCreateView
2021-06-30 11:07:32.443 21249-21249/com.vincent.testfragment I/zzz: 第7个fragment执行 onViewCreated
2021-06-30 11:07:32.443 21249-21249/com.vincent.testfragment I/zzz: 第5个fragment执行 onStart
2021-06-30 11:07:32.443 21249-21249/com.vincent.testfragment I/zzz: 第5个fragment执行 onResume
2021-06-30 11:07:32.443 21249-21249/com.vincent.testfragment I/zzz: 第7个fragment执行 onStart
2021-06-30 11:07:32.443 21249-21249/com.vincent.testfragment I/zzz: 第7个fragment执行 onResume
2021-06-30 11:07:33.099 21249-21249/com.vincent.testfragment I/zzz: 第2个fragment执行 onPause
2021-06-30 11:07:33.099 21249-21249/com.vincent.testfragment I/zzz: 第2个fragment执行 onStop
2021-06-30 11:07:33.102 21249-21249/com.vincent.testfragment I/zzz: 第2个fragment执行 onDestroy
2021-06-30 11:07:33.102 21249-21249/com.vincent.testfragment I/zzz: 第1个fragment执行 onPause
2021-06-30 11:07:33.103 21249-21249/com.vincent.testfragment I/zzz: 第1个fragment执行 onStop
2021-06-30 11:07:33.103 21249-21249/com.vincent.testfragment I/zzz: 第1个fragment执行 onDestroy
分析
在ViewPager中使用Fragment 。onResume方法需要加以判断,onReusme方法会有两种方式被调用。一种是Fragment所在的Activity从后台到前台,一种ViewPager预加载。而此时能判断fragment是否显示就需要通过setUserVisibleHint方法中传递过来的参数。只有在fragment显示的时候,并且调用onResume方法的时候。才去调用相关的逻辑代码。当然在setUserVisibleHint方法中如果参数是true也可以判断。不过这个方法第一次调用的时候比onCreate还要早,此时fragment中需要的参数可能还不完整。建议保留到onResume方法。
show hide的影响
使用如下的代码来显示或隐藏fragment时,fragment中的onPause和onResume等方法不会被调用。
只会调用onHiddenChanged方法。需要在这个方法中接受变量,并保存。用于标识当前的fragment是否可见。
FragmentTransaction transaction=fm.beginTransaction();
transaction.hide(fragemnt);
transaction.commit();
特别需要注意的是onHiddenChanged方法 默认是空实现,而且该方法只有直接被**FragmentTransaction **影响到的fragment才会调用。如果你是fragment中还有子fragment,需要自己将调用传递下去。对于一般的使用了ViewPager的fragment。我自己写了一个工具类来实现。
/**
* Created by zhuguohui
* Date: 2021/6/29
* Time: 15:26
* Desc:用于从viewpager中获取正在显示的fragment
*/
public class ViewPagerUtil {
public static Fragment getCurrentFragment(ViewPager viewPager) {
if (viewPager == null) {
return null;
}
try {
Field field = viewPager.getClass().getDeclaredField("mItems");
field.setAccessible(true);
ArrayList arrayList = (ArrayList) field.get(viewPager);
for (int i = 0; i < arrayList.size(); i++) {
Object item = arrayList.get(i);
Field fieldPosition = item.getClass().getDeclaredField("position");
fieldPosition.setAccessible(true);
int position = (int) fieldPosition.get(item);
if (position != viewPager.getCurrentItem()) {
continue;
}
Field field1 = item.getClass().getDeclaredField("object");
field1.setAccessible(true);
Object obj = field1.get(item);
if (obj instanceof Fragment) {
return (Fragment) obj;
}
}
return null;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
使用也很简单,在被调用了onHiddenChanged方法的fragment中,使用
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
//为了统计fragment正确的显示时间,需要将hidden状态传递给正在显示的子fragment。
Fragment currentFragment = ViewPagerUtil.getCurrentFragment(mViewPager);
if (currentFragment != null) {
currentFragment.onHiddenChanged(hidden);
}
}
解决方案
最后基于以上的事实,我写了个工具类
package com.trs.library.fragment;
import android.support.v4.view.ViewPager;
import android.view.View;
import android.view.ViewParent;
import com.trs.library.bean.TRSChannel;
/**
* Created by zhuguohui
* Date: 2021/6/29
* Time: 14:44
* Desc:用于记录fragment的显示时长的,可以在ViewPager中使用。也可以自动感知show和hide方法的影响。
*
* 需要在fragment的setUserVisibleHint,onPause,onResume,onHiddenChanged onViewCreated方法中。调用该类中的同名方法
* 以达到记录fragment显示时长的作用
* <p>
* 如果是在含有ViewPager的fragment中使用。需要在onHiddenChanged方法中使用下列代码把,hide状态传递给子fragment
* <pre>
* ...fragment.class
* public void onHiddenChanged(boolean hidden) {
* super.onHiddenChanged(hidden);
* //为了统计fragment正确的显示时间,需要将hidden状态传递给正在显示的子fragment。
* Fragment currentFragment = ViewPagerUtil.getCurrentFragment(mViewPager);
* if (currentFragment != null) {
* currentFragment.onHiddenChanged(hidden);
* }
* }
* <pre>
* 该类需要在onCreate()方法之前初始化,因为setUserVisibleHint 方法在onCreate之前执行。
* 需要提前初始化。最好作为类的成员变量初始化
* <pre>
* ...fragment.class
* class TestFragment extends Fragment{
* FragmentShowTimeRecorder timeRecorder=new FragmentShowTimeRecorder();
* }
* </pre>
*/
public abstract class FragmentShowTimeRecorder {
//是否是在ViewPager中显示,如果是则需要通过判断setUserVisibleHint传递的参数,来判断当前的显示状态
//通过这个参数可以优化,页面浏览时长的记录。
boolean showInViewPager = false;
boolean isVisibleToUser = false;
boolean isHide = false;
boolean checkViewPager = false;//是否检查了当前是在viewPager中显示
/**
* 这个方法由ViewPager调用。
*
* @param isVisibleToUser
*/
public final void setUserVisibleHint(boolean isVisibleToUser) {
this.isVisibleToUser = isVisibleToUser;
onFragmentShowStateChange(isVisibleToUser);
}
public final void onPause() {
if (isShow()) {
onFragmentShowStateChange(false);
}
}
private boolean isShow() {
if (isHide) {
return false;//viewPager中也可能hide
}
if (showInViewPager) {
return isVisibleToUser;
}
return true;
}
/**
* 使用FragmentTransaction的show或者hide方法的时候。会回调改方法。
* hide的本质就是将fragment中的view设置为GONE。
* 注意这个状态需要自己手动分发,需要在能接受到该回调的fragment中。将状态传递给子fragment
*
* @param hidden
*/
public final void onHiddenChanged(boolean hidden) {
isHide = hidden;
onFragmentShowStateChange(!hidden);
}
/**
* 在viewPager中由于有预加载功能,当调用onResume的时候,其实并没有显示给用户
* 需要通过isVisibleToUser 来判断是否显示
*
* @param view
*/
public final void onResume(View view) {
checkViewPager(view);
if (isShow()) {
onFragmentShowStateChange(true);
}
}
private void checkViewPager(View view) {
if (checkViewPager) {
return;
}
if (view == null) {
return;
}
ViewParent parent = view.getParent();
while (parent != null) {
if (parent instanceof ViewPager) {
showInViewPager = true;
break;
}
parent = parent.getParent();
}
checkViewPager = true;
}
/**
* 子类重写该方法用于记录fragment给用户展示的时间
*
* @param showToUser
*/
protected abstract void onFragmentShowStateChange(boolean showToUser);
}
使用只需要在你的BaseFragment中相关的生命周期方法中回调对应的方法。需要注意的是。由于setUserVisibleHint被调用的时间早于onCreate所以FragmentShowTimeRecorder的初始化要足够的早。最好作为成员变量初始化。
package com.trs.library.fragment;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.trs.library.bean.TRSChannel;
import com.trs.library.rx.bus.RxBus;
import java.util.LinkedList;
import java.util.Queue;
import rx.subscriptions.CompositeSubscription;
/**
* Created by Vincent Woo
* Date: 2016/6/8
* Time: 10:41
*/
public class TRSFragment extends Fragment {
FragmentShowTimeRecorder timeRecorder = getTimeRecorder();
protected FragmentShowTimeRecorder getTimeRecorder() {
return null;
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (timeRecorder != null) {
timeRecorder.setUserVisibleHint(isVisibleToUser);
}
}
@Override
public void onResume() {
super.onResume();
if (timeRecorder != null) {
timeRecorder.onResume(getView());
}
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (timeRecorder != null) {
timeRecorder.onHiddenChanged(hidden);
}
}
@Override
public void onPause() {
super.onPause();
if (timeRecorder != null) {
timeRecorder.onPause();
}
}
}
Androidx.Fragment
对于AndroidX 有更优雅的解决办法。只要使fragment有正确的生命周期很多问题就迎刃而解。
直接使用FragmentTransactio的setMaxLifecycle就可以控制Fragment的生命周期。
@NonNull
public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment,
@NonNull Lifecycle.State state) {
addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state));
return this;
}
Lifecycle.State 有五中状态
INITIALIZING 初始状态
CREATED 已创建状态
ACTIVITY_CREATED 完全创建,但是没有started
STARTED 创建并启动,可见不可操作
RESUMED 创建启动并可操作
对于ViewPager,
只需要将FragmentStatePagerAdapter的构造方法中,使用BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT这个参数就可以了。
public class MyAdapter extends FragmentStatePagerAdapter {
public MyAdapter ( FragmentManager fm) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
}
使用了这个标志,只会让当前正在显示的fragment被调用onResume。
/**
* Indicates that only the current fragment will be in the {@link Lifecycle.State#RESUMED}
* state. All other Fragments are capped at {@link Lifecycle.State#STARTED}.
*
* @see #FragmentStatePagerAdapter(FragmentManager, int)
*/
public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;
对于hide和show
在hide一个fragment之前,需要将fragment设置到STARTED状态,这时如果fragment处于RESUMED状态,会调用onPause方法。
transaction.setMaxLifecycle(from, Lifecycle.State.STARTED);
对于需要show的fragment需要使用。
transaction.setMaxLifecycle(to, Lifecycle.State.RESUMED);
汇总
通过以上方法,就可以将fragment的显示与隐藏的行为和对于的生命周期方法关联。只需要在对于的生命周期方法中记录事件即可。
总结
在软件开发过程中,并不是一开始就能有最好的处理方法。就像Fragment的setUserHint方法。也是一种打补丁的办法,但是通过不断的思考,最后也能有更优雅的解决办法。所以代码是一时的,思考是永恒的。