前言
在Android开发中,利用ViewPager+Fragment实现页签的切换几乎是每个app的必备功能,虽然实现起来并不难,但是还是有一些需要我们注意的地方。我自己此前也了解过一些关于Fragment懒加载的实现,但是基本停留在拿来直接用的程度,很多地方还有一些疑惑,本文主要介绍一下使用ViewPager和Fragment的一些技巧和可能会踩到的坑,也算是对这块知识的梳理。
1.Fragment的懒加载
所谓懒加载,指的就是延迟加载,在需要的时候再加载数据,这个概念其实在Web开发中很常见,那么在Android开发中,为什么Fragment要实现懒加载?什么场景下需要实现懒加载呢?
相信我们在开发中都实现过底部和顶部的标签导航功能,点击相应的标签可以切换到相应的页面,当然使用的就是Fragment,由于同一时间只有一个页面(Fragment)能显示在屏幕中,因此没有显示出来的Fragment就没有必要在此时加载数据,特别是从网络获取数据这种比较耗时的操作,会产生不太好的用户体验,理想的情况是在Fragment可见的时候才加载数据,这就是为什么Fragment需要懒加载的原因。在实际开发中,实现Fragment的切换有两种方式,使用FragmentManager或ViewPager,那么这两种情况下Fragment是何时加载的呢,我们来看两个常见场景:
场景一 使用FragmentManager实现底部导航栏页面切换
首先简单介绍一下FragmentManager,用于管理Fragment,能够实现Fragment的添加、移除、显示和隐藏,虽然我们可能已经用过很多次了,但还是有一些需要注意的地方。系统提供了三个API来获得FragmentManager,但是它们的使用场景是不一样的:
- getSupportFragmentManager
getSupportFragmentManager()用于Activity中,用于管理Activity中添加的Fragment,该方法只有在FragmentActivity中才有,FragmentActivity是v4包中的,继承自Activity,用于兼容低版本没有Fragment的API问题,AppCompatActivity就是继承了FragmentActivity,因此如果我们的Activity是继承自AppCompatActivity,可以直接使用getSupportFragmentManager()方法来获得FragmentManager。
- getFragmentManager
该方法既可用于Activity中,也可以用于Fragment中。该方法位于Activity类中,如果用于Activity中,与getSupportFragmentManager()方法作用相同,用于获取Activity中的FragmentManager,需要注意的是该方法是app包中的,因此如果我们使用的Fragment是v4包中的,那么应该让Activity继承自FragmentActivity,使用getSupportFragmentManager()。Fragment中也有该方法,返回的是管理当前Fragment自身的那个FragmentManager,也就是将当前Fragment添加进来的FragmentManager,有可能是Activity中的FragmentManager,也有可能是Fragment中的FragmentManager。
- getChildFragmentManager
getChildFragmentManager()用于Fragment中,用于管理当前Fragment中添加的子Fragment,换句话说就是Fragment中嵌套Fragment的情况。
总结一下,在Activity中管理Fragment,如果Fragment是位于v4包中的,使用getSupportFragmentManager();如果Fragment是位于app包中的,使用getFragmentManager()。如果要在Fragment中嵌套子Fragment,使用getChildFragmentManager()。
实现底部导航栏的方式有很多:包括RadioButton、TabHost甚至是LinearLayout都可以,这里使用了官方design库提供的BottomNavigationView,使用方式不是本文的重点,也比较简单,这里就不提了,可以自行百度或是参考我的Demo。在使用时有几个需要注意的问题:
-
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" />
-
Tab切换时文字大小会变化
其实这个效果是否需要保留因人而异,通过查看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);
}
}
在Fragment的几个生命周期回调方法中打印日志,下面我们就来看一下整个加载过程。
-
初始状态显示第一个Fragment
可以看出,此时依次回调了第一个Fragment的生命周期方法,并没有加载其他的两个Fragment。
-
切换到第二个Fragment
此时依次回调了第二个Fragment的生命周期方法,并没有加载第三个Fragment,第一个Fragment也没有被销毁。
-
切换到第三个Fragment
此时依次回调了第三个Fragment的生命周期方法,前两个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
与调用hide()和show()的情况没有区别,只是执行了第一个Fragment的生命周期方法。
-
切换到第二个Fragment
这种情况下就有区别了,可以发现在调用第二个Fragment的生命周期方法同时销毁了第一个Fragment。
-
切换到第三个Fragment
同上分析,创建第三个Fragment的同时回调了第二个Fragment销毁相关的生命周期方法。
之后切换回前几个Fragment,我们应该能够想到会发生什么情况,由于之前的Fragment已经被销毁,因此会重新创建Fragment,加载数据,同时销毁切换前的Fragment对象。
因此,当调用replace()实现Fragment的动态显示时,会销毁不可见的Fragment,重新创建当前Fragment,虽然Fragment也是在可见时加载数据的,但是会导致数据的多次加载,浪费资源,因此相比于hide()和show()方法,并不推荐这种方法切换Fragment。
最后总结一下,当我们使用FragmentManager管理多个Fragment,实现Fragment之间的切换时,有两种方法:hide()
+show()
或者replace()
,两种方法的共同点是只有在Fragment显示时才创建Fragment对象,加载页面数据,也就是实现了懒加载,区别是前者在Fragment切换时不会销毁之前的Fragment对象,后者会销毁,推荐使用第一种方式,当然还是要看实际情况哪种方式更适合。
场景二 使用ViewPager实现顶部标签栏页面切换
实现顶部标签栏的方式同样有很多,github上很多优秀的第三方库,可以实现各种酷炫的效果,这里为了简单,依然是使用官方design库中提供的TabLayout,使用方式比较简单,就不展示了,配合ViewPager可以实现页面的滑动切换。
Fragment依然使用之前的那三个,完整代码如下:
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() {
}
}
这里提几点在使用TabLayout和ViewPager时需要注意的地方,调用setupWithViewPager()
关联ViewPager后,TabLayout会remove掉所有的tab,运行后会发现无法显示标签文字。解决方法有两种:第一种是重写ViewPager的adapter的getPageTitle()
方法,设置每个Tab的标题。
@Override
public CharSequence getPageTitle(int position) {
String title;
switch (position) {
case 0:
title = "首页";
break;
case 1:
title = "知识体系";
break;
case 2:
title = "项目";
break;
default:
title = "";
break;
}
return title;
}
第二种是在setupWithViewPager()后重新设置Tab标题,这种方式更适合与Tab个数和标题未知的情况。
mTabLayout.getTabAt(0).setText("首页");
mTabLayout.getTabAt(1).setText("知识体系");
mTabLayout.getTabAt(2).setText("项目");
ViewPager的Adapter有两种:FragmentPagerAdapter和FragmentStatePagerAdapter,这两种的区别是什么呢,我们分别继承一下这两种Adapter,看一下效果。继承只需要实现几个方法就可以了,方法名一看就知道是什么意思,这里就不展示了。
- 继承FragmentPagerAdapter
1.初始状态显示第一个Fragment
可以看出,此时不仅加载了第一个Fragment,第二个Fragment也创建并加载了。
2.切换到第二个Fragment
此时,第三个Fragment被创建并加载。
3.切换到第三个Fragment
此时,第一个Fragment依次执行onPause()、onStop()和onDestoryView()方法,注意只是销毁了视图,并没有执行onDestory()方法,销毁Fragment对象。
当我们重新切换到第二个Fragment时,第一个Fragment依次执行onCreateView()、onActivityCreate()、onStart()和onResume()方法,重新创建视图。
之后我们再切换回第一个Fragment,可以发现第三个Fragment的视图被销毁。
- 继承FragmentStatePagerAdapter
下面我们再来看一下继承FragmentStatePagerAdapter的情况。
1.初始状态显示第一个Fragment
和继承FragmentPagerAdapter的情况没有区别,同样是创建加载出了前两个Fragment。
2切换到第二个Fragment
同样是提前创建出了第三个Fragment。
3.切换到第三个Fragment
这里就有区别了,同样是要销毁第一个Fragment,继承FragmentPagerAdapter时只是销毁了视图,并没有执行onDestory()方法;而继承FragmentStatePagerAdapter不仅会销毁视图,还销毁了Fragment对象,执行了onDestory()和onDetach()方法。
之后切换回第二个Fragment,会重新创建第一个Fragment对象,执行onAttach()和onCreate()方法。
再切换回第一个Fragment,第三个Fragment被销毁。
上面展示了不同情况下打印出来的生命周期执行日志,可能不是很清楚,这里我就总结一下,使用ViewPager切换Fragment时,默认会提前加载出下一个位置Fragment,与当前位置间隔超过1的Fragment会被销毁,这里又分为了两种情况:如果ViewPager的adapter继承自FragmentPagerAdapter,那么只会销毁Fragment的视图,不会销毁Fragment对象;如果ViewPager的adapter继承自FragmentStatePagerAdapter,那么不仅会销毁Fragment的视图,而且也会销毁Fragment对象(这好像是废话,对象都销毁了哪里来的视图)。
由于FragmentStatePagerAdapter会完全销毁Fragment对象,因此更适用于Fragment比较多的情况,保证Fragment的回收,节省内存;FragmentPagerAdapter更适合Fragment数量较少的情况,不会频繁地创建和销毁Fragment对象。
有人可能要问了,有没有什么方法可以防止Fragment的销毁呢?当然有了,这里先介绍一种方式,后面介绍ViewPager的预加载时会再介绍一种方法。通过查看FragmentStatePagerAdapter的源码会发现,Fragment的销毁是在destroyItem()
方法中声明的(FragmentPagerAdapter也是这样),如果我们不需要销毁Fragment,只需要复写该方法即可,记住不要使用super调用父类的实现。
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
// super.destroyItem(container, position, object);
}
2.ViewPager的预加载机制
通过之前的例子我们已经知道ViewPager会提前加载下一个位置的Fragment,这就叫做VIewPager的预加载机制,作用是为了让ViewPager的切换更加流畅。提到ViewPager的预加载机制,我们就不得不提到一个方法setOffscreenPageLimit(int limit)
。该方法的作用就是设置ViewPager的预加载页面数量,同时也决定了ViewPager能够缓存的页面数量。举个例子,如果我们调用mViewPager.setOffscreenPageLimit(3)
,那么ViewPager会提前加载当前页面两边相邻的3个Fragment,此时VIewPager可缓存的Fragment数量为2*3+1=7
,与当前Fragment间距超过3的Fragment就会被销毁回收(是否会销毁Fragment实例对象由我们继承的Adapter决定)。limit的默认值是1,这就解释了为什么ViewPager会提前加载下一个位置的Fragment,并且显示第三个Fragment时会销毁第一个Fragment。
这里依然采用之前顶部标签栏的例子,添加一行代码,设置ViewPager的预加载数量,重新来看一下Fragment的创建和加载过程。
mViewPager.setOffscreenPageLimit(mFragments.size());
可以看出当初始状态显示第一个Fragment时,就已经创建并加载了所有的Fragment,并且当我们在几个Tab之间切换时,也不会销毁并重新创建Fragment。
这就是我在上文中提到的如何防止Fragment被销毁的第二种方法,就是通过setOffscreenPageLimit(),设置预加载数量为Tab总数,使得所有Fragment都能被缓存。
ViewPager的预加载机制其实和我们想要实现的懒加载是背道而驰的,那么我们可以取消预加载吗?答案是不能,或许有的人想到了设置预加载数量为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.如何实现Fragment的懒加载
既然我们无法取消ViewPager的预加载,那就只能从Fragment的角度来实现懒加载了。基本思路是判断Fragment是否可见,当可见时才加载数据,这就涉及到了Fragment的两个方法:setUserVisibleHint(boolean isVisibleToUser)
和onHiddenChanged(boolean hidden)
,下面我们就来具体看一下这两个方法。
- setUserVisibleHint
setUserVisibleHint()方法只有在使用ViewPager管理Fragment时才会调用,有一个参数isVisibleToUser,字面意思就是是否对于用户可见,那么我们是否可以直接利用该参数来判断Fragment的可见与否呢?先别急,我们来看一下该方法的执行情况,依然采用之前顶部标签栏的例子,在每个Fragment中重写setUserVisibleHint()方法,打印isVisibleToUser的值。
首先来看一下初始状态显示第一个Fragment时的情况,由于已经设置了预加载数量为3,因此三个Fragment全部被创建和加载,但是我们注意setUserVisibleHint()方法的执行,对于第二和第三个Fragment来说,和我们预想中的一样,执行了一次,isVisibleToUser的值为false,也就是不可见;但对于第一个Fragment,setUserVisibleHint()方法执行了两次,并且第一次执行打印出来的isVisibleToUser的值为false,第二次才为true。再看一下setUserVisibleHint()的执行时机,我们发现该方法是在Fragment所有的生命周期方法之前就执行的,这一点需要注意。
再来看一下切换到第二个和第三个Fragment时的情况
这两种情况下就和预想的一样了,分别只执行了一次该方法,将相应Fragment的可见状态改变。
之后切换ViewPager都会执行两个Fragment的setUserVisibleHint()方法,不可见的的那个isVisibleToUser的值为false,显示出来的那个isVisibleToUser的值为true。
结合一开始显示第一Fragment时打印的结果来看,每个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是否隐藏,下面我们就以之前底部导航栏的例子验证一下onHiddenChanged()方法的执行时机和作用。
首先是初始状态显示第一个Fragment时,可以发现并没有执行第一个Fragment的onHiddenChanged()方法,这是由于我在代码添加了判断,如果Fragment实例对象为空,就调用add()方法先将Fragment添加到FragmentTransaction中,并没有调用show()方法。
切换到第二个Fragment时,由于调用了hide()方法隐藏第一个Fragment,因此执行了第一个Fragment的onHiddenChanged()方法,hidden参数的值为true,表示隐藏了Fragment。
切换到第三个Fragment时同上,会调用第二个Fragment的onHiddenChanged()方法,hidden参数的值为true。大家可能注意到了,我在代码中是先调用了hide()方法隐藏了所有不为空的Fragment,那么为什么这里这里没有调用第一个Fragment的onHiddenChanged()方法呢,其实很简单,因为之前第一个Fragment就已经是隐藏状态了,我们注意方法名后缀是'Changed',因此只有在隐藏或显示状态改变的情况下才会调用onHiddenChanged()方法。
之后在任意两个Fragment之间切换时会分别执行两个Fragment的onHiddenChanged()方法,可见的那个hidden值为false,表示显示;不可见的hidden值为true,表示隐藏。
由此我们可以得出结论,onHiddenChanged()方法是在调用show()和hide()方法时被调用的,并且只有在Fragment的隐藏或显示状态发生了改变时才会调用。不同于setUserVisibleHint()方法,调用onHiddenChanged()时Fragment已经完成了创建相关生命周期(onAttach()~onResume())的回调。
既然已经清楚了这两个方法的调用时机和作用,那么我们就可以来实现懒加载了,首先确定实现思路:
- 要在Fragment可见时加载数据,并且只加载一次。
- 由于ViewPager的预加载机制,因此要利用setUserVisibleHint()方法,根据参数isVisibleToUser来判断Fragment是否可见。
- setUserVisibleHint()方法不止会在Fragment切换时调用,在onCreateView()之前也会被调用,此时isVisibleToUser的值为false,这时是获取不到视图和控件的,因此不能只根据isVisibleToUser来判断是否需要加载数据,需要引入一个变量标识视图是否已经加载完成。
- 由于加载数据后继续切换ViewPager仍然会执行setUserVisibleHint()方法,因此还需要引入一个变量标识是否已经加载过数据,防止数据的重复加载。
- 只有当同时满足以下三个条件时才加载数据:
- 视图已加载完成
- 数据未加载
- Fragment可见
清楚了思路后,我们就可以来封装自己的LazyFragment了,完整代码如下:
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();
}
}
像我前面分析的那样,声明了两个变量,分别标识视图是否加载完成和数据是否已加载,执行懒加载的条件有三个:视图已加载、数据未加载、isVisibleToUser的值为true。lazyLoad()方法的作用是判断是否可以加载数据,真正的加载数据逻辑在onLazyLoad()方法中声明。
关于上面的代码有几点我要说一下,第一点是为什么要在onCreateView中再执行一次lazyLoad()方法,我们前面分析过,setUserVisibleHint()是在onCreateView()之前调用的,这时hasViewCreated的值为false,不满足条件,是无法执行加载数据的逻辑的,因此要在onCreateView中将hasViewCreated设置为true之后再判断一次是否可以加载数据,这也是为什么我要单独写一个lazyLoad()方法的原因。第二点是我在lazyLoad()方法中使用了getUserVisibleHint()方法,之前提到过,该方法的返回值就是setUserVisibleHint()中的参数isVisibleToUser,因此可以直接利用该方法来判断Fragment的可见性,就不需要额外再声明一个变量了。使用方法也很简单,只需要继承LazyFragment,实现getContentViewId()方法,返回布局文件id即可。如果不需要实现懒加载,就重写initData()方法,内部添加数据加载逻辑;如果需要实现懒加载,就不需要重写initData()方法,将加载数据的逻辑放到onLazyLoad()方法中就可以了。
这样封装其实也有一个问题,就是当同一个Fragment同时需要用于FragmentManager场景和ViewPager场景中时,如果将加载数据逻辑放到onLazyLoad()中,那么在使用FragmentManager管理Fragment时不会调用setUsersetUserVisibleHint()方法,也就无法加载数据了;如果把加载数据逻辑放到initData()中,那么就失去了懒加载的作用。我有看到过一种解决方法是重写onHiddenChanged()方法,根据相同的判断条件,执行加载数据逻辑,但是这样有一个问题是在每一个Fragment第一次调用add()
方法被添加后,需要手动调用hide()
和show()
方法来触发onHiddenChanged()方法,个人觉得还是有些奇怪,这里就不展示了。考虑到这种情况也不是很常见,如果真的遇到了,还是写两个Fragment吧。
为了效果明显我在LazyFragment中添加了一个ProgressDialog来显示数据加载进度,我们来看一下实现懒加载后的效果,只有当ViewPager切换到Fragment时才开始加载数据,如下图所示:
其实懒加载Fragment的具体封装方式有很多,但都是基于setUserVisibleHint()方法的,上面的代码只是我自己的一种封装,大家可以根据自己习惯的编码方式来实现自己的懒加载Fragment,重点还是要清楚原理和思路。
总结与后记
本文主要介绍了Fragment的懒加载实现以及ViewPager的预加载机制。实现Fragment的切换有两种方式:FragmentManager和ViewPager,其中前者不会提前加载Fragment,因此不需要实现懒加载;后者由于自身的预加载机制,需要考虑懒加载来使得页面的加载更加流畅。我们要清楚懒加载的实现并不是因为Fragment被延迟加载了,Fragment仍然会被预加载,只是当Fragment可见时才加载数据而已。
关于ViewPager和Fragment还有很多使用的技巧和可以深入去挖掘的东西,限于个人水平的原因,就不多提了,大家如果感兴趣可以查阅相关的资料。现在谷歌官方新推出了一个新的组件ViewPager2来取代ViewPager,支持了竖直方向的滑动,虽然由于兼容性等问题,短时间内ViewPager还不会被取代,但是有兴趣的话还是可以了解一下的。
本文的相关代码我已经上传到了github,由于自身水平的原因,我可能有些地方分析地不是很准确,表述地不是很清楚,欢迎大家指正,这样才能不断进步嘛。
Demo地址