如何使用FragmentPagerAdapter获取现有片段

我有问题,通过使用FragmentPagerAdapterActivity将我的片段彼此进行通信,作为帮助程序类实现标签的pipe理以及将ViewPager与关联的ViewPager连接的所有详细信息。 我已经实现了FragmentPagerAdapter就像Android示例项目Support4Demos提供的一样

主要的问题是我怎样才能从FragmentManager得到特定的片段,当我没有Id或标签? FragmentPagerAdapter正在创build片段并自动生成标识和标签。

问题的总结

注意:在这个答案中,我将引用FragmentPagerAdapter及其源代码。 但一般的解决scheme也应该适用于FragmentStatePagerAdapter

如果你正在读这个,你可能已经知道FragmentPagerAdapter / FragmentStatePagerAdapter是为你的ViewPager创buildFragments ,但在Activity重新创build时(无论是从设备旋转还是系统杀死你的应用程序重新获得内存),这些Fragments都不会被创build再次,而是他们的实例从FragmentManager检索 。 现在说你的Activity需要得到这些Fragments的参考来做他们的工作。 您没有这些创build的Fragmentsidtag ,因为FragmentPagerAdapter 在内部设置它们 。 所以问题是如何得到一个没有这些信息的参考…

目前的解决scheme的问题:依靠内部代码

我在这个和类似的问题上看到的很多解决scheme都是通过调用FragmentManager.findFragmentByTag()和模仿内部创build的标签来获得对现有Fragment的引用"android:switcher:" + viewId + ":" + id 。 与此相关的问题是您依赖内部源代码,因为我们都知道这是不能保证永远保持不变的。 Google的Android工程师可以轻松地决定更改tag结构,这会破坏您的代码,从而无法find对现有Fragments的引用。

不依赖于内部tag替代解决scheme

下面是一个简单的例子,介绍如何获得对Fragments返回的FragmentPagerAdapter的引用,而不依赖FragmentPagerAdapter上设置的内部tags 。 关键是覆盖instantiateItem()并保存引用, 而不是getItem()

 public class SomeActivity extends Activity { private FragmentA m1stFragment; private FragmentB m2ndFragment; // other code in your Activity... private class CustomPagerAdapter extends FragmentPagerAdapter { // other code in your custom FragmentPagerAdapter... public CustomPagerAdapter(FragmentManager fm) { super(fm); } @Override public Fragment getItem(int position) { // Do NOT try to save references to the Fragments in getItem(), // because getItem() is not always called. If the Fragment // was already created then it will be retrieved from the FragmentManger // and not here (ie getItem() won't be called again). switch (position) { case 0: return new FragmentA(); case 1: return new FragmentB(); default: // This should never happen. Always account for each position above return null; } } // Here we can finally safely save a reference to the created // Fragment, no matter where it came from (either getItem() or // FragmentManger). Simply save the returned Fragment from // super.instantiateItem() into an appropriate reference depending // on the ViewPager position. @Override public Object instantiateItem(ViewGroup container, int position) { Fragment createdFragment = (Fragment) super.instantiateItem(container, position); // save the appropriate reference depending on position switch (position) { case 0: m1stFragment = (FragmentA) createdFragment; break; case 1: m2ndFragment = (FragmentB) createdFragment; break; } return createdFragment; } } public void someMethod() { // do work on the referenced Fragments, but first check if they // even exist yet, otherwise you'll get an NPE. if (m1stFragment != null) { // m1stFragment.doWork(); } if (m2ndFragment != null) { // m2ndFragment.doSomeWorkToo(); } } } 

或者如果您喜欢使用tags而不是类成员variables/对Fragments引用,那么您也可以按照相同的方式获取由FragmentPagerAdapter设置的tags :注意:这不适用于FragmentStatePagerAdapter因为它在创build时不会设置tagsFragments

 @Override public Object instantiateItem(ViewGroup container, int position) { Fragment createdFragment = (Fragment) super.instantiateItem(container, position); // get the tags set by FragmentPagerAdapter switch (position) { case 0: String firstTag = createdFragment.getTag(); break; case 1: String secondTag = createdFragment.getTag(); break; } // ... save the tags somewhere so you can reference them later return createdFragment; } 

请注意,此方法不依赖于模仿由FragmentPagerAdapter设置的内部tag ,而是使用适当的API来检索它们。 这样,即使tag在未来版本的SupportLibrary发生变化,您仍然是安全的。


不要忘记 ,根据您的Activity的devise,您尝试处理的Fragments可能还可能不存在,因此您必须在使用引用之前通过执行null检查来解决这个问题。

另外,如果你正在使用FragmentStatePagerAdapter ,那么你不想保留对你的Fragments硬引用,因为你可能拥有很多引用,而且硬引用会不必要地将它们留在内存中。 而是将Fragment引用保存在WeakReferencevariables中而不是标准引用中。 喜欢这个:

 WeakReference<Fragment> m1stFragment = new WeakReference<Fragment>(createdFragment); // ...and access them like so Fragment firstFragment = m1stFragment.get(); if (firstFragment != null) { // reference hasn't been cleared yet; do work... } 

我已经find了回答我的问题基于以下post: 重复使用fragmentpageradapter中的片段

我所学到的一些事情:

  1. FragmentPagerAdapter中的getItem(int position)相当具有误导性,这个方法实际上做了什么。 它创build新的碎片,不返回现有的碎片。 在这个意义上,该方法应该重命名为Android SDK中的createItem(int position) 。 所以这个方法不能帮助我们得到碎片。
  2. 根据后期支持FragmentPagerAdapterholds对旧片段的引用中的解释,您应该将片段的创build保留到FragmentPagerAdapter中,这意味着您没有对片段或其标签的引用。 如果你有片段标签,你可以通过调用findFragmentByTag()FragmentManager轻松地获得对它的引用。 我们需要一种方法来查找给定页面位置的片段标签。

在你的类中添加下面的帮助器方法来检索片段标签并将其发送到findFragmentByTag()方法。

 private String getFragmentTag(int viewPagerId, int fragmentPosition) { return "android:switcher:" + viewPagerId + ":" + fragmentPosition; } 

注意! 这是FragmentPagerAdapter在创build新片段时使用的相同方法。 请参阅此链接http://code.google.com/p/openintents/source/browse/trunk/compatibility/AndroidSupportV2/src/android/support/v2/app/FragmentPagerAdapter.java#104

我创build了这个方法,它正在为我获得对当前片段的引用。

 public static Fragment getCurrentFragment(ViewPager pager, FragmentPagerAdapter adapter) { try { Method m = adapter.getClass().getSuperclass().getDeclaredMethod("makeFragmentName", int.class, long.class); Field f = adapter.getClass().getSuperclass().getDeclaredField("mFragmentManager"); f.setAccessible(true); FragmentManager fm = (FragmentManager) f.get(adapter); m.setAccessible(true); String tag = null; tag = (String) m.invoke(null, pager.getId(), (long) pager.getCurrentItem()); return fm.findFragmentByTag(tag); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } return null; } 

我做的方式是定义一个弱引用Hashtable如下:

 protected Hashtable<Integer, WeakReference<Fragment>> fragmentReferences; 

然后,我写下了这样的getItem()方法:

 @Override public Fragment getItem(int position) { Fragment fragment; switch(position) { case 0: fragment = new MyFirstFragmentClass(); break; default: fragment = new MyOtherFragmentClass(); break; } fragmentReferences.put(position, new WeakReference<Fragment>(fragment)); return fragment; } 

那么你可以写一个方法:

 public Fragment getFragment(int fragmentId) { WeakReference<Fragment> ref = fragmentReferences.get(fragmentId); return ref == null ? null : ref.get(); } 

这似乎工作得很好,我发现它比黑客less一点

 "android:switcher:" + viewId + ":" + position 

技巧,因为它不依赖于如何实现FragmentPagerAdapter。 当然,如果片段已经被FragmentPagerAdapter释放,或者如果尚未创build,getFragment将返回null。

如果有人发现这种方法有什么问题,评论是值得欢迎的。

您不需要重写instantiateItem也不需要使用内部makeFragmentName创build片段标记的makeFragmentNameinstantiateItem是一个公共的方法,所以你可以(实际上你应该 )在你的activity的onCreate方法中调用它来获取(并存储一个本地var,如果你需要的话)引用你的片段的实例。 只要记住围绕一组instantiateItem调用startUpdatefinishUpdate方法,如在PagerAdapter javadoc中所述:

对PagerAdapter方法startUpdate(ViewGroup)的调用表明ViewPager的内容即将改变。 随后将对一个或多个对instantiateItem(ViewGroup,int)和/或destroyItem(ViewGroup,int,Object)的调用进行调用,并通过调用finishUpdate(ViewGroup)来通知更新结束。

所以例如这是在onCreate方法中存储对你的标签片段的引用的方法:

 public class MyActivity extends AppCompatActivity { Fragment0 tab0; Fragment1 tab1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.myLayout); ViewPager viewPager = (ViewPager) findViewById(R.id.myViewPager); MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager()); viewPager.setAdapter(adapter); TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); tabLayout.setupWithViewPager(viewPager); adapter.startUpdate(viewPager); tab0 = (Fragment0) adapter.instantiateItem(viewPager, 0); tab1 = (Fragment1) adapter.instantiateItem(viewPager, 1); adapter.finishUpdate(viewPager); } class MyPagerAdapter extends FragmentPagerAdapter { public MyPagerAdapter(FragmentManager manager) {super(manager);} @Override public int getCount() {return 2;} @Override public Fragment getItem(int position) { if (position == 0) return new Fragment0(); if (position == 1) return new Fragment1(); return null; // or throw some exception } @Override public CharSequence getPageTitle(int position) { if (position == 0) return getString(R.string.tab0); if (position == 1) return getString(R.string.tab1); return null; // or throw some exception } } } 

instantiateItem将首先尝试从FragmentManager获取对现有片段实例的引用,并且只有当它们不存在时,才会使用来自适配器的getItem方法创build新的FragmentManager (以及在FragmentManager中的“存储”)。

一些额外的信息:
如果你没有在你的onCreate方法中调用instantiateItem然后finishUpdate (和之前的startUpdate ),那么你有冒险,你的片段实例永远不会被提交FragmentManager :当你的活动成为前景时instantiateItem将被自动调用来获取你的片段,但start / finishUpdate 可能 (取决于实现细节),他们基本上是做什么是开始/提交FragmentTransaction 。 这可能会导致对创build的片段实例的引用非常快(例如,当您旋转屏幕时)并重新创build的次数超过必要的次数。 根据碎片的“重”程度,可能会有不可忽视的性能后果。

@ personne3000提出的解决scheme是很好的,但它有一个问题:当活动进入后台,被系统杀死(为了获得一些空闲的内存),然后恢复, fragmentReferences将是空的,因为getItem wouldn'不要叫。

下面的课程处理这种情况:

 public abstract class AbstractHolderFragmentPagerAdapter<F extends Fragment> extends FragmentPagerAdapter { public static final String FRAGMENT_SAVE_PREFIX = "holder"; private final FragmentManager fragmentManager; // we need to store fragment manager ourselves, because parent's field is private and has no getters. public AbstractHolderFragmentPagerAdapter(FragmentManager fm) { super(fm); fragmentManager = fm; } private SparseArray<WeakReference<F>> holder = new SparseArray<WeakReference<F>>(); protected void holdFragment(F fragment) { holdFragment(holder.size(), fragment); } protected void holdFragment(int position, F fragment) { if (fragment != null) holder.put(position, new WeakReference<F>(fragment)); } public F getHoldedItem(int position) { WeakReference<F> ref = holder.get(position); return ref == null ? null : ref.get(); } public int getHolderCount() { return holder.size(); } @Override public void restoreState(Parcelable state, ClassLoader loader) { // code inspired by Google's FragmentStatePagerAdapter implementation super.restoreState(state, loader); Bundle bundle = (Bundle) state; for (String key : bundle.keySet()) { if (key.startsWith(FRAGMENT_SAVE_PREFIX)) { int index = Integer.parseInt(key.substring(FRAGMENT_SAVE_PREFIX.length())); Fragment f = fragmentManager.getFragment(bundle, key); holdFragment(index, (F) f); } } } @Override public Parcelable saveState() { Bundle state = (Bundle) super.saveState(); if (state == null) state = new Bundle(); for (int i = 0; i < holder.size(); i++) { int id = holder.keyAt(i); final F f = getHoldedItem(i); String key = FRAGMENT_SAVE_PREFIX + id; fragmentManager.putFragment(state, key, f); } return state; } } 

获取片段句柄的主要障碍是你不能依赖getItem()。 方向更改后,对片段的引用将为空,并且不会再调用getItem()。

这是一个不依赖于FragmentPagerAdapter的实现来获取标签的方法。 覆盖instantiateItem(),它将返回从getItem()创build的片段或从片段pipe理器中find的片段。

 @Override public Object instantiateItem(ViewGroup container, int position) { Object value = super.instantiateItem(container, position); if (position == 0) { someFragment = (SomeFragment) value; } else if (position == 1) { anotherFragment = (AnotherFragment) value; } return value; } 

我总是使用这个基类,当我需要访问子片段或主(当前可见)片段。 它不依赖于任何实现细节,并且考虑到生命周期的变化,因为在这两种情况下都会调用覆盖的方法 – 创build新的片段实例,以及何时从FragmentManager接收到实例。

 public abstract class FragmentPagerAdapterExt extends FragmentPagerAdapter { private final ArrayList<Fragment> mFragments; private Fragment mPrimaryFragment; public FragmentPagerAdapterExt(FragmentManager fm) { super(fm); mFragments = new ArrayList<>(getCount()); } @Override public Object instantiateItem(ViewGroup container, int position) { Object object = super.instantiateItem(container, position); mFragments.add((Fragment) object); return object; } @Override public void destroyItem(ViewGroup container, int position, Object object) { mFragments.remove(object); super.destroyItem(container, position, object); } @Override public void setPrimaryItem(ViewGroup container, int position, Object object) { super.setPrimaryItem(container, position, object); mPrimaryFragment = (Fragment) object; } /** Returns currently visible (primary) fragment */ public Fragment getPrimaryFragment() { return mPrimaryFragment; } /** Returned list can contain null-values for not created fragments */ public List<Fragment> getFragments() { return Collections.unmodifiableList(mFragments); } } 

我设法通过使用ids而不是标签来解决这个问题。 (我正在使用我定义的FragmentStatePagerAdapter,它使用我的自定义片段,其中我覆盖了onAttach方法,你保存在某处的id:

 @Override public void onAttach(Context context){ super.onAttach(context); MainActivity.fragId = getId(); } 

然后您只需在活动内部轻松访问片段即可:

 Fragment f = getSupportFragmentManager.findFragmentById(fragId); 

看到这个post从FragmentPagerAdapter返回片段。 是否依靠你知道你的片段的索引 – 但这将设置在getItem()(仅在实例化)

我不知道这是否是最好的办法,但没有别的办法为我工作。 所有其他选项,包括getActiveFragment返回null或导致应用程序崩溃。

我注意到,在屏幕旋转的片段被连接,所以我用它发送片段回到活动。

在片段中:

 @Override public void onAttach(Activity activity) { super.onAttach(activity); try { mListener = (OnListInteractionListener) activity; mListener.setListFrag(this); } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement OnFragmentInteractionListener"); } } 

然后在活动中:

 @Override public void setListFrag(MyListFragment lf) { if (mListFragment == null) { mListFragment = lf; } } 

最后在activityCreate()中:

 if (savedInstanceState != null) { if (mListFragment != null) mListFragment.setListItems(items); } 

这种方法将实际可见的片段附加到活动,而不创build一个新的。

只要继续尝试这个代码,

 public class MYFragmentPAdp extends FragmentPagerAdapter { public MYFragmentPAdp(FragmentManager fm) { super(fm); } @Override public int getCount() { return 2; } @Override public Fragment getItem(int position) { if (position == 0) Fragment fragment = new Fragment1(); else (position == 1) Fragment fragment = new Fragment2(); return fragment; } }