多个ViewPager出现相同id场景

一、场景

有时候我们会在一个页面中添加多个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的显示逻辑就会正常。

你可能感兴趣的:(多个ViewPager出现相同id场景)