从ViewPager中检索一个片段

256

我正在使用 ViewPagerFragmentStatePagerAdapter 一起托管三个不同的片段:

  • [片段1]
  • [片段2]
  • [片段3]

当我想从 FragmentActivity 中获得 ViewPager 中的 片段1 时。

问题是什么,如何解决?

23个回答

507
主要答案依赖于框架生成的名称。如果这个名称改变了,那么它将不再起作用。
那么这个解决方案呢?覆盖您的Fragment(State)PagerAdapterinstantiateItem()destroyItem()
public class MyPagerAdapter extends FragmentStatePagerAdapter {
    SparseArray<Fragment> registeredFragments = new SparseArray<Fragment>();

    public MyPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public int getCount() {
        return ...;
    }

    @Override
    public Fragment getItem(int position) {
        return MyFragment.newInstance(...); 
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Fragment fragment = (Fragment) super.instantiateItem(container, position);
        registeredFragments.put(position, fragment);
        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        registeredFragments.remove(position);
        super.destroyItem(container, position, object);
    }

    public Fragment getRegisteredFragment(int position) {
        return registeredFragments.get(position);
    }
}

这个方法对于已经存在的Fragment有效。当调用getRegisteredFragment时,尚未实例化的Fragment将返回null。但我大多数情况下使用它来从ViewPager中获取当前的Fragmentadapater.getRegisteredFragment(viewPager.getCurrentItem()),这不会返回null
我不知道这种解决方案是否存在其他缺点。如果有任何问题,请告诉我。

42
我也认为这是最好的解决方案。唯一的建议是也许将“Fragment”包裹在“WeakReference”中,以确保您不会阻止销毁的片段被垃圾回收?这似乎是正确的做法... - Alex Lockwood
7
MyPagerAdapter由于生命周期(例如旋转)被销毁时,会发生什么情况?registerdFragments会丢失吗? 使用MyPagerAdapterActivity/Fragment是否必须在onSaveInstanceState中保存它,然后需要使用新的FragmentManager引用更新它? - Steven Byle
10
如果我没有弄错的话(你应该检查一下),getItem方法并没有被调用 - 如你所知 - 但是在旋转后由框架恢复每个Fragment时,会调用instantiateItem方法。 - Streets Of Boston
6
是的,我已经确认getItem在旋转时不会再次调用(对于已经创建的任何碎片),因为FragmentManager恢复了它持有的pager中Fragments的状态。如果当每个Fragment被恢复时调用instantiateItem,那么这个解决方案实际上比我的或被接受的答案更安全和具有未来性。我会考虑自己尝试这个解决方案。 - Steven Byle
6
@Dori 我没有破坏它。我的解决方案是不使用“getItem()”调用的结果,而是使用从“instantiateItem()”调用中存储的结果。并且片段的生命周期没有被破坏,因为存储的片段将在“destroyItem()”调用时从稀疏数组中删除。 - Streets Of Boston
显示剩余21条评论

85

对于从ViewPager中获取片段,这里和其他相关的SO线程/博客上有很多答案。我看到的每一个都是错误的,并且它们通常似乎分为下面列出的两种类型。如果您只想抓取当前片段,则还有一些其他有效的解决方案,例如此线程上的this

如果使用FragmentPagerAdapter,请参见下文。如果使用FragmentStatePagerAdapter,值得看看this。在FragmentStateAdapter中抓取不是当前项的索引并不像它的本质那样有用,因为这些将在视图/超出屏幕限制范围后被完全拆除。

不良路径

错误:维护自己的内部片段列表,在调用FragmentPagerAdapter.getItem()时添加

  • 通常使用SparseArrayMap
  • 我看过的许多示例都没有考虑生命周期事件,因此该解决方案很脆弱。由于在ViewPager中仅在第一次滚动到页面(或者如果您的ViewPager.setOffscreenPageLimit(x)>0)时才调用getItem,因此如果主机Activity/Fragment被杀死或重新启动,则在重新创建自定义FragmentPagerActivity时将清除内部SparseArray,但是在幕后,ViewPagers内部片段将被重新创建,并且不会为任何索引调用getItem,因此将永远失去根据索引获取片段的能力。您可以通过FragmentManager.getFragment()putFragment保存和恢复这些片段引用来解决这个问题,但我认为这开始变得混乱了。

错误:构建与FragmentPagerAdapter底层使用的标记ID匹配的标记ID,然后使用它从FragmentManager检索页面片段

  • 这种方法比第一个内部数组解决方案更好,因为它处理了丢失片段引用的问题,但正如上面和网络其他地方正确指出的那样 - 它感觉很hacky,因为它是 ViewPager 内部的一个私有方法,可能随时或针对任何操作系统版本进行更改。

为此解决方案重新创建的方法是

private static String makeFragmentName(int viewId, long id) {
    return "android:switcher:" + viewId + ":" + id;
}

一个愉快的路径:ViewPager.instantiateItem()

类似于上面的getItem()方法,但不会破坏生命周期的方法是使用instantiateItem()而不是getItem(),因为前者每次创建/访问索引时都会被调用。请参见此答案

一个愉快的路径:构建自己的FragmentViewPager

最新支持库的源码中构建您自己的FragmentViewPager类,并更改在其中生成片段标记的内部方法。您可以用以下代码替换它。这样做的好处是,您知道标记创建永远不会改变,并且不依赖于私有API /方法,这总是很危险的。

/**
 * @param containerViewId the ViewPager this adapter is being supplied to
 * @param id pass in getItemId(position) as this is whats used internally in this class
 * @return the tag used for this pages fragment
 */
public static String makeFragmentName(int containerViewId, long id) {
    return "android:switcher:" + containerViewId + ":" + id;
}

接着文档所说,当您想要获取用于索引的片段时,只需调用类似于此方法的内容(您可以将其放入自定义的FragmentPagerAdapter或其子类中),请注意,如果尚未为该页面调用getItem(即尚未创建该页面),则结果可能为空

/**
 * @return may return null if the fragment has not been instantiated yet for that position - this depends on if the fragment has been viewed
 * yet OR is a sibling covered by {@link android.support.v4.view.ViewPager#setOffscreenPageLimit(int)}. Can use this to call methods on
 * the current positions fragment.
 */
public @Nullable Fragment getFragmentForPosition(int position)
{
    String tag = makeFragmentName(mViewPager.getId(), getItemId(position));
    Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
    return fragment;
}

这是一个简单的解决方案,解决了在网络上找到的其他两个解决方案中存在的问题。

6
这样做是可行的,但需要将ViewPager类复制到您自己的源代码中。依赖于ViewPager的pager-adapter的'instantiateItem()'和'destroyItem()'将起作用,并将遵守片段的生命周期(当从保存的实例状态创建片段时,这两种方法也会被调用)。您是正确的,依赖于调用'getItem()'并不是一个好主意,使用私有API也不是好的选择。 - Streets Of Boston
如果setOffscreenPageLimit=1(默认值),FragmentStatePagerAdapter将在访问视图页时开始交换片段,以便将其加载到内存中。通过调用getItem()方法,将创建重新访问的页面/片段的新实例。在这种重新访问情况下,是否有一种方法可以指示系统恢复先前创建的实例,而不是创建新实例? - carl
如果超出页面限制,则必须创建一个新页面,但要传递保存的状态。如果想保留旧页面,则可以增加页面限制。 - Dori
@DoriО╪▄Е°╗FragmentStatePagerAdapterД╦╜Ф╡║Ф°┴getItemId(pos)Ф√╧ФЁ∙Ц─┌ - Jemshit Iskenderov
当我调用getFragmentForPosition时,我得到了null。另外,上面的答案对理论有很长的描述,但是对于ViewPager新手来说,它们的实现并不好...请解释在哪里放置上面的代码块以及如何实现它们以从ViewPager获取片段对象。 - Crawler
显示剩余2条评论

70

在您的FragmentPagerAdapter中添加以下方法:

public Fragment getActiveFragment(ViewPager container, int position) {
String name = makeFragmentName(container.getId(), position);
return  mFragmentManager.findFragmentByTag(name);
}

private static String makeFragmentName(int viewId, int index) {
    return "android:switcher:" + viewId + ":" + index;
}

需要确保getActiveFragment(0)能够正常工作。

这里提供了一个实现在ViewPager中的解决方案https://gist.github.com/jacek-marchwicki/d6320ba9a910c514424d,如果出现问题,您将看到良好的崩溃日志。


非常感谢:)。虽然我用另一种方式解决了它,我使用了一个处理程序来发布片段。这不是很合适,但最终解决了问题。顺便说一下,你很好:) - Qun Qin
12
我曾经使用这种方法,但我相信当我升级到API 17时它已经不起作用了。我推荐另一种方法,并希望Google能够发布一个API来根据位置检索ViewPager中的Fragment。 - gcl1
我在调用第二个片段时遇到了问题,该方法会创建一个新的片段。 - Vasil Valchev
2
自从某些API版本,makeFragmentName方法已更改为 private static String makeFragmentName(int viewId, long id) { return "android:switcher:" + viewId + ":" + id; } 因此,getActiveFragment方法现在应该像这样(对于第二个参数使用getItemId而不是position) public Fragment getActiveFragment(ViewPager container, int position) { String name = makeFragmentName(container.getId(), getItemId(position)); return mFragmentManager.findFragmentByTag(name); } - Eugene Popovich
1
它确实可以工作,但非常危险,因为它依赖于视图分页器的内部实现,如果谷歌更改了实现方式(他们可以这样做),那么糟糕的事情就会发生。我真的希望谷歌能够提供获取实时片段的功能。 - shem
我的回答指出了如何使其不依赖于私有实现。https://dev59.com/vGoy5IYBdhLWcg3wFqFT#24662049 - Dori

37

另一个简单的解决方案:

    public class MyPagerAdapter extends FragmentPagerAdapter {
        private Fragment mCurrentFragment;

        public Fragment getCurrentFragment() {
            return mCurrentFragment;
        }
//...    
        @Override
        public void setPrimaryItem(ViewGroup container, int position, Object object) {
            if (getCurrentFragment() != object) {
                mCurrentFragment = ((Fragment) object);
            }
            super.setPrimaryItem(container, position, object);
        }
    }

1
这似乎非常适合我当前的片段需求。 - danny117
如果你只需要获取当前选中的Fragment,那么你只需要使用setPrimaryItem方法 - 这个方法在attach时设置,并且在每次更新Fragment的内部集合作为当前项时也会被调用。 - Graeme
1
这听起来是一个不错的解决方案,但由于setPrimaryItem()ViewPager.OnPageChangeListener#onPageSelected()之后被调用,所以我不能使用它 :-( - hardysim

21
我知道这个问题有一些答案,但也许这会对某个人有所帮助。 当我需要从我的ViewPager中获取一个Fragment时,我使用了一个相对简单的解决方案。 在持有ViewPager的Activity或Fragment中,您可以使用此代码来循环遍历它所包含的每个Fragment。
FragmentPagerAdapter fragmentPagerAdapter = (FragmentPagerAdapter) mViewPager.getAdapter();
for(int i = 0; i < fragmentPagerAdapter.getCount(); i++) {
    Fragment viewPagerFragment = fragmentPagerAdapter.getItem(i);
    if(viewPagerFragment != null) {
        // Do something with your Fragment
        // Check viewPagerFragment.isResumed() if you intend on interacting with any views.
    }
}
  • 如果您知道在ViewPager中您的Fragment的位置,可以直接调用getItem(knownPosition)方法。

  • 如果您不知道在ViewPager中您的Fragment的位置,您可以让子Fragment实现一个带有getUniqueId()方法的接口,并使用它来区分它们。或者您可以循环遍历所有的Fragment并检查其类类型,例如if(viewPagerFragment instanceof FragmentClassYouWant)

!!!编辑!!!

我发现只有当需要创建每个Fragment时,FragmentPagerAdapter才会调用getItem方法,之后,似乎Fragments是通过FragmentManager进行回收利用的。因此,许多FragmentPagerAdapter的实现都会在getItem方法中创建新的Fragment。使用上面的方法,这意味着我们将在通过FragmentPagerAdapter中的所有项目时每次都创建新的Fragment。由于这一点,我找到了一种更好的方法,使用FragmentManager获取每个Fragment(使用被接受的答案)。这是一个更完整的解决方案,并且对我来说一直运作良好。

FragmentPagerAdapter fragmentPagerAdapter = (FragmentPagerAdapter) mViewPager.getAdapter();
for(int i = 0; i < fragmentPagerAdapter.getCount(); i++) {
    String name = makeFragmentName(mViewPager.getId(), i);
    Fragment viewPagerFragment = getChildFragmentManager().findFragmentByTag(name);
    // OR Fragment viewPagerFragment = getFragmentManager().findFragmentByTag(name);
    if(viewPagerFragment != null) {

        // Do something with your Fragment
        if (viewPagerFragment.isResumed()) {
            // Interact with any views/data that must be alive
        }
        else {
            // Flag something for update later, when this viewPagerFragment
            // returns to onResume
        }
    }
}

而您将需要这种方法。

private static String makeFragmentName(int viewId, int position) {
    return "android:switcher:" + viewId + ":" + position;
}

1
你刚刚找到了findFragmentByTag()方法,但是我们在哪里可以设置fragment的TAG呢? - VinceStyling
@vince ViewPager 本身设置了这些标签。这种解决方案很脆弱,因为它依赖于了解 ViewPager 的“隐藏”命名约定。Boston 街道的答案是一个更全面的解决方案,我现在在我的项目中使用它(而不是这种方法)。 - Steven Byle
这个很好用!但是我移除了“if (viewPagerFragment.isResumed())”的条件,因为我需要更新我的离屏ListFragment页面。然后当用户返回到ListFragment页面时,它会使用新数据全部更新。 - JDJ

11

针对我的情况,以上解决方案均无效。

但由于我在Fragment中使用了Child Fragment Manager,因此采用了以下方法:

Fragment f = getChildFragmentManager().getFragments().get(viewPager.getCurrentItem());

仅当Manager中的fragments与viewpager项对应时,才会起作用。


这是一个完美的答案,可以在活动或片段重新创建时从视图页面获取旧片段。 - Smeet

9
为了获取ViewPager中当前可见的片段,我使用以下简单语句,并且它可以很好地工作。
      public Fragment getFragmentFromViewpager()  
      {
         return ((Fragment) (mAdapter.instantiateItem(mViewPager, mViewPager.getCurrentItem())));
      }

3

这是基于Steven上面的回答。这将返回已附加到父活动的片段的实例。

FragmentPagerAdapter fragmentPagerAdapter = (FragmentPagerAdapter) mViewPager.getAdapter();
    for(int i = 0; i < fragmentPagerAdapter.getCount(); i++) {

        Fragment viewPagerFragment = (Fragment) mViewPager.getAdapter().instantiateItem(mViewPager, i);
        if(viewPagerFragment != null && viewPagerFragment.isAdded()) {

            if (viewPagerFragment instanceof FragmentOne){
                FragmentOne oneFragment = (FragmentOne) viewPagerFragment;
                if (oneFragment != null){
                    oneFragment.update(); // your custom method
                }
            } else if (viewPagerFragment instanceof FragmentTwo){
                FragmentTwo twoFragment = (FragmentTwo) viewPagerFragment;

                if (twoFragment != null){
                    twoFragment.update(); // your custom method
                }
            }
        }
    }

3

我通过首先创建一个包含所有碎片(List<Fragment> fragments;)的列表,然后将它们添加到分页器中,使得当前查看的碎片更容易处理。

因此:

@Override
onCreate(){
    //initialise the list of fragments
    fragments = new Vector<Fragment>();

    //fill up the list with out fragments
    fragments.add(Fragment.instantiate(this, MainFragment.class.getName()));
    fragments.add(Fragment.instantiate(this, MenuFragment.class.getName()));
    fragments.add(Fragment.instantiate(this, StoresFragment.class.getName()));
    fragments.add(Fragment.instantiate(this, AboutFragment.class.getName()));
    fragments.add(Fragment.instantiate(this, ContactFragment.class.getName()));


    //Set up the pager
    pager = (ViewPager)findViewById(R.id.pager);
    pager.setAdapter(new MyFragmentPagerAdapter(getSupportFragmentManager(), fragments));
    pager.setOffscreenPageLimit(4);
}

那么这可以称为:
public Fragment getFragment(ViewPager pager){   
    Fragment theFragment = fragments.get(pager.getCurrentItem());
    return theFragment;
}

那么我可以将其放在一个if语句中,仅当它在正确的片段上运行时才会运行。
Fragment tempFragment = getFragment();
if(tempFragment == MyFragmentNo2.class){
    MyFragmentNo2 theFrag = (MyFragmentNo2) tempFragment;
    //then you can do whatever with the fragment
    theFrag.costomFunction();
}

但这只是我的“hack and slash”方法,但它对我有用,当按下返回按钮时,我使用它来进行当前显示片段的相关更改。


1
最适合我的答案 - Sonu Kumar

2
嘿,我在这里回答了这个问题。基本上,您需要重写FragmentStatePagerAdapter的

public Object instantiateItem(ViewGroup container, int position)

方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接