[关闭]
@946898963 2019-02-13T06:59:18.000000Z 字数 10177 阅读 1627

Fragment扩展二

Android学习笔记


onHiddenChanged方法

Fragment是我们经常使用到的一个控件,但是,相信大家会出现这样的一个问题,每次切换Fragment的时候之前我们一直都是这样写的代码:

  1. /**
  2. * 根据传入的参数来改变当前的fragment
  3. * @param fragment
  4. */
  5. private void showFragment(Fragment fragment) {
  6. FragmentTransaction transaction = fragmentManager.beginTransaction();
  7. transaction.replace(R.id.layout_content, fragment);
  8. transaction.commit();
  9. }

但是这样的写法在需要网络数据时会很消耗用户的数据流量和机器性能,所以我们就正确的切换方式是add(),切换时hide(),add()另一个Fragment;再次切换时,只需hide()当前,show()另一个。(这也是谷歌官方推荐的做法)

  1. /**
  2. * 修改显示的内容 不会重新加载
  3. * to 下一个fragment
  4. * mContent 当前的fragment
  5. */
  6. private void switchContent(Fragment to) {
  7. if (mContent != to) {
  8. FragmentTransaction transaction = fragmentManager.beginTransaction();
  9. if (!to.isAdded()) { // 判断是否被add过
  10. // 隐藏当前的fragment,将 下一个fragment 添加进去
  11. transaction.hide(mContent).add(R.id.layout_content, to).commit();
  12. } else {
  13. // 隐藏当前的fragment,显示下一个fragment
  14. transaction.hide(mContent).show(to).commit();
  15. }
  16. mContent = to;
  17. }
  18. }

你会发现使用hide和show这时Fragment的生命周期不再执行,不走任何的生命周期,这样在有的情况下,数据将无法通过生命周期方法进行刷新,所以你可以使用onHiddenChanged方法来解决这问题。

  1. @Override
  2. public void onHiddenChanged(boolean hidden) {
  3. super.onHiddenChanged(hidden);
  4. if (hidden) { // 不在最前端显示 相当于调用了onPause();
  5. return;
  6. }else{ // 在最前端显示 相当于调用了onResume();
  7. //网络数据刷新
  8. }
  9. }

onHiddenChanged()方法的变化直接影响着isHidden()方法的返回值。除了isHidden()方法,还有一个isVisible()方法,也用于判断Fragment的状态,表明Fragment是否对用户可见,如果为true,必须满足三点条件:1,Fragment 已经被 add 至 Activity 中;2,视图内容已经被关联到 window 上;3. 没有被隐藏,即 isHidden() 为 false。这三点,从 isVisible() 源码中可以看出:

  1. /**
  2. * Return true if the fragment is currently visible to the user. This means
  3. * it: (1) has been added, (2) has its view attached to the window, and
  4. * (3) is not hidden.
  5. */
  6. final public boolean isVisible() {
  7. return isAdded() && !isHidden() && mView != null
  8. && mView.getWindowToken() != null && mView.getVisibility() == View.VISIBLE;
  9. }

其中,尤其需要注意 mView.getWindowToken() != null 这个条件,有时候我们需要在 onCreate() 或者 onResume() 方法中使用 isVisible() 判断 Fragment 的状态,但是经常遇见 isVisible() 为 false 的情况,很大一个原因就是因为 mView.getWindowToken() 为 null 导致的!

关于onHiddenChanged方法,有以下几个需要注意的地方:

第一,会先调用被show的Fragment的onHiddenChanged,再顺序调用hide起来的Fragment的onHiddenChanged。

第二,非第一个显示出来的Fragment,虽然onCreateView没有被调用,但是onHiddenChanged也会被调用,所以如果你尝试去获取活动的话,注意防止出现空指针。

第三,onHiddenChanged仅在以FragmentManager中进行hide或者show某个Fragment的形式切换时候会被调用。也就是说当你用FragmentTransaction来控制Fragment的hide和show时,这个方法才会被调用。

第四,经过实验发现onHiddenChanged方法,是在onResume方法之前执行的。。

例子,如图:

此处输入图片的描述

这种 UI 结构想必大家都很熟悉,经常作为 App 的首页主界面布局,这里我们假设没有使用 ViewPager,而是普通操作 Fragment,通过 add()、show()、hide() 方法实现切换不同 Tab 控制 Activity 里面各个 Fragment的显示和隐藏。并且这四个Fragment都需要通过加载网络数据显示内容,同时要求不同 Fragment之间切换显示时都要重新请求服务器刷新当前界面的数据,这该怎么做呢?

第一种,在四个 Fragment 都已经显示过的情况下,不同 Tab 切换时,当前 Fragment 的 onResume() 方法不会被调用,需要在 onHiddenChanged() 方法中请求服务器;

第二种,从其他 Activity 返回这个 Activity 时,当前 Fragment 不会调用 onHiddenChanged() 方法,需要在 onResume() 方法中请求服务器;

第三种,类似第二种场景,不同的是其他 Activity 返回时,还指定切换至处于隐藏状态的另一个 Fragment,对于这个 Fragment 来说,onResume() 和 onHiddenChanged()方法都会被调用;

综上所述,为了实现切换刷新操作,必须在onResume()和onHiddenChanged()方法中请求服务器。但是还得避免重复多次请求服务器操作,必须在两个方法中添加状态判断,只有对用户可见时,才刷新界面。示例代码如下:

  1. @Override
  2. public void onResume() {
  3. super.onResume();
  4. if (isVisible()){
  5. // 发起网络请求, 刷新界面数据
  6. requestData();
  7. }
  8. }
  9. @Override
  10. public void onHiddenChanged(boolean hidden) {
  11. super.onHiddenChanged(hidden);
  12. // 这里的 isResumed() 判断就是为了避免与 onResume() 方法重复发起网络请求
  13. if (isVisible() && isResumed()){
  14. requestData();
  15. }
  16. }

这种写法需要注意的是,第一个Fragment,也就是图中的OneFragment,必须在create时调用一次requestData()操作,如前面所说,此时onResume()方法中的isVisible()值为false;其他Fragment 则可以不用这么做,因为isVisible()值为true。

最后,还有一种特殊情况需要处理,就是系统由于内存不足时杀掉App的情况。如果当前显示的不是第一个 Fragment,App 被杀掉再次重启时,显示这个Fragment时,isVisible()的判断始终为false,这种情况下刷新数据的操作,还要额外处理,比如引入这个判断:

  1. @Override
  2. public void onActivityCreated(@Nullable Bundle savedInstanceState) {
  3. super.onActivityCreated(savedInstanceState);
  4. if (savedInstanceState!=null){
  5. requestData();
  6. }
  7. }

当然,这只是一种处理方式,有其独特的适应场景。其他应用场景,有其他的对应解决方案。可以看出,Fragment 的使用一定要多加注意,三思而后行,稍有不慎,就写了个坑!

setUserVisibleHint方法

前面我们讲过了,可以通过onHiddenChanged来判断Fragment的显示和隐藏,从而进行相关的操作。但是当Fragment和ViewPager结合使用的时候,由于ViewPager的预加载机制,在Viewpager里面的Fragment其生命周期会发生混乱,例如onResume方法在没有用户可见的情况下就会调用,这个时候onHiddenChanged也会失效。

这时候我们判断Fragment是否是被用户可见,可以用下面方法来完成:

  1. @Override
  2. public void setUserVisibleHint(boolean isVisibleToUser) {
  3. super.setUserVisibleHint(isVisibleToUser);
  4. if (getUserVisibleHint()) {
  5. //界面可见
  6. } else {
  7. //界面不可见 相当于onPause
  8. }
  9. }

关于setUserVisibleHint方法,有以下几个需要注意的地方:

第一, setUserVisibleHint是在onCreateView周期前调用,此时布局中各View还未初始化,所以只能在setUserVisibleHint中进行纯数据的加载。如果在这个方法里面使用控件的话,需要进行相应的判断。

第二, FragmentPagerAdapter和FragmentStatePagerAdapter会显示的对setUserVisibleHint()方法进行调用,而PagerAdapter不会对setUserVisibleHint()进行显示的调用,Fragment和ViewPager结合使用的时候,这点要注意。

  1. /**
  2. * @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),false if it is not.
  3. */
  4. public void setUserVisibleHint(boolean isVisibleToUser) {
  5. if (!mUserVisibleHint && isVisibleToUser && mState < STARTED) {
  6. mFragmentManager.performPendingDeferredStart(this);
  7. }
  8. mUserVisibleHint = isVisibleToUser;
  9. mDeferStart = !isVisibleToUser;
  10. }
  11. /**
  12. * @return The current value of the user-visible hint on this fragment.
  13. * @see #setUserVisibleHint(boolean)
  14. */
  15. public boolean getUserVisibleHint() {
  16. return mUserVisibleHint;
  17. }

如上面的代码所示,setUserVisibleHint()方法只是Fragment源码中的一个方法,他是需要手动调用的,之所以Fragment+ViewPager架构可以用这个方法来判断Fragment是否可见,是因为FragmentPagerAdapter中对这个方法进行了主动调用,源码如下:

  1. @Override
  2. public Object instantiateItem(ViewGroup container, int position) {
  3. ...
  4. if (fragment != mCurrentPrimaryItem) {
  5. fragment.setMenuVisibility(false);
  6. fragment.setUserVisibleHint(false);
  7. }
  8. return fragment;
  9. }
  10. @Override
  11. public void setPrimaryItem(ViewGroup container, int position, Object object) {
  12. Fragment fragment = (Fragment)object;
  13. if (fragment != mCurrentPrimaryItem) {
  14. if (mCurrentPrimaryItem != null) {
  15. mCurrentPrimaryItem.setMenuVisibility(false);
  16. mCurrentPrimaryItem.setUserVisibleHint(false);
  17. }
  18. if (fragment != null) {
  19. fragment.setMenuVisibility(true);
  20. fragment.setUserVisibleHint(true);
  21. }
  22. mCurrentPrimaryItem = fragment;
  23. }
  24. }

这个方法调用实际在FragmentPagerAdapter中。那么当你是使用FragmentTabHost+Fragment的结构的时候,你会发现这个方法压根不会被调用。

第三, setUserVisibleHint()方法在Fragment所有生命周期之前,无论ViewPager是在Activity哪个生命周期里初始化。Activity生命周期和Fragment生命周期时序并不是按序来的,也就是说Fragment的onCreate方法时序并不一定在Activity的onCreate方法之后。详细的分析建议阅读:Fragment的setUserVisibleHint详解(有源码分析)。

setUserVisibleHint() 方法,主要应用在Fragment懒加载中。

Fragment懒加载

懒加载,顾名思义,是希望在展示相应Fragment页面时再动态加载页面数据,数据通常来自于网络或本地数据库。这种做法的合理性在于用户可能不会滑到一下页面,同时还能帮助减轻当前页面数据请求的带宽压力,如果是用户使用流量的话,还能避免无用的流量消耗。Google之所以预加载相邻的fragment是为了滑动更加流畅,给用户更佳的体验感。

以下场景可以使用懒加载:

ViewPager在展示当前页面时,会同时预加载下一页面。事实上,可以通过ViewPager提供的setOffscreenPageLimit(int limit)方法设置ViewPager预加载的页面数量,默认值为1,并且这个参数的值不能小于1。所以也就无法通过这个方法实现ViewPager中Fragment的懒加载,一定要改ViewPager的话只能通过自定义一个Viewpager类来实现,这种做法就比较繁琐。其实可以从Fragment下手。

ViewPager本质上是通过Fragment调用setUserVisibleHint方法实现Fragment页面的展示与隐藏,这一点从FragmentPagerAdapter和FragmentStatePagerAdapter的源码和上面的截图中都可以看出。那么对应的解决方案就有了,自定义一个LazyLoadFragment基类,利用setUserVisibleHint和生命周期方法,通过对Fragment状态判断,进行数据加载,并将数据加载的接口提供开放出去,供子类使用。

示例代码一:

  1. public abstract class LazyLoadFragment extends BaseFragment {
  2. protected boolean isViewInitiated;
  3. protected boolean isDataLoaded;
  4. @Override
  5. public void onCreate(Bundle savedInstanceState) {
  6. super.onCreate(savedInstanceState);
  7. }
  8. @Override
  9. public void onActivityCreated(Bundle savedInstanceState) {
  10. super.onActivityCreated(savedInstanceState);
  11. isViewInitiated = true;
  12. prepareRequestData();
  13. }
  14. @Override
  15. public void setUserVisibleHint(boolean isVisibleToUser) {
  16. super.setUserVisibleHint(isVisibleToUser);
  17. prepareRequestData();
  18. }
  19. public abstract void requestData();
  20. public boolean prepareRequestData() {
  21. return prepareRequestData(false);
  22. }
  23. public boolean prepareRequestData(boolean forceUpdate) {
  24. if (getUserVisibleHint() && isViewInitiated && (!isDataLoaded || forceUpdate)) {
  25. requestData();
  26. isDataLoaded = true;
  27. return true;
  28. }
  29. return false;
  30. }
  31. }

然后在子类Fragment中实现requestData方法即可。这里添加了一个isDataLoaded变量,目的是避免重复加载数据。考虑到有时候需要刷新数据的问题,便提供了一个用于强制刷新的参数判断。实际上,在项目开发过程中,还需处理网络请求失败等特殊情况,我想,了解原理之后,这些问题都不再是问题。

示例代码二:

  1. public abstract class BaseFragment extends Fragment {
  2. private static final String TAG = "tianrui";
  3. protected boolean mIsVisible;
  4. protected boolean mHasLoaded;
  5. protected boolean mHasPrepare;
  6. @Override
  7. public void onCreate(@Nullable Bundle savedInstanceState) {
  8. super.onCreate(savedInstanceState);
  9. Log.d(TAG, "onCreate: ");
  10. }
  11. @Override
  12. public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
  13. Log.d("tianrui", "onViewCreated: ");
  14. super.onViewCreated(view, savedInstanceState);
  15. ButterKnife.bind(this, view);
  16. if (mIsVisible) {
  17. Log.d(TAG, "onViewCreated: load data in #onViewCreated ");
  18. initData();
  19. mHasLoaded = true;
  20. }
  21. mHasPrepare = true;
  22. }
  23. @Override
  24. public void setUserVisibleHint(boolean isVisibleToUser) {
  25. super.setUserVisibleHint(isVisibleToUser);
  26. Log.d("tianrui", "setUserVisibleHint: " + isVisibleToUser);
  27. mIsVisible = getUserVisibleHint();
  28. lazyLoad();
  29. }
  30. @Override
  31. public void onDestroyView() {
  32. Log.d(TAG, "onDestroyView: ");
  33. super.onDestroyView();
  34. mHasLoaded = false;
  35. mHasPrepare = false;
  36. }
  37. protected void lazyLoad() {
  38. Log.d(TAG, "lazyLoad: mIsVisible " + mIsVisible + " mHasLoaded " + mHasLoaded + " mHasPrepare " + mHasPrepare);
  39. if (!mIsVisible || mHasLoaded || !mHasPrepare) {
  40. return;
  41. }
  42. Log.d(TAG, "lazyLoad: load data in lazyLoad ");
  43. initData();
  44. }
  45. abstract protected void initData();
  46. }

子类实现initData()绑定数据即可。

两个方法对比

onhiddenchanged方法,当你用FragmentTransaction来控制Fragment的hide和show时,那么这个方法就会被调用。每当你对某个Fragment使用hide或者是show的时候,那么这个Fragment就会自动调用这个方法。
(使用情况:你自己去管理Fragment,而不是用viewpager管理的时候)

setUserVisibleHint方法,当我们在使用viewpager的时候,viewpager内部有个提前缓存的机制(默认是提前缓存一页),比如你在看第一个Fragment的时候,隔壁的Fragment已经创建好了,但此时的状态却是不可见的。 但是这时候Fragment不会去调用上面说的onhiddenchanged方法,只会调用setUserVisibleHint这个方法。

参考链接:

Fragment

Android Fragment 的使用,一些你不可不知的注意事项

[译]Android Activity 和 Fragment 状态保存与恢复的最佳实践

Android Fragment+ViewPager 组合,一些你不可不知的注意事项

Android~Fragment 中的常用方法 isAdded()、isVisible()、isHidden()、isRemoving()、isResumed()、 isInLayout() 等详解

onHiddenChanged

Fragment 使用hide和show,使用onHiddenChanged代替执行生命周期

android fragment onHiddenChanged的使用

【Android】Fragment中onHiddenChanged方法使用

fragment 切换判断界面是否可见 setUserVisibleHint和onHiddenChanged使用场景

ViewPager 显示Fragment 对Fragment 设置显示的监听onHiddenChanged不起作用

setUserVisibleHint

关于onhiddenchanged和setUserVisibleHint函数的知识

Fragment的setUserVisibleHint详解(有源码分析)

ViewPager+ Fragment结合的setUserVisibleHint()调用时机(有具体生命周期Log示例)

Android使用setUserVisibleHint()实现Fragment懒加载

Fragment的setUserVisibleHint方法实现懒加载,但setUserVisibleHint 不起作用?

您真的懂fragment的onResume,setUserVisibleHint,onHiddenChanged,isVisible方法吗!

ViewPager

Android——ViewPager缓存(预加载)机制及如何禁止预加载

关于禁止ViewPager预加载问题,完美解决!

Android中ViewPager+Fragment取消(禁止)预加载延迟加载(懒加载)问题解决方案

android 开发viewPager滑动实现禁止预加载机制

ViewPager中使用Fragment时防止数据预加载

Android Fragment 的使用,一些你不可不知的注意事项

[译]Android Activity 和 Fragment 状态保存与恢复的最佳实践

Android Fragment+ViewPager 组合,一些你不可不知的注意事项

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注