Android 视图

Fragment懒加载

2022-01-21  本文已影响0人  EnzoRay

转载自:https://www.jianshu.com/p/2e927f687506

1.Fragment懒加载

所谓懒加载,指的就是延迟加载,在需要的时候再加载数据。相信我们在开发中都实现过底部和顶部的标签导航功能,点击相应的标签可以切换到相应的Fragment,由于同一时间只有一个Fragment显示在屏幕中,因此没有显示出来的Fragment就没有必要在此时加载数据,特别是从网络获取数据这种比较耗时的操作,会产生不太好的用户体验,理想的情况是在Fragment可见的时候才加载数据,这就是为什么Fragment需要懒加载的原因。实现Fragment的切换有两种方式,使用FragmentManager或ViewPager,我们来看看这两个常见场景:

场景一 使用FragmentManager实现底部导航栏页面切换
这种切换方式的缺点是不能左右滑动切换页面。系统提供了三个API来获得FragmentManager,但是它们的使用场景是不一样的:
getSupportFragmentManager
getSupportFragmentManager()来自FragmentActivity中,FragmentActivity是v4包中的,继承自Activity,用于兼容低版本没有Fragment的API问题,AppCompatActivity就是继承了FragmentActivity,因此如果我们的Activity是继承自AppCompatActivity,可以直接使用getSupportFragmentManager()方法来获得FragmentManager。
getFragmentManager
该方法位于Activity类中,是app包中的,相对应的Fragment也要是app包中的Fragment。
getChildFragmentManager
getChildFragmentManager()在Fragment中,用于管理当前Fragment中添加的子Fragment,就是Fragment中嵌套Fragment的情况。

实现底部导航栏的方式有很多:包括RadioButton、TabHost甚至是LinearLayout都可以,这里使用了官方design库提供的BottomNavigationView。
Tab标签多于3个时标签切换默认会有动画效果:图片和文字在点击的时候会放大。如果不需要可以取消,如何取消图片放大呢,针对design库的版本有不同的解决方法:
com.android.support:design:28.0.0以下:
通过反射调用setShiftingMode(false)方法,完整代码如下:

public void disableShiftMode(BottomNavigationView view) {
    BottomNavigationMenuView menuView = (BottomNavigationMenuView) view.getChildAt(0);
    try {
        Field shiftingMode = menuView.getClass().getDeclaredField("mShiftingMode");
        shiftingMode.setAccessible(true);
        shiftingMode.setBoolean(menuView, false);
        shiftingMode.setAccessible(false);
        for (int i = 0; i < menuView.getChildCount(); i++) {
            BottomNavigationItemView item = (BottomNavigationItemView) menuView.getChildAt(i);
            //noinspection RestrictedApi
            item.setShiftingMode(false);
            // set once again checked value, so view will be updated
            //noinspection RestrictedApi
            item.setChecked(item.getItemData().isChecked());
        }
    } catch (NoSuchFieldException e) {
        Log.e("BNVHelper", "Unable to get shift mode field", e);
    } catch (IllegalAccessException e) {
        Log.e("BNVHelper", "Unable to change value of shift mode", e);
    }
}

使用时直接调用该方法,传入BottomNavigationView即可:

// BottomNavigationView禁止3个item以上动画切换效果
BottomNavigationViewHelper.disableShiftMode(mBottomNavigationView);

com.android.support:design:28.0.0:
无法调用setShiftingMode()方法,官方提供了解决方法,只需要在xml布局文件的BottomNavigationView下添加app:labelVisibilityMode="labeled"属性即可。

<android.support.design.widget.BottomNavigationView
    android:id="@+id/bnv_bar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:itemIconTint="@drawable/nav_item_color_state"
    app:itemTextColor="@drawable/nav_item_color_state"
    app:labelVisibilityMode="labeled"
    app:menu="@menu/menu_bottom_navigation" />

如何取消文字变大?通过查看BottomNavigationItemView的源码我们可以发现选中和未选中字体的大小是由两个属性决定的。

public BottomNavigationItemView(Context context, AttributeSet attrs, int defStyleAttr) {
    ...
    int inactiveLabelSize =
            res.getDimensionPixelSize(android.support.design.R.dimen.design_bottom_navigation_text_size);
    int activeLabelSize = res.getDimensionPixelSize(
            android.support.design.R.dimen.design_bottom_navigation_active_text_size);
    ...
}

只要在自己项目中的values文件夹下新建dimens.xml文件,声明同名的属性,覆盖BottomNavigationView的默认属性值即可。

<!-- BottomNavigationView选中和未选中文字大小 -->
<dimen name="design_bottom_navigation_active_text_size">14sp</dimen>
<dimen name="design_bottom_navigation_text_size">14sp</dimen>

下面回到正题,我给BottomNavigationView添加了三个标签,分别对应三个Fragment,每个Fragment的代码结构基本一致,只是加载的数据不一样,在Fragment的生命周期回调方法中打印日志,完整代码如下:

import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.example.viewpagerfragment.R;
import com.example.viewpagerfragment.adapter.ListAdapter;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;

public class HomeFragment extends Fragment {

    private RecyclerView mRecyclerView;
    private ListAdapter mAdapter;
    private List<String> mData;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Log.e("TAG", "HomeFragment onAttach()");
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.e("TAG", "HomeFragment onCreate()");
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Log.e("TAG", "HomeFragment onCreateView()");
        View view = inflater.inflate(R.layout.fragment_home, container, false);
        initView(view);
        initData();
        initEvent();
        return view;
    }

    /**
     * 初始化视图
     *
     * @param view
     */
    private void initView(View view) {
        mRecyclerView = view.findViewById(R.id.rv_home);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        mRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL));
    }

    /**
     * 初始化数据
     */
    private void initData() {
        mData = new ArrayList<>();
        // 模拟数据的延迟加载
        Observable.timer(3, TimeUnit.SECONDS)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Long>() {
                    @Override
                    public void accept(Long aLong) throws Exception {
                        for (int i = 0; i < 20; i++) {
                            mData.add("首页文章" + (i + 1));
                        }
                        mAdapter = new ListAdapter(getActivity(), mData);
                        mRecyclerView.setAdapter(mAdapter);
                    }
                });
    }

    /**
     * 初始化事件
     */
    private void initEvent() {

    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.e("TAG", "HomeFragment onActivityCreated()");
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.e("TAG", "HomeFragment onStart()");
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.e("TAG", "HomeFragment onResume()");
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.e("TAG", "HomeFragment onPause()");
    }

    @Override
    public void onStop() {
        super.onStop();
        Log.e("TAG", "HomeFragment onStop()");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.e("TAG", "HomeFragment onDestroyView()");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.e("TAG", "HomeFragment onDestroy()");
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.e("TAG", "HomeFragment onDetach()");
    }
}

使用FragmentManager管理Fragment,调用hide()和show()方法来切换页面的显示。

/**
 * 显示当前Fragment
 *
 * @param index
 */
private void showFragment(int index) {
    FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
    hideFragment(ft);
    switch (index) {
        case FRAGMENT_HOME:
            /**
             * 如果Fragment为空,就新建一个实例
             * 如果不为空,就将它从栈中显示出来
             */
            if (homefragment == null) {
                homefragment = new HomeFragment();
                ft.add(R.id.fl_container, homefragment, HomeFragment.class.getName());
            } else {
                ft.show(homefragment);
            }
            break;
        case FRAGMENT_KNOWLEDGESYSTEM:
            if (knowledgeSystemFragment == null) {
                knowledgeSystemFragment = new KnowledgeSystemFragment();
                ft.add(R.id.fl_container, knowledgeSystemFragment, KnowledgeSystemFragment.class.getName());
            } else {
                ft.show(knowledgeSystemFragment);
            }
            break;
        case FRAGMENT_PROJECT:
            if (projectFragment == null) {
                projectFragment = new ProjectFragment();
                ft.add(R.id.fl_container, projectFragment, ProjectFragment.class.getName());
            } else {
                ft.show(projectFragment);
            }
            break;
        default:
            break;
    }
    ft.commit();
}

/**
 * 隐藏全部Fragment
 *
 * @param ft
 */
private void hideFragment(FragmentTransaction ft) {
    // 如果不为空,就先隐藏起来
    if (homefragment != null) {
        ft.hide(homefragment);
    }
    if (knowledgeSystemFragment != null) {
        ft.hide(knowledgeSystemFragment);
    }
    if (projectFragment != null) {
        ft.hide(projectFragment);
    }
}

通过Log打印我们看到:点击相应的标签,就会回调相应的Fragment的生命周期方法,另外两个Fragment并没有被销毁。之后在几个Fragment之间切换也不会回调任何的生命周期方法。因此可以得出结论,当我们是通过调用hide()和show()方法来实现Fragment的切换时,不需要做额外的操作即可实现懒加载。

如果是调用replace()来实现Fragment的切换呢?会不会销毁之前的Fragment呢?下面来看一下这种情况:

/**
 * 显示当前Fragment
 *
 * @param index
 */
private void showFragment(int index) {
    FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
    switch (index) {
        case FRAGMENT_HOME:
            if (homefragment == null) {
                homefragment = new HomeFragment();
                ft.add(R.id.fl_container, homefragment, HomeFragment.class.getName());
            }
            ft.replace(R.id.fl_container, homefragment);
            break;
        case FRAGMENT_KNOWLEDGESYSTEM:
            if (knowledgeSystemFragment == null) {
                knowledgeSystemFragment = new KnowledgeSystemFragment();
                ft.add(R.id.fl_container, knowledgeSystemFragment, KnowledgeSystemFragment.class.getName());
            }
            ft.replace(R.id.fl_container, knowledgeSystemFragment);
            break;
        case FRAGMENT_PROJECT:
            if (projectFragment == null) {
                projectFragment = new ProjectFragment();
                ft.add(R.id.fl_container, projectFragment, ProjectFragment.class.getName());
            }
            ft.replace(R.id.fl_container, projectFragment);
            break;
        default:
            break;
    }
    ft.commit();
}

可以发现在创建第二个Fragment的生命周期方法同时会完全销毁第一个Fragment,创建第三个Fragment的同时会完全销毁了第二个Fragment。
总结一下:hide()+show()和replace(),前者在Fragment切换时不会销毁之前的Fragment,后者会销毁,推荐使用第一种方式实现懒加载,当然还是要结合实际情况看哪种方式更适合。

场景二 使用ViewPager实现顶部标签栏页面切换
这种场景实现顶部标签栏的方式同样有很多,github上很多优秀的第三方库,可以实现各种酷炫的效果,这里使用官方design库中提供的TabLayout,配合ViewPager可以实现页面的滑动切换。

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;

import com.example.viewpagerfragment.R;
import com.example.viewpagerfragment.adapter.MyPagerAdapter;

import java.util.ArrayList;
import java.util.List;

public class TabActivity extends AppCompatActivity {

    private TabLayout mTabLayout;
    private ViewPager mViewPager;

    private HomeFragment homefragment;
    private KnowledgeSystemFragment knowledgeSystemFragment;
    private ProjectFragment projectFragment;
    private List<Fragment> mFragments;
    private MyPagerAdapter mAdapter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tab);
        initView();
        initData();
        initEvent();
    }

    /**
     * 初始化视图
     */
    private void initView() {
        mTabLayout = findViewById(R.id.tl_tabs);
        mViewPager = findViewById(R.id.vp_tabs);
    }

    /**
     * 初始化数据
     */
    private void initData() {
        mFragments = new ArrayList<>();
        homefragment = new HomeFragment();
        knowledgeSystemFragment = new KnowledgeSystemFragment();
        projectFragment = new ProjectFragment();
        mFragments.add(homefragment);
        mFragments.add(knowledgeSystemFragment);
        mFragments.add(projectFragment);

        mAdapter = new MyPagerAdapter(getSupportFragmentManager(), mFragments);
        mViewPager.setAdapter(mAdapter);
        // 关联ViewPager
        mTabLayout.setupWithViewPager(mViewPager);
        // mTabLayout.setupWithViewPager方法内部会remove所有的tabs,这里重新设置一遍tabs的text,否则tabs的text不显示
        mTabLayout.getTabAt(0).setText("首页");
        mTabLayout.getTabAt(1).setText("知识体系");
        mTabLayout.getTabAt(2).setText("项目");
    }

    /**
     * 初始化事件
     */
    private void initEvent() {

    }
}

此时我们通过Log可以看出,创建第一个Fragment的同时,第二个Fragment也创建并加载了。这里的MyPagerAdapter可以继承FragmentPagerAdapter,也可以继承FragmentStatePagerAdapter,那么两者有什么区别呢,当我们切换到第三个Fragment,观察第一个Fragment的生命周期,就会发现区别:如果ViewPager的adapter继承自FragmentPagerAdapter,那么只会销毁Fragment的视图,不会销毁Fragment对象;如果ViewPager的adapter继承自FragmentStatePagerAdapter,那么不仅会销毁Fragment的视图,也会销毁Fragment对象。

2.ViewPager的预加载机制

通过之前的例子我们已经知道ViewPager会提前加载下一个位置的Fragment,其实预加载的Fragment个数是可以设置的,setOffscreenPageLimit(int limit)的作用就是设置ViewPager的预加载页面数量,同时也决定了ViewPager能够缓存的页面数量。举个例子,如果我们调用mViewPager.setOffscreenPageLimit(3),那么ViewPager会提前加载当前页面两边相邻的3个Fragment,此时VIewPager可缓存的Fragment数量为2*3+1=7,与当前Fragment间距超过3的Fragment就会被销毁回收(是否会销毁Fragment实例对象由我们继承的Adapter决定)。当我们setOffscreenPageLimit(3),显示第一个Fragment时,就已经创建并加载了所有的Fragment,我们会发现ViewPager的预加载机制其实和我们想要实现的懒加载是背道而驰的,那么我们可以通过setOffscreenPageLimit(0)取消预加载吗?答案是不能。我们来看一下setOffscreenPageLimit()方法内部就明白了:

private static final int DEFAULT_OFFSCREEN_PAGES = 1;

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();
    }
}

我们可以很清楚地看出,如果我们传入了小于1的值,最后都会取默认值1,因此这种方法是无法取消预加载的。

3.如何实现ViewPager的Fragment懒加载

既然我们无法取消ViewPager的预加载,那就只能从Fragment的角度来实现懒加载了。基本思路是判断Fragment是否可见,当可见时才加载数据,这就涉及到了Fragment的两个方法:setUserVisibleHint(boolean isVisibleToUser)和onHiddenChanged(boolean hidden),下面我们就来具体看一下这两个方法。
setUserVisibleHint
每个Fragment的setUserVisibleHint()方法都会至少执行两次,一次是在Fragment的生命周期方法执行之前,此时isVisibleToUser的值为false;一次是在Fragment变为可见时,此时isVisibleToUser的值为true。
这里还需要提一下getUserVisibleHint()方法,也有人是利用该方法来判断Fragment是否可见的,那么该方法的返回值代表什么呢,通过查看源码,我们可以发现,其实getUserVisibleHint()的返回值就是setUserVisibleHint()方法的isVisibleToUser参数,因此,这种判断方式本质上和利用isVisibleToUser来判断是一样的。

public void setUserVisibleHint(boolean isVisibleToUser) {
    if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
            && mFragmentManager != null && isAdded()) {
        mFragmentManager.performPendingDeferredStart(this);
    }
    // 这里对mUserVisibleHint赋值
    mUserVisibleHint = isVisibleToUser;
    mDeferStart = mState < STARTED && !isVisibleToUser;
}

public boolean getUserVisibleHint() {
    return mUserVisibleHint;
}

onHiddenChanged
onHiddenChanged()方法只有在利用FragmentManager管理Fragment,并且使用hide()和show()方法切换Fragment时才会被调用,该方法同样有一个参数hidden,表示Fragment是否隐藏。当显示第一个Fragment时,可以发现并没有执行第一个Fragment的onHiddenChanged()方法,这是由于我在代码添加了判断,如果Fragment实例对象为空,就调用add()方法先将Fragment添加到FragmentTransaction中,并没有调用show()方法。onHiddenChanged()方法只有在Fragment的隐藏或显示状态发生了改变时才会调用。不同于setUserVisibleHint()方法,调用onHiddenChanged()时Fragment已经完成了创建相关生命周期(onAttach()~onResume())的回调。
既然已经清楚了这两个方法的调用时机和作用,那么我们就可以来实现懒加载了,首先确定实现思路:

import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public abstract class LazyFragment extends Fragment {

    private Context mContext;
    private boolean hasViewCreated; // 视图是否已加载
    private boolean isFirstLoad; // 是否首次加载

    private ProgressDialog mProgressDialog; // 加载进度对话框

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mContext = getActivity();
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        hasViewCreated = true;
        isFirstLoad = true;
        View view = LayoutInflater.from(mContext).inflate(getContentViewId(), null);
        initView(view);
        initData();
        initEvent();
        lazyLoad();
        return view;
    }

    /**
     * 设置布局资源id
     *
     * @return
     */
    protected abstract int getContentViewId();

    /**
     * 初始化视图
     *
     * @param view
     */
    protected void initView(View view) {

    }

    /**
     * 初始化数据
     */
    protected void initData() {

    }

    /**
     * 初始化事件
     */
    protected void initEvent() {

    }

    /**
     * 懒加载
     */
    protected void onLazyLoad(){

    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isVisibleToUser) {
            lazyLoad();
        }
    }

    private void lazyLoad() {
        if (!hasViewCreated || !isFirstLoad || !getUserVisibleHint()) {
            return;
        }
        isFirstLoad = false;
        onLazyLoad();
    }
}

这样封装其实也有一个问题,就是当同一个Fragment同时需要用于FragmentManager场景和ViewPager场景中时,如果将加载数据逻辑放到onLazyLoad()中,那么在使用FragmentManager管理Fragment时不会调用setUsersetUserVisibleHint()方法,也就无法加载数据了;如果把加载数据逻辑放到initData()中,那么就失去了懒加载的作用。我有看到过一种解决方法是重写onHiddenChanged()方法,根据相同的判断条件,执行加载数据逻辑,但是这样有一个问题是在每一个Fragment第一次调用add()方法被添加后,需要手动调用hide()和show()方法来触发onHiddenChanged()方法,个人觉得还是有些奇怪,这里就不展示了。考虑到这种情况也不是很常见,如果真的遇到了,还是写两个Fragment吧。

总结与后记

本文主要介绍了Fragment的懒加载实现以及ViewPager的预加载机制。实现Fragment的切换有两种方式:FragmentManager和ViewPager,其中前者不会提前加载Fragment,因此不需要实现懒加载;后者由于自身的预加载机制,需要考虑懒加载来使得页面的加载更加流畅。我们要清楚懒加载的实现并不是因为Fragment被延迟加载了,Fragment仍然会被预加载,只是当Fragment可见时才加载数据而已。看到网上有些文章直接在onResume()方法中判断一次有没有加载过数据就认为已经实现了懒加载,这样是不对的。注意懒加载是页面可见的时候才去加载数据,在ViewPager的预加载机制中会提前运行预加载的Fragment的onResume()方法,如果用这种方法就与懒加载的概念背道而驰了。
Demo地址

上一篇下一篇

猜你喜欢

热点阅读