对于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亿中国人表现出的团结,那些身穿白大褂的医护工作者,身穿警服或军装头顶国徽的人民子弟兵表现出的无私的精神,足以让所有的中国人为之动容。这是中国人骨子里的那种优秀品质,这是华夏民族五千年未曾丢失的优良传统。
愿,新冠病毒早日被彻底控制住,向我们的白衣天使,平民英雄致敬。你们是祖国的骄傲,是国人心中的定心丸。