ViewPager+Fragment懒加载自我救赎之路

对于ViewPager+Fragment的组合使用,想必所有的Android开发者都不会陌生吧。它在Android开发中是非常常用的,其重要性不言而喻。但是这个组合的常规使用是存在问题的,今天就和大家分享一下我在使用ViewPager+Fragment的过程中遇到的问题,并分析其中的原因,以及解决方案。现在开始我们的自我救赎之路吧。

首先罗列出ViewPager+Fragment组合使用过程中遇到的问题:

1. ViewPager的layout_height属性设置为wrap_content或者某一具体的值无效

2. ViewPager.setOffscreenPageLimit(0)无效的问题

3. Fragment的常规懒加载

4. ViewPager+Fragment嵌套使用ViewPager+Fragment的时候,Fragment懒加载的问题

想必这几个问题,对于用过ViewPager+Fragment的同学来说并不会感到陌生吧。现在我就来一一分析一下这四个问题。

一、ViewPager的layout_height属性设置为wrap_content或者某一具体值无效

为了分析这个问题首先我们需要了解一下View的Measure过程,我有一篇文章就是专门分析View的三大流程的,Android View的工作流程分析学习,大家有兴趣可以去看一下,这里不再对此问题做介绍。对于其他的ViewGroup而言它的Measure过程,首先会先去测量子View的宽和高,然后再去测量自身的宽和高。现在我们来看一下ViewPager源码中对测量过程是怎么描述的。源码如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // For simple implementation, our internal size is always 0.
        // We depend on the container to specify the layout size of
        // our view.  We can't really know what it is since we will be
        // adding and removing different arbitrary views and do not
        // want the layout to change as this happens.
        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
                getDefaultSize(0, heightMeasureSpec));

        ......

}

源码中对测量过程的描述篇幅还是比较长的,这里仅仅贴出最重要的部分。熟悉View测量流程的同学对setMeasuredDimension方法应该不陌生吧。它必须被onMeasure方法调用,其作用是保存测量的宽度和高度。看到这里大家可能看出了一点端倪,一般ViewGroup的测量流程是先测量子View的宽高然后再测量自身的宽高,而ViewPager这个自私的蠢货它在一进入onMeasure方法就直接保存了自身测量的宽高,根本就没有理会其子View的宽和高的信息。所以,它的宽高信息根本不会受到其子View宽高信息的影响。其解决办法就是自定义一个ViewPager重写其onMeasure方法重新定义它的高。想必这个问题大家也不陌生,解决办法许多文章也都提到过,此处不再对此问题做详细的描述。

二、ViewPager.setOffscreenPageLimit(0)无效的问题

现在我们为这个问题寻找一下证据。首先看一下ViewPager+Fragment组合的简单使用,从它们的使用中为此问题找寻一下证据。

public class NormalStartActivity extends AppCompatActivity {

    private ViewPager mViewPager;
    private BottomNavigationView bottomNavigationView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mViewPager = findViewById(R.id.viewPager);
        bottomNavigationView = findViewById(R.id.bottomNavigationView);
        bottomNavigationView.setOnNavigationItemSelectedListener(onNavigationItemSelectedListener);
//        BottomNavigationViewHelper.disableShiftMode(bottomNavigationView);
        List<Fragment> fragmentList = new ArrayList<>();
        fragmentList.add(NormalFragment.newInstance(1));
        fragmentList.add(NormalFragment.newInstance(2));
        fragmentList.add(NormalFragment.newInstance(3));
        fragmentList.add(NormalFragment.newInstance(4));
        fragmentList.add(NormalFragment.newInstance(5));
        NormalFragmentPagerAdapter pagerAdapter = new NormalFragmentPagerAdapter(getSupportFragmentManager(), fragmentList);
        mViewPager.setAdapter(pagerAdapter);
        // 需要关注的就是setOffscreenPageLimit传的参数
        mViewPager.setOffscreenPageLimit(0);
        mViewPager.setOnPageChangeListener(viewPagerPageChangeListener);
    }

    ViewPager.OnPageChangeListener viewPagerPageChangeListener = new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

        }

        @Override
        public void onPageSelected(int position) {
            int itemId = R.id.fragment_1;
            switch (position) {
                case 0:
                    itemId = R.id.fragment_1;
                    break;
                case 1:
                    itemId = R.id.fragment_2;
                    break;
                case 2:
                    itemId = R.id.fragment_3;
                    break;
                case 3:
                    itemId = R.id.fragment_4;
                    break;
                case 4:
                    itemId = R.id.fragment_5;
                    break;
            }
            bottomNavigationView.setSelectedItemId(itemId);
        }

        @Override
        public void onPageScrollStateChanged(int state) {

        }
    };

    BottomNavigationView.OnNavigationItemSelectedListener onNavigationItemSelectedListener = new BottomNavigationView.OnNavigationItemSelectedListener() {
        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
            boolean result;
            switch (menuItem.getItemId()) {
                case R.id.fragment_1:
                    mViewPager.setCurrentItem(0, true);
                    result = true;
                    break;
                case R.id.fragment_2:
                    mViewPager.setCurrentItem(1, true);
                    result = true;
                    break;
                case R.id.fragment_3:
                    mViewPager.setCurrentItem(2, true);
                    result = true;
                    break;
                case R.id.fragment_4:
                    mViewPager.setCurrentItem(3, true);
                    result = true;
                    break;
                case R.id.fragment_5:
                    mViewPager.setCurrentItem(4, true);
                    result = true;
                    break;
                default:
                    result = false;
                    break;
            }
            return result;
        }
    };

}

这是Activity的代码,现在我把Fragment的代码也贴出来,这样大家可能看的更加清晰一些,不过这会增加文章的篇幅。

public class NormalFragment extends Fragment {

    private static final String TAG = "TODAY";

    public static final String INTENT_INT_INDEX = "index";
    int tabIndex;
    ImageView imageView;
    TextView textView;
    CountDownTimer count;


    public static NormalFragment newInstance(int tabIndex) {
        Bundle bundle = new Bundle();
        bundle.putInt(INTENT_INT_INDEX, tabIndex);
        NormalFragment fragment = new NormalFragment();
        fragment.setArguments(bundle);
        return fragment;
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Log.d(TAG, tabIndex + " fragment " + "onAttach: ");
        tabIndex = getArguments().getInt(INTENT_INT_INDEX);
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, tabIndex + " fragment " + "onCreate: ");
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_lazy_loading, null);
        imageView = view.findViewById(R.id.iv_content);
        textView = view.findViewById(R.id.tv_loading);
        getData();
        Log.d(TAG, tabIndex + " fragment " + "onCreateView: " );
        return view;
    }

    private void getData(){
        count = new CountDownTimer(1000,100) {
            @Override
            public void onTick(long millisUntilFinished) {

            }

            @Override
            public void onFinish() {
                handler.sendEmptyMessage(0);
            }
        };
        count.start();
    }


    private Handler handler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            textView.setVisibility(View.GONE);
            int id;
            switch (tabIndex){
                case 1:
                    id = R.drawable.a;
                    break;
                case 2:
                    id = R.drawable.b;
                    break;
                case 3:
                    id = R.drawable.c;
                    break;
                case 4:
                    id = R.drawable.d;
                    break;
                default:
                    id = R.drawable.a;
            }
            imageView.setImageResource(id);
            imageView.setScaleType(ImageView.ScaleType.FIT_XY);
            imageView.setVisibility(View.VISIBLE);
            Log.d(TAG, tabIndex +" handleMessage: " );
            // 模拟耗时操作
            try {
                Thread.sleep(40);
                Log.d(TAG, tabIndex +" 做了耗时操作" );
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };

    @Override
    public void onResume() {
        super.onResume();
        Log.d(TAG, tabIndex + " fragment " + "onResume: " );
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.d(TAG, tabIndex + " fragment " + "onPause: " );
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        tabIndex = getArguments().getInt(INTENT_INT_INDEX);
        Log.d(TAG, tabIndex + " fragment " + "setUserVisibleHint: " + isVisibleToUser );
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.d(TAG, tabIndex + " fragment " + "onDetach: " );
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        count.cancel();
        Log.d(TAG, tabIndex + " fragment " + "onDestroyView: " );
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, tabIndex + " fragment " + "onDestroy: " );
    }
}

对于ViewPager的适配器那些代码我没有给出,我觉得没有必要,大家都用过肯定都没问题。很简单,没有什么要说的。主要看一下对setOffscreenPageLimit方法调用的时候传的实参。现在我们传的是0。看一下此时的运行效果:

我们看到setOffscreenPageLimit(0)时,未经过任何滑动或者点击Tab操作而得到的结果是Fragment1和Fragment2都执行了模拟数据加载的操作。这说明此时进行了一帧数据的缓存。当我们把调用次函数的地方传递的参数由0改成1之后我们再来看运行结果:

我们发现结果是一样的。现在我们再来滑动一次底部Tab页,看一下打印的结果是什么:

从打印的信息中可以看到滑动一次Tab页,此时Fragment2的setUserVisibleHint的值为true,而Fragment3做出了模拟数据加载的操作。此时我们可以得出结论,setOffscreenPageLimit()方法中传递的实参为0和为1时的效果是一致的,这也就证明了setOffscreenPageLimit(0)是无效的。现在我们从源码中去寻找一下原因,源码如下:

/**
     * Set the number of pages that should be retained to either side of the
     * current page in the view hierarchy in an idle state. Pages beyond this
     * limit will be recreated from the adapter when needed.
     *
     * <p>This is offered as an optimization. If you know in advance the number
     * of pages you will need to support or have lazy-loading mechanisms in place
     * on your pages, tweaking this setting can have benefits in perceived smoothness
     * of paging animations and interaction. If you have a small number of pages (3-4)
     * that you can keep active all at once, less time will be spent in layout for
     * newly created view subtrees as the user pages back and forth.</p>
     *
     * <p>You should keep this limit low, especially if your pages have complex layouts.
     * This setting defaults to 1.</p>
     *
     * @param limit How many pages will be kept offscreen in an idle state.
     */
    public void setOffscreenPageLimit(int limit) {
        if (limit < DEFAULT_OFFSCREEN_PAGES) {
            Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
                    + DEFAULT_OFFSCREEN_PAGES);
            limit = DEFAULT_OFFSCREEN_PAGES;
        }
        if (limit != mOffscreenPageLimit) {
            mOffscreenPageLimit = limit;
            populate();
        }
    }

阅读源码可以得知,DEFAULT_OFFSCREEN_PAGES的值为1,源码中对传递的参数limit做了判断,如果limit小于1,就将limit置为1。ViewPager的源码已经做了处理,所以我们在外边设置实参为0的时候是没有效果的。源码中下面的if判断中调用了populate()方法。这个方法我们需要看一下它的源码,它几乎是ViewPager中最重要的方法了。源码如下:

 void populate(int newCurrentItem) {
       
        .......

        if (mAdapter == null) {
            sortChildDrawingOrder();
            return;
        }

       
        .......
    
        // Adapter开始更新画面
        mAdapter.startUpdate(this);

        final int pageLimit = mOffscreenPageLimit;
        final int startPos = Math.max(0, mCurItem - pageLimit);
        // mAdapter.getCount()获取Adapter中的item数量
        final int N = mAdapter.getCount();
        final int endPos = Math.min(N - 1, mCurItem + pageLimit);
        // 缓存空间[startPos,endPos]也就是[mCurItem - pageLimit,mCurItem + pageLimit]
        

        if (curItem == null && N > 0) {
            // 当前Item为空,增加Item
            curItem = addNewItem(mCurItem, curIndex);
        }

        // Fill 3x the available width or up to the number of offscreen
        // pages requested to either side, whichever is larger.
        // If we have no current item we have no work to do.
        if (curItem != null) {
            // 当前显示的item左边的部分进行处理
            
            for (int pos = mCurItem - 1; pos >= 0; pos--) {
                
                ......

                // 根据传入的pos销毁一个item
                mAdapter.destroyItem(this, pos, ii.object);

                ......
                       
            }
            
            // 对当前显示的item右边的部分进行处理
            float extraWidthRight = curItem.widthFactor;
            itemIndex = curIndex + 1;
            if (extraWidthRight < 2.f) {
                
                for (int pos = mCurItem + 1; pos < N; pos++) {
                    
                     ......

                     mAdapter.destroyItem(this, pos, ii.object);

                     ......
                            
                }
            }

            // 设置当前item
            mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
        }

        // Adapter结束更新
        mAdapter.finishUpdate(this);

        
        ......

    }

仅贴出了关键的部分,我们都知道ViewPager的使用时需要PagerAdapter来配合的,而populate方法中几乎出现了PagerAdapter中的所有方法。可见它的重要性。我们主要再看一下addItem方法。源码如下:

ItemInfo addNewItem(int position, int index) {
        ItemInfo ii = new ItemInfo();
        ii.position = position;
        // 构建一个Item
        ii.object = mAdapter.instantiateItem(this, position);
        // 返回给定页面的比例宽度
        ii.widthFactor = mAdapter.getPageWidth(position);
        if (index < 0 || index >= mItems.size()) {
            mItems.add(ii);
        } else {
            mItems.add(index, ii);
        }
        return ii;
}

代码不多,全贴出来了。ViewPager的addItem方法中调用了mAdapter.instantiateItem方法,并且将构建的item保存到了一个mItems里,mItems就是专门用于缓存的ArrayList。不仅如此,,populate方法中还调用了mAdapter.destroyItem来销毁一个item,通过mAdapter.setPrimaryItem方法可以通知Adapter当前Item是主要的Item。从开始更新到停止更新可以看出populate()与整个Adapter的声明周期方法是紧密绑定的。这就意味着,adapter的所有流程都是由populate管理的,意味着每一个item的管理都是由populate()控制的,也意味着缓存是由populate()方法控制的。至此,第二个问题我们也分析完了。

三、Fragment的常规懒加载

首先,我们需要弄清楚什么是懒加载?为什么需要懒加载?通俗的讲懒加载就是在Fragment可见的时候才去加载数据不可见的时候我们不需要去加载数据。上面我们已经看到过ViewPager+Fragment组合没有考虑处理懒加载时的运行结果,它是会至少缓存一页数据。这样对于用户而言是不友好的,会造成流量的浪费,体验会比较差,对于我们开发者而言也可能导致数据更新不及时的情况发生。所以,基于此我们需要去解决这个问题。这就是我们为什么需要懒加载的原因。但为什么又叫做常规的懒加载呢,难道还有非常规的吗?之所以说是常规的懒加载是因为,在这一小节我们暂不考虑ViewPager+Fragment再去嵌套一层ViewPager+Fragment这种情况的懒加载的处理,先只考虑一层的懒加载。

上面setOffscreenPageLimit(0)的运行结果我们也看到了setUserVisibleHint方法的执行时机是Fragment的所有声明周期之前。并且,先会执行Fragment1.setUserVisibleHint(false)和Fragment2.setUserVisibleHint(false),然后再执行Fragment1.setUserVisibleHint(true)的情况。至于onResume方法在Fragment2不可见的时候也已经执行了。所以在此我们需要打破对生命周期方法的常规认识了,之前认为onResume只有在可见的时候才会执行到。现在看来可完全不是那么回事儿哦!话不多说,我们来分析一下懒加载该怎么实现吧。

1. 作为一个Fragment的通用类首先我们得考虑到Fragment加载不同xml布局文件的事情吧,这个不用多说很好理解。需要我们重写onCreateView方法。代码如下:

@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 用于添加默认的界面
        initView(rootView);
        // 将View创建完成标志位设置为true
        isViewCreated = true;
        Log.e(TAG, "onCreateView:");
        // 本次分发主要是用于分发默认tab可见状态,这种情况下它的生命周期是:
        // fragment setUserVisibleHint: true -> onAttach -> onCreate -> onCreateView -> onResume
        // 默认Tab getUserVisibleHint () = true !isHidden() = true
        // 对于非默认tab或者非默认显示的fragment在该生命周期中只做了isViewCreated标志位设置,可见状态将不会在这里分发
        if (!isHidden() && getUserVisibleHint()) {
            dispatchUserVisibleHint(true);
        }
        return rootView;
    }

通过对isHidden()和getUserVisibleHint()返回值的判断可以确定当前Fragment是否可见,dispatchUserVisibleHint(true/false)方法就是统一处理用户可见信息分发。getLayoutRes()和initView(rootView)是抽象方法,共它的继承者重写的。

2.  我们再来分析setUserVisibleHint(true/false)方法

作用:我们看一下源码

/**
     * 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() && mIsCreated) {
            mFragmentManager.performPendingDeferredStart(this);
        }
        mUserVisibleHint = isVisibleToUser;
        mDeferStart = mState < STARTED && !isVisibleToUser;
        if (mSavedFragmentState != null) {
            // Ensure that if the user visible hint is set before the Fragment has
            // restored its state that we don't lose the new value
            mSavedUserVisibleHint = isVisibleToUser;
        }
    }

分析它的作用,主要解释一下该方法的注释:

1)向系统设置一个标示,说明该Fragment的UI当前是否对用户可见。这个标示默认为true,并且跨Fragment实例状态保存和恢复是永久的。

2)应用程序可以将其设置为false,以指示Fragment的UI已经滚动到不可见的位置,或者对用户不直接可见。系统可以使用它来对诸如Fragment生命周期更新或加载程序排序行为等操作进行优先等级排序。

3)这个方法可以在Fragment生命周期之外调用。因此对于Fragment生命周期方法调用没有顺序保证。

它的调用是在FragmentPagerAdapter类中setPrimaryItem方法中。分析完它的作用之后,我们来看一下在实现懒加载的时候我们应该在setUserVisibleHint方法中做一些什么处理。代码如下:

// 修改Fragment的可见性
    // setUserVisibleHint 被调用有两种情况
    // 1) 在tab切换的时候,会先于Fragment其他的所有生命周期调用
    //      对于默认 tab 和间隔 checked tab 需要等到isViewCreated = true 后才可以通过此方法通知用户是否可见
    // 2) 对于之前已经调用过setUserVisibleHint 方法的Fragment,可以当做Fragment从可见到不可见之间状态变化的依据
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.e(TAG, "setUserVisibleHint: ");
        // 对于情况1,不予处理,用isViewCreated进行判断,如果isViewCreated为false,说明它没有被创建
        if (isViewCreated) {
            // 对于情况2,又要分两种情况讨论 2.1) Fragment从不可见 -> 可见;2.2) Fragment从可见 -> 不可见
            // 对于2.1)我们需要怎么判断呢?首先是可见的(isVisibleToUser一定为true)
            //      而且只有当可见状态发生改变的时候才需要切换(此时就添加了currentVisibleState来辅助判断),否则容易出现反复调用的情况
            //      从而导致事件分发带来的多次更新
            // 对于2.2)如果是可见->不可见,判断条件恰好和 2.1)相反
            if (isVisibleToUser && !currentVisibleState) { // 从不可见 - > 可见状态
                dispatchUserVisibleHint(true);
            } else if (!isVisibleToUser && currentVisibleState) {
                dispatchUserVisibleHint(false);
            }
        }
    }

它的调用时机分为两种情况,注释中已经写清楚了。只是currentVisibleState这个变量需要注意一下,它是用来记录当前的Fragment当前是否是可见状态的。

3. dispatchUserVisibleHint(true/false)方法已经出现过几次了,现在我们来分析它的实现

首先它的作用就是统一分发用户是否可见的信息。分为第一次可见,后面可见以及不可见分发。其实现代码如下:

/**
     * 统一处理用户可见信息分发
     * 分第一次可见,可见,不可见分发
     *
     * @param isVisible
     */
    private void dispatchUserVisibleHint(boolean isVisible) {
        Log.e(TAG, "dispatchUserVisibleHint: ");
        //为了代码严谨
        if (currentVisibleState == isVisible) {
            return;
        }
        currentVisibleState = isVisible;
        if (isVisible) {
            // 可见也分为第一次可见与后面可见
            if (mIsFirstVisible) {
                mIsFirstVisible = false;
                onFragmentFirstVisible();
            }
            onFragmentResume();
        } else {
            onFragmentPause();
        }
    }

在此对currentVisibleState变量进行了赋值。第一次可见的时候调用一个抽象方法onFragmentFirstVisible去做第一次可见状态下该做的操作,例如,网络请求并缓存数据等。而onFragmentResume就是用于通知用户,可见状态调用并显示加载数据。onFragmentPause方法是在当前item不可见的时候调用的,通知用户当前item不可见,中断加载数据。这三个方法都是抽象方法,具体的操作需要放置到具体继承该类的具体Fragment实例中去。

至此,一个懒加载Fragment的大体框架就已经搭载出来了,基本也就是这些东西,只不过还有一些重要的细节需要我们继续推敲。

4. 打破常规的onResume方法

通过上面我们给出的运行结果可以看出,在Fragment不可见的时候onResume方法也有可能已经被调用了。但是此时分明是不需要进行数据加载的。是否还有其他类似的情况呢?这里给出两类:

1)在滑动或者跳转过程中,第一次创建Fragment的时候都会调用onResume方法,类似于在tab1滑动到tab2时,此时tab3 会缓存,这个时候tab3的onResume方法已经被调用了,但是此时是不需要去调用dispatchUserVisibleHint(true)的。因此需要对此做一处理。

2)如果Activity1中有多个Fragment,然后从Activity1跳转到Activity2,此时会有多个Fragment在Activity1 中缓存。此时,如果再从Activity2跳回到Activity1,这个时候将会执行缓存在Activity1中的所有Fragment的onResume方法。但是,此时好像我们也不需要对所欲缓存的Fragment执行dispatchUserVisibleHint(true)的操作。因此,这种情况也需要做一处理。

现在我们看代码:

@Override
    public void onResume() {
        super.onResume();
        Log.e(TAG, "onResume:");
        // 在滑动或者跳转过程中,第一次创建Fragment的时候都会调用onResume方法,类似于在tab1滑动到tab2,此时tab3会缓存这个时候会调用tab3
        // 的onResume方法,所以此时是不需要去调用dispatchUserVisibleHint(true)的,因而出现了下面的判断
        if (!mIsFirstVisible) {
            // 由于Activity1中如果有多个Fragment,然后从Activity1跳转到Activity2,此时会有多个Fragment会在Activity1 中缓存,
            // 此时,如果再从Activity2跳回到Activity1,这个时候将会执行所有缓存的Fragment的onResume方法,这个时候我们无需对所有缓存
            // 的fragment调用dispatchUserVisibleHint(true),我们只需要对可见的Fragment进行加载,因此有了下面的判断
            if (!isHidden() && !currentVisibleState && getUserVisibleHint()) {
                dispatchUserVisibleHint(true);
            }
        }
    }

代码中的注释写的很详细,因此就不再做解释了。

至于onPause和onDestroyView方法做的处理就比较简单了。onPause一定是在当前页面由可见转变为不可见的时候调用的,而onDestroyView方法只需要将isViewCreated和mIsFirstVisible重置就好了。代码如下:

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

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

至此,常规懒加载的代码实现算是结束了。至于Activity和继承自懒加载Fragment的Fragment由于篇幅原因我没有贴出来,没有什么特殊的地方。现在我们看一下在使用了懒加载Fragment之后的运行结果是什么样子的吧。

这是刚运行上来没有做任何滑动或者点击tab操作的时候的运行结果。我们可以看到它与上边未使用懒加载Fragment的区别是,未使用时,Fragment2也会执行模拟的耗时操作,而使用了懒加载之后只有在当前Fragment被用户可见的时候才会执行更新页面这样需要加载数据的操作。然后我们再看一下滑动一下底部tab页时,运行结果是什么样的?

滑动一次tab页用户能看到的结果是,Fragment2变成了当前可见的Fragment。此时我们看到的log是Fragment1暂停了一切操作,Fragment2更新了页面,而对于Fragment3执行了onResume。但是此时Fragment3是不可见的,所以它没有做耗时操作。所以,由此我们需要注意的是,对于Fragment而言执行onResume不代表是可见的状态,这需要我们推翻对生命周期的惯性认识。以上我们真正实现了只有当Fragment处于可见状态时才会加载数据的目标。

至此,我们的常规懒加载算是分析完了。下一小节我们分析一下ViewPager+Fragment嵌套使用场景下的懒加载。

四、ViewPager的深层嵌套懒加载

ViewPager嵌套使用的场景不难理解,就是在ViewPager+Fragment组合大的框架下,其中的某一个Fragment里又使用了一套ViewPager+Fragment。这样的场景也很常见,并且,也有其特殊的地方,所以我们将它单独拿出来分析一下。

首先,先描述一下我构造的Demo场景。外层有五个Fragment其中在第二个Fragment中又嵌套使用了一组ViewPager+Fragment。现在看一下,使用第三节中分析的懒加载Fragment运行结果会是什么样的?

图中的log是刚运行上来的运行结果,此时外层第一个Fragment是可见的,对于Fragment2和内层的Fragment都是不可见的。但是图中的log显示Fragment2_vp_1已经执行了onFragmentResume方法,也就是执行了模拟的耗时操作。这跟我们懒加载的宗旨可见方可加载是相违背的。此时,我们就需要对此情况作出处理。

处理方式就是:在外层Fragment可见的情况下才去向内层Fragment分发可见与不可见的信息。我们将这句话涉及到的逻辑转换成代码看一下。

首先我们需要在dispatchUserVisibleHint方法中增加一个,只有当前Fragment的父Fragment可见的时候才去分发可见事件的逻辑处理。

// 事实上作为父Fragment的BottomTabFragment2并没有分发可见事件,
// 它通过getUserVisibleHint得到的是false
// 因此我们需要在负责分发可见事件的方法中添加一个当前父Fragment是否可见的判断
// 如果当前父Fragment不可见我们就不分发可见事件
if (isVisible && isParentInVisible()) {
    return;
}

相信这个逻辑大家都是理解的。现在看一下判断父Fragment是否可见的方法isParentInVisible。

private boolean isParentInVisible() {
    Fragment parentFragment = getParentFragment();
    if (parentFragment instanceof LazyFragment3) {
        LazyFragment3 fragment = (LazyFragment3) parentFragment;
        return !fragment.isSupportVisible();
    }
    return false;
}

private boolean isSupportVisible() {
    return currentVisibleState;
}

最后我们还需要一个方法,目的是做当满足条件时分发可见事件给自己内嵌的所有Fragment知晓。

private void dispatchChildVisibleState(boolean visible) {
    FragmentManager manager = getChildFragmentManager();
    List<Fragment> fragments = manager.getFragments();
    for (Fragment fragment : fragments) {
        if (fragment instanceof LazyFragment3
            && !fragment.isHidden()
            && fragment.getUserVisibleHint()) {
                ((LazyFragment3) fragment).dispatchUserVisibleHint(visible);
        }
    }
}

这个方法的调用是在dispatchUserVisibleHint方法中,在当前Fragment可见时调用。

// 在当前Fragment可见时,调用此方法加载数据
onFragmentResume();
// 在双层Fragment嵌套的情况下,第一次滑动Fragment嵌套ViewPager(Fragment)的时候
// 此时只会加载外层Fragment的数据,而不会加载Fragment内嵌套的ViewPager中的Fragment的数据,
// 因此我们需要在此增加一个当外层Fragment可见的时候,分发可见事件给自己内嵌的所有Fragment显示
dispatchChildVisibleState(true);

当然,在当前Fragment不可见时,也需要调用分发不可见事件给自己内嵌的Fragment知晓。

// 分发不可见状态
onFragmentPause();
dispatchChildVisibleState(false);

至此,深层嵌套ViewPager的懒加载也分析完了。现在还有一个小尾巴,那就是在使用FragmentTrsaction来控制Fragment的hide和show时,onHiddenChanged方法会被调用到。因此,在此方法中也需要去分发可见与不可见事件。代码如下:

/**
  *
  * 用FragmentTransaction来控制fragment的hide和show时,
  * 那么这个方法就会被调用。每当你对某个Fragment使用hide
  * 或者是show的时候,那么这个Fragment就会自动调用这个方法。
  * @param hidden
  */
@Override
public void onHiddenChanged(boolean hidden) {
    logD("onHiddenChanged: " + hidden);
    super.onHiddenChanged(hidden);
    if (hidden) {
        dispatchUserVisibleHint(false);
    } else {
        dispatchUserVisibleHint(true);
    }
}

关于这个FragmentTransaction中hide、show、add和replace大家可以看一下Fragment使用hide和show、使用onHiddenChanged执行代替声明周期这一篇文章。最后我把整个的懒加载的代码贴一下,这样大家看着也方便,更能从中分析逻辑。

public abstract class LazyFragment3 extends Fragment {

    private static final String TAG = "LazyFragment3";

    // Fragment生命周期,
    // onAttach -> onCreate -> onCreateView -> onActivityCreated -> onStart ->
    // onResume -> onPause -> onStop -> onDestroyView -> onDestroy -> onDetach
    // 对于ViewPager + Fragment我们需要关注的生命周期有
    // onCreateView -> onActivityCreated —> onResume -> onPause -> onDestroyView

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

    // 为了获得Fragment 不可见的状态,和再次回到可见状态的判断,我们还需要增加一个 currentVisibleState 标志位,
    // 该标志位在 onResume和 onPause中结合getUserVisibleHint函数的返回值来决定是否应该回调可见与不可见状态函数
    boolean currentVisibleState = false;
    FragmentDelegater mFragmentDelegater;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

        if (rootView == null) {
            rootView = inflater.inflate(getLayoutRes(), container, false);
        }
        // initView 用于添加默认的界面
        initView(rootView);
        // 将View创建完成标志位设置为true
        isViewCreated = true;
        logD("onCreateView: ");
        if (!isHidden() && getUserVisibleHint()){
            dispatchUserVisibleHint(true);
        }
        return rootView;
    }

    protected abstract int getLayoutRes();

    protected abstract void initView(View view);

    /**
     * 修改Fragment的可见性
     * setUserVisibleHint被调用有两种情况
     * 1) 在tab切换的时候,会先于Fragment的所有其他生命周期调用
     * 对于默认tab和间隔 checked tab 需要等到 isViewCreated = true 之后才可以通过此方法通知用户是否可见
     * 2) 对于之前已经调用过setUserVisibleHint 方法的Fragment,可以当做Fragment从可见到不可见状态变化的依据
     *
     * @param isVisibleToUser
     */
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        logD("setUserVisibleHint: " + isVisibleToUser);
        // 对于情况1,不用做任何处理,用isViewCreated进行判断,如果isViewCreated为false,说明它没有被创建
        if (isViewCreated) {
            // 对于情况2,又要分两种情况讨论 2.1) Fragment从不可见 -> 可见;2.2) Fragment从可见 -> 不可见
            // 对于2.1)我们需要怎么判断呢?首先是可见的(isVisibleToUser一定为true)
            //      而且只有当可见状态发生改变的时候才需要切换(此时就添加了currentVisibleState来辅助判断),否则容易出现反复调用的情况
            //      从而导致事件分发带来的多次更新
            // 对于2.2)如果是可见->不可见,判断条件恰好和 2.1)相反
            if (isVisibleToUser && !currentVisibleState) {  // 从不可见 -> 可见状态
                dispatchUserVisibleHint(true);
            } else if (!isVisibleToUser && currentVisibleState) {
                dispatchUserVisibleHint(false);
            }
        }

    }

    /**
     * 统一处理用户可见信息分发
     * 分第一次可见,可见,不可见分发
     *
     * @param isVisible
     */
    private void dispatchUserVisibleHint(boolean isVisible) {
        logD( "dispatchUserVisibleHint: " + isVisible);

        // 事实上作为父Fragment的BottomTabFragment2并没有分发可见事件,
        // 它通过getUserVisibleHint得到的是false
        // 因此我们需要在负责分发可见事件的方法中添加一个当前父Fragment是否可见的判断
        // 如果当前父Fragment不可见我们就不分发可见事件
        if (isVisible && isParentInVisible()) {
            return;
        }

        if (currentVisibleState == isVisible) {
            return;
        }

        currentVisibleState = isVisible;

        if (isVisible) {
            // 分发可见状态,可见状态又可以分为第一次可见和后面可见,
            // 区别就是第一次可见需要从网络加载数据,后面可见可以直接从数据库中获取数据
            if (mIsFirstVisible) {
                mIsFirstVisible = false;
                onFragmentFirstVisible();
            }
            onFragmentResume();
            // 在双层Fragment嵌套的情况下,第一次滑动Fragment嵌套ViewPager(Fragment)的时候
            // 此时只会加载外层Fragment的数据,而不会加载Fragment内嵌套的ViewPager中的Fragment的数据,
            // 因此我们需要在此增加一个当外层Fragment可见的时候,分发可见事件给自己内嵌的所有Fragment显示
            dispatchChildVisibleState(true);
        } else {
            // 分发不可见状态
            onFragmentPause();
            dispatchChildVisibleState(false);
        }

    }

    private boolean isParentInVisible() {
        Fragment parentFragment = getParentFragment();
        if (parentFragment instanceof LazyFragment3) {
            LazyFragment3 fragment = (LazyFragment3) parentFragment;
            return !fragment.isSupportVisible();
        }
        return false;
    }

    private boolean isSupportVisible() {
        return currentVisibleState;
    }

    private void dispatchChildVisibleState(boolean visible) {
        FragmentManager manager = getChildFragmentManager();
        List<Fragment> fragments = manager.getFragments();
        for (Fragment fragment : fragments) {
            if (fragment instanceof LazyFragment3
                    && !fragment.isHidden()
                    && fragment.getUserVisibleHint()) {
                ((LazyFragment3) fragment).dispatchUserVisibleHint(visible);
            }
        }
    }

    /**
     *
     * 用FragmentTransaction来控制fragment的hide和show时,
     * 那么这个方法就会被调用。每当你对某个Fragment使用hide
     * 或者是show的时候,那么这个Fragment就会自动调用这个方法。
     * https://blog.csdn.net/u013278099/article/details/72869175
     * @param hidden
     */
    @Override
    public void onHiddenChanged(boolean hidden) {
        logD("onHiddenChanged: " + hidden);
        super.onHiddenChanged(hidden);
        if (hidden) {
            dispatchUserVisibleHint(false);
        } else {
            dispatchUserVisibleHint(true);
        }
    }

    protected abstract void onFragmentFirstVisible();

    /**
     * 用于通知用户,可见状态调用onFragmentResume 加载数据
     */
    protected void onFragmentResume() {
        logD("onFragmentResume " + " 真正的resume,开始相关操作耗时");
    }

    /**
     * 用于通知用户,不可见状态调用onFragmentPause 加载数据
     */
    protected void onFragmentPause() {
        logD("onFragmentPause" + " 真正的Pause,结束相关操作耗时");
    }

    public void setFragmentDelegater(FragmentDelegater fragmentDelegater) {
        mFragmentDelegater = fragmentDelegater;
    }

    @Override
    public void onResume() {
        super.onResume();
        logD( "onResume: ");
        // 在滑动或者跳转的过程中,第一次创建Fragment的时候都会调用onResume方法,类似于从tab1切换到tab2的时候,
        // 此时tab3会缓存这个时候tab3的onResume方法也会得到执行,很显然此时tab3是不可见的,所以此时tab3是不需要去
        // 调用dispatchUserVisibleHint(true)方法
        if (!mIsFirstVisible) {
            // 由于在Activity1中如果有多个Fragment,然后从Activity1跳转到Activity2,此时会有多个Fragment会在Activity1中缓存,
            // 此时,如果再从Activity2跳回到Activity1,此时缓存在Activity1中的所有Fragment的onResume方法都会得到执行。
            // 很显然,此时我们也不需要对所有缓存的Fragment调用dispatchUserVisibleHint(true)方法。因此出现了下面的判断
            if (!currentVisibleState && !isHidden() && getUserVisibleHint()) {
                dispatchUserVisibleHint(true);
            }
        }
    }

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

    @Override
    public void onStop() {
        super.onStop();
        logD("onStop");
    }

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

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

    @Override
    public void onDetach() {
        super.onDetach();
    }

    private void logD(String infor) {
        Log.i("TODAY", "name: " + this.getClass().getSimpleName() + " -> " + infor);
    }

}

至此,ViewPager+Fragment懒加载相关的知识,就分享完了。可以说这一块的内容逻辑还是挺多的,还包含一些比较深的坑。但是仔细分析,逻辑并不难,这一篇文章能写出来,其中的代码我也是多亏了自己报的课程老师的视频讲解。因此,我还是很感谢享学课堂的。最后,希望与大家共勉。

这段时间看了太多的震撼人心的视频和新闻,在这次新冠病毒疫情防护工作中,祖国展示出的强大的执行力,坚定的决心,足以让每个中国人骄傲,也足以让所有中国人放心。14亿中国人表现出的团结,那些身穿白大褂的医护工作者,身穿警服或军装头顶国徽的人民子弟兵表现出的无私的精神,足以让所有的中国人为之动容。这是中国人骨子里的那种优秀品质,这是华夏民族五千年未曾丢失的优良传统。

愿,新冠病毒早日被彻底控制住,向我们的白衣天使,平民英雄致敬。你们是祖国的骄傲,是国人心中的定心丸。

猜你喜欢

转载自blog.csdn.net/zhourui_1021/article/details/105037156