在很多项目中都有 Banner 广告页或者是 Tab 切换页,如果是如下布局的情况,那么肯定遇到过这个问题,那就是这个 ViewPager 并不会包裹内容填充,而是会填充满整个布局,如下:

看一下 ViewPager 中的绘制源码

View 的自身的宽高是和父 View 的剩余大小有关系的,那么在这个地方,由于 ViewPager 的父 View 是填充满整个屏幕的,所以 ViewPager 中的测量规格也是填充满整个屏幕的,而且也直接通过 setMeasuredDimension 设置了。所以也是填充满整个屏幕的。解决方式的话,可以通过自定义 View 的方式,测量子 View 的大小,然后再给定测量规格,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int height = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED));
            int h = child.getMeasuredHeight();
            if (h > height) {
                height = h;
            }
        }
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

接着再看看 setOffscreenPageLimit 这个方法,在早期的时候,我们可能为了 Fragment 可以提前加载好数据,或者在切换到第二个界面的时候,这个数据就已经加载好了。这里会遇到一个问题,就是即使我们设置的这个值是 0 那么也是无法起到作用的,因为源码中如果设置比 1 还小的数,最终也会设置为 1 个缓存的数量,如下源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    public void setOffscreenPageLimit(int limit) {
      	// DEFAULT_OFFSCREEN_PAGES 默认值就是 1 
        if (limit < DEFAULT_OFFSCREEN_PAGES) {
            Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
                    + DEFAULT_OFFSCREEN_PAGES);
            limit = DEFAULT_OFFSCREEN_PAGES; // DEFAULT_OFFSCREEN_PAGES 默认值是 1; 
        }
        if (limit != mOffscreenPageLimit) {
            mOffscreenPageLimit = limit;
            populate();
        }
    }

那么现在就引出了另外一个问题,那么就是当界面不可见的时候,用户就不想去加载数据,用户想在当前界面可见的时候才去加载数据,假设有一个 App 是如下类似这样的,其实也就是懒加载的情况了

其实这里还有个极端一点的情况,那么就是当 ViewPager + fragment 显示的时候,fragment 1 内部有一个 Banner,这个 fragment1 是用户启动 App 的时候就处于可见,那么假如 fragment2 内部也有一个 Banner,由于 ViewPager 的预加载特性,那么 Fragment2 中的数据也会被提前加载好了,其实是可以不必要的

懒加载

要掌握 Fragment 的懒加载,首先要了解一下 Fragment 的生命周期的变化,然后观察从哪个方法可以入手懒加载数据

Fragment 生命周期:onAttach -> onCreate -> onCreatedView -> onActivityCreated -> onStart -> onResume -> onPause -> onStop -> onDestroyView -> onDestroy -> onDetach

Fragment 的生命周期就是这些方法了,显而易见如果在这几个方法中做数据的懒加载,那么肯定是实现不了的。所以这里先了解一下 Fragment 不涉及生命周期的其他方法

setUserVisibleHint

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
     * 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;
    }

这个方法一般只会在 ViewPager 和 FragmentPagerAdapter 一起使用时才会触发。我们可以通过 getUserVisibleHint 来得到这个状态。isVisibleToUser 的值表示 Fragment 对用户是否可见,默认为 false

1
2
3
4
5
6
7
    /**
     * @return The current value of the user-visible hint on this fragment.
     * @see #setUserVisibleHint(boolean)
     */
    public boolean getUserVisibleHint() {
        return mUserVisibleHint;
    }

onHiddenChanged

1
2
3
4
5
6
7
8
9
/**
     * 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) {
    }

在我们使用show(fragment)和 hide(fragment)改变了 fragment 的显示状态时,会触发此函数,并且可以通过 isHidden() 来获取当前显示隐藏的状态,默认为 false

加入了这两个方法以后,再看一下 ViewPager + Fragment 生命周期是如何变化的,假设当前的界面是这样的:

首次启动 App, 该 Fragment1 和 Fragment2 的生命周期

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
1 fragment setUserVisibleHint: false
2 fragment setUserVisibleHint: false
1 fragment setUserVisibleHint: true
1 fragment onAttach: 
1 fragment onCreate: 
2 fragment onAttach: 
2 fragment onCreate: 
1 fragment onCreateView: 
1 fragment onResume: 
2 fragment onCreateView: 
2 fragment onResume: 

点击 Fragment 2 的生命周期

1
2
3
4
5
6
7
3 fragment setUserVisibleHint: false
1 fragment setUserVisibleHint: false
2 fragment setUserVisibleHint: true
3 fragment onAttach: 
3 fragment onCreate: 
3 fragment onCreateView: 
3 fragment onResume: 

点击 Fragment 3 的生命周期

1
2
3
4
5
6
7
8
4 fragment setUserVisibleHint: false
2 fragment setUserVisibleHint: false
3 fragment setUserVisibleHint: true
4 fragment onAttach: 
4 fragment onCreate: 
1 fragment onPause: 
4 fragment onCreateView: 
4 fragment onResume: 

点击 Fragment5 的生命周期

1
2
3
4
5
6
7
8
9
5 fragment setUserVisibleHint: false
3 fragment setUserVisibleHint: false
5 fragment setUserVisibleHint: true
5 fragment onAttach: 
5 fragment onCreate: 
5 fragment onCreateView: 
5 fragment onResume: 
3 fragment onPause: 
2 fragment onPause: 

回到 Fragment2 的生命周期

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
2 fragment setUserVisibleHint: false
1 fragment setUserVisibleHint: false
3 fragment setUserVisibleHint: false
5 fragment setUserVisibleHint: false
2 fragment setUserVisibleHint: true
2 fragment onCreateView: 
2 fragment onResume: 
1 fragment onCreateView: 
3 fragment onCreateView: 
1 fragment onResume: 
3 fragment onResume: 
4 fragment onPause: 
5 fragment onPause: 

首次进入 Fragment1,然后直接点击跳转到 Fragment5 的变化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 首次进入 App
1 fragment setUserVisibleHint: false
2 fragment setUserVisibleHint: false
1 fragment setUserVisibleHint: true
1 fragment onAttach: 
1 fragment onCreate: 
2 fragment onAttach: 
2 fragment onCreate: 
1 fragment onCreateView: 
1 fragment onResume: 
2 fragment onCreateView: 
2 fragment onResume: 
// 跳转到 Fragment5 
5 fragment setUserVisibleHint: false
4 fragment setUserVisibleHint: false
1 fragment setUserVisibleHint: false
5 fragment setUserVisibleHint: true
5 fragment onAttach: 
5 fragment onCreate: 
4 fragment onAttach: 
4 fragment onCreate: 
5 fragment onCreateView: 
5 fragment onResume: 
4 fragment onCreateView: 
4 fragment onResume: 
2 fragment onPause: 
1 fragment onPause: 

通过观察这几个 Fragment 的变化,可以发现都会伴随着 setUserVisibleHint 的一个方法的调用,最终发现一些规律:

  • setUserVisibleHint 会优先于 Fragment 的生命周期的调用
  • 相邻的 Fragment 会调用 setUserVisibleHint 并且 isVisibleToUser 的值为 false,当前可见的 Fragment isVisibleToUser 会为 true
  • 如果当前的 Fragment 未被(缓存/创建),并且相邻的 Fragment 都未被(缓存/创建),都会调用 setUserVisibleHint 方法,当前的 Fragment 的 isVisibleToUser 会为 true,并且相邻之间的 Fragment 都会调用生命周期方法

所以如果设计 Fragment 的懒加载,需要 setUserVisibleHint 方法的配合来使用,最终的方案如下:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
public abstract class LazyFragment extends Fragment {
    private static final String TAG = "LazyFragment";

    protected View rootView = null;
    // view 是否已经创建
    boolean isViewCreated = false;
    // 是否第一次创建的标志位
    boolean mIsFirstVisible = true;

    // 记录当前的 Fragment 可见状态
    boolean currentVisibleState = false;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        if (rootView == null) {
            rootView = inflater.inflate(getLayoutRes(), container, false);
        }
        
        initView(rootView);
        isViewCreated = true;
      
      	// isHidden() 表示当前的 Fragment 是否隐藏默认为 false
        // getUserVisibleHint 获取 Fragment 是否显示,当前首次为 true
        if (!isHidden() && getUserVisibleHint()) {
            dispatchUserVisibleHint(true);
        }
        return rootView;
    }

    protected abstract int getLayoutRes();

    protected abstract void initView(View view);

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.d(TAG, "setUserVisibleHint: ");
        // 1.由于 setUserVisibleHint 会优先于生命周期方法的调用,所以需要判断 View 是否创建过
        if (isViewCreated) {
          	// 2.该处是判断 Fragment 的可见状态到不可见状态,或者是不可见状态到可见状态
          
            if (isVisibleToUser && !currentVisibleState ) {
                // 2.1 不可见状态 -> 可见状态
                dispatchUserVisibleHint(true);
            } else if (!isVisibleToUser && currentVisibleState) {
                // 2.2 可见状态 -> 不可见状态
                dispatchUserVisibleHint(false);
            }
        }
    }

    /**
     * 统一处理用户可见信息分发
     * 分第一次可见、可见、不可见分发
     */
    private void dispatchUserVisibleHint(boolean isVisible) {
        if (currentVisibleState == isVisible) return;
        currentVisibleState = isVisible;
      
        if (isVisible) {
          	// 第一次可见
            if (mIsFirstVisible) {
                mIsFirstVisible = false;
                onFragmentFirstVisible();
            }
            onVisible();
        } else {
            onInVisible();
        }
    }

    /**
     * 用 FragmentTransaction 来控制 Fragment 的 hide 和 show 时,
     * 那么这个方法就会被调用。每当你对某个 Fragment 使用 hide
     * 或者是 show 的时候,那么这个 Fragment 就会自动调用这个方法。
     * https://blog.csdn.net/u013278099/article/details/72869175
     *
     * 你会发现使用 hide 和 show 这时 Fragment 的生命周期不再执行,
     * 不走任何的生命周期,
     * 这样在有的情况下,数据将无法通过生命周期方法进行刷新,
     * 所以你可以使用 onHiddenChanged 方法来解决这问题。
     * @param hidden
     */
    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        if (hidden) {
            dispatchUserVisibleHint(false);
        } else {
            dispatchUserVisibleHint(true);
        }
    }

    protected abstract void onFragmentFirstVisible();

    protected void onVisible() {
    }

    protected void onInVisible() {
    }



    @Override
    public void onResume() {
        super.onResume();
      	// onResume 并不代表 fragment 可见

        // 起过滤作用,处理回退到 Activity 的时候,如果从未加载过 Fragment,就不调用 dispatchUserVisibleHint(true)
        if (!mIsFirstVisible) {
          	// isHidden() 兼容 hide 和 show 的方法
            if (!isHidden() && !currentVisibleState && getUserVisibleHint()) {
              	// 由于会存在当前的 Activity1 缓存了多个 Fragment,此时跳转到 Activity2,那么 Activity1 中的Fragment 都被缓存起来了
                // 所以当从其他 Activity2 中回来 Activity1 的时候,只有当前的 Fragment 才去加载,其余相邻缓存的 Fragment 不做处理 (处理回退的情况)
                dispatchUserVisibleHint(true);
            }
        }
    }

    /**
     * 只有当当前页面由可见状态转变到不可见状态时才需要调用 dispatchUserVisibleHint
     * currentVisibleState && getUserVisibleHint() 能够限定是当前可见的 Fragment
     */
    @Override
    public void onPause() {
        super.onPause();
        if (currentVisibleState && getUserVisibleHint()) {
            dispatchUserVisibleHint(false);
        }
    }


    @Override
    public void onDestroyView() {
        super.onDestroyView();
        isViewCreated = false;
        mIsFirstVisible = false;
    }
}

好了,代码就如上所示了,源码注释都有相关的注释说明,如果需要懒加载只需要继承 LazyFragment ,然后再 onVisible 界面可见的时候加载需要的数据,界面不可见的时候停止相关操作