一、场景
有时候我们会在一个页面中添加多个ViewPager,有些场景可能由于特殊情况两个ViewPager id相同,而这时候就会出现一种情况:只有第一个ViewPager正常展示,其余ViewPager不展示。
首先解释下结果:
所有的Fragment都被添加到第一个ViewPager中,而且当第一个ViewPager也是ViewPager+Fragment的情况下,会出现相同position的Fragment只会被添加最先添加进的一个,Fragment的生命周期正常,且Fragment.getView().getParent()全都指向第一个ViewPager。
例如:
样例1、
ViewPager A:ViewPager+Fragment形式,添加了Fragment AF、BF。
ViewPager B:ViewPager+Fragment形式,添加了Fragment CF、DF。
ViewPager C:ViewPager+Fragment形式,添加了Fragment EF、FF,GF。
这种场景下,只有AF、BF、GF会被添加到ViewPager A中。ViewPager B和C都是无子Item。
样例2、
ViewPager A:ViewPager+Fragment形式,添加了View A、B。
ViewPager B:ViewPager+Fragment形式,添加了Fragment CF、DF。
ViewPager C:ViewPager+Fragment形式,添加了Fragment EF、FF,GF。
这种场景下,View A、B和CF、DF、GF会被添加到ViewPager A中。ViewPager B和C都是无子Item。
二、原因分析
1、首先需要看FragmentPagerAdapter的instantiateItem方法
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
final long itemId = getItemId(position);
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
#1 步骤
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
#2 步骤 这句是关键
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
} else {
fragment.setUserVisibleHint(false);
}
}
return fragment;
}
public long getItemId(int position) {
return position;
}
方法中传入的container对象即为ViewPager。
步骤1、代码会根据生成的name从FragmentManager中找对应的Fragment,如果找到,则不走getItem()获取Fragment。如果我们的页面中存在2个同名的Fragment而我们又没有去修改makeFragmentName方法,就会产生2个ViewPager所生成的name值相同,则此时第二个ViewPager通过FragmentManager.findFragmentByTag(name)方法找到的Fragment即不为空,此时不会执行getItem()方法,则第二个ViewPager对应位置上的Fragment就不会被添加到FragmentManager中,进而导致样例1中的情况。
我们当然可以通过重写makeFragmentName()方法来使得name值唯一,但这依然会出现异常,且看后面分析。
步骤2、Fragment会被添加到FragmentTransaction事务中。
接下来看FragmentTransaction中如何添加:
@NonNull
public FragmentTransaction add(@IdRes int containerViewId, @NonNull Fragment fragment,
@Nullable String tag) {
doAddOp(containerViewId, fragment, tag, OP_ADD);
return this;
}
void doAddOp(int containerViewId, Fragment fragment, @Nullable String tag, int opcmd) {
final Class> fragmentClass = fragment.getClass();
final int modifiers = fragmentClass.getModifiers();
...
if (containerViewId != 0) {
if (containerViewId == View.NO_ID) {
throw new IllegalArgumentException("Can't add fragment "
+ fragment + " with tag " + tag + " to container view with no id");
}
if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) {
throw new IllegalStateException("Can't change container ID of fragment "
+ fragment + ": was " + fragment.mFragmentId
+ " now " + containerViewId);
}
#步骤1
fragment.mContainerId = fragment.mFragmentId = containerViewId;
}
addOp(new Op(opcmd, fragment));
}
步骤1、这是我认为最关键的一步,将Fragment的mContainerId指向为containerViewId,而这个id便为我们ViewPager的id 。
接下来搜索Fragment.mContainerId在哪里使用:
#FragmentMangerImpl.java
void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive) {
...
case Fragment.CREATED:
...
if (newState > Fragment.CREATED) {
if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f);
if (!f.mFromLayout) {
ViewGroup container = null;
if (f.mContainerId != 0) {
if (f.mContainerId == View.NO_ID) {
throwException(new IllegalArgumentException(
"Cannot create fragment "
+ f
+ " for a container view with no id"));
}
#步骤1
container = (ViewGroup) mContainer.onFindViewById(f.mContainerId);
if (container == null && !f.mRestored) {
String resName;
try {
resName = f.getResources().getResourceName(f.mContainerId);
} catch (Resources.NotFoundException e) {
resName = "unknown";
}
throwException(new IllegalArgumentException(
"No view found for id 0x"
+ Integer.toHexString(f.mContainerId) + " ("
+ resName
+ ") for fragment " + f));
}
}
#步骤2
f.mContainer = container;
f.performCreateView(f.performGetLayoutInflater(
f.mSavedFragmentState), container, f.mSavedFragmentState);
if (f.mView != null) {
f.mInnerView = f.mView;
f.mView.setSaveFromParentEnabled(false);
if (container != null) {
#步骤3
container.addView(f.mView);
}
if (f.mHidden) {
f.mView.setVisibility(View.GONE);
}
f.onViewCreated(f.mView, f.mSavedFragmentState);
dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState,
false);
// Only animate the view if it is visible. This is done after
// dispatchOnFragmentViewCreated in case visibility is changed
f.mIsNewlyAdded = (f.mView.getVisibility() == View.VISIBLE)
&& f.mContainer != null;
} else {
f.mInnerView = null;
}
}
f.performActivityCreated(f.mSavedFragmentState);
dispatchOnFragmentActivityCreated(f, f.mSavedFragmentState, false);
if (f.mView != null) {
f.restoreViewState(f.mSavedFragmentState);
}
f.mSavedFragmentState = null;
}
...
}
在FragmentManagerImpl.moveToState方法中,当newState为CREATED的时候,演示了Fragment是如何添加到布局中的:
步骤1、通过mContainer.onFindViewById方法,根据Fragment.mContainerId从页面中找到Fragment需要添加的父布局。
mContainer是FragmentManagerImpl的局部变量,追溯onFindViewById方法最终会发现调用的是FragmentActivity.findViewById()或Fragment.mView.findViewById(),取决于该FragmentManager是在Activity下还是Fragment下。
也就是说,mContainer.onFindViewById方法最终会从根布局上找Fragment的父布局,在该场景下,由于单个页面中存在多个相同id的ViewPager,所以FragmentManagerImpl根据Fragment.mContainerId找到的一直都是第一个ViewPager。
步骤2、调用Fragment.performCreateView(),在里面又会调用我们熟悉的onCreateView()方法创建根布局。
步骤3、container.addView()这里会把Fragment.mView添加到container里,这里也就证实了为什么后面ViewPager的Fragment会被添加进第一个ViewPager里面了。
结论
当同一个页面下如果存在多个id相同的ViewPager时,除了第一个ViewPager,后面的ViewPager如果添加的是Fragment时则会出现添加异常的情况,目标Fragment由于父布局id相同,在FragmentMangerImpl中找父View时会找到第一个ViewPager,从而出现所有的Fragment都添加到第一个ViewPager的情况,从而导致后面几个ViewPager没有内容的情况。
解决方案:改ViewPager的id就可以了。
还有种情况是可能让同一个页面中允许存在2个相同id的ViewPager且显示正常的。当且仅当两个ViewPager传入的FragmentManger不是同一个的情况(例如一个传入的是Activity的getSupportFragmentManager,另一个传入的是Fragment的getChildFragmentManager),当FragmentManger不为同一个的情况时,在步骤1中就会因为从不同的根布局中寻找各自的子View,这个时候找到的ViewPager就不会是同一个,因而添加Fragment的显示逻辑就会正常。