作为ViewPager的一部分,在ListFragment中更新数据

141

我正在使用 Android 中的 v4 兼容性 ViewPager。我的 FragmentActivity 有一堆数据,需要在 ViewPager 的不同页面上以不同的方式显示。到目前为止,我只有三个相同 ListFragment 的实例,但将来会有三个不同 ListFragment 的实例。ViewPager 在垂直手机屏幕上,列表并不是并排的。

现在,ListFragment 上的一个按钮通过 FragmentActivity 启动一个单独的全屏活动,然后该活动返回并修改数据,保存数据,然后尝试更新其 ViewPager 中的所有视图。在这里,我卡住了。

public class ProgressMainActivity extends FragmentActivity
{
    MyAdapter mAdapter;
    ViewPager mPager;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
    ...
        mAdapter = new MyAdapter(getSupportFragmentManager());

        mPager = (ViewPager) findViewById(R.id.viewpager);
        mPager.setAdapter(mAdapter);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        ...
        updateFragments();
        ...
    }
    public void updateFragments()
    {
        //Attempt 1:
        //mAdapter.notifyDataSetChanged();
        //mPager.setAdapter(mAdapter);

        //Attempt 2:
        //HomeListFragment fragment = (HomeListFragment) getSupportFragmentManager().findFragmentById(mAdapter.fragId[0]);
        //fragment.updateDisplay();
    }

    public static class MyAdapter extends FragmentPagerAdapter implements
         TitleProvider
    {
      int[] fragId = {0,0,0,0,0};
      public MyAdapter(FragmentManager fm)
      {
         super(fm);
      }
      @Override
      public String getTitle(int position){
         return titles[position];
      }
      @Override
      public int getCount(){
         return titles.length;
      }

      @Override
      public Fragment getItem(int position)
      {

         Fragment frag = HomeListFragment.newInstance(position);
         //Attempt 2:
         //fragId[position] = frag.getId();
         return frag;
      }

      @Override
      public int getItemPosition(Object object) {
         return POSITION_NONE; //To make notifyDataSetChanged() do something
     }
   }

    public class HomeListFragment extends ListFragment
    {
    ...
        public static HomeListFragment newInstance(int num)
        {
            HomeListFragment f = new HomeListFragment();
            ...
            return f;
        }
   ...
现在你可以看到,我的第一次尝试是在整个FragmentPagerAdapter上调用notifyDataSetChanged,这有时会更新数据,但其他时候我会收到一个IllegalStateException: Can not perform this action after onSaveInstanceState。
我的第二次尝试是尝试在ListFragment中调用一个更新函数,但是getItem中的getId返回0。根据文档,我尝试通过从FragmentManager获取对Fragment的引用来解决此问题,使用findFragmentById()或findFragmentByTag(),但我不知道Fragment的标签或ID!我在ViewPager中有一个android:id="@+id/viewpager",在ListFragment布局中有一个android:id="@android:id/list"用于我的ListView,但我认为这些都没有用处。
那么,我应该如何: a)一次安全地更新整个ViewPager(理想情况下将用户返回到之前所在的页面)- 用户可以看到视图更改,这是可以接受的。 或者更好的选择是, b)调用每个受影响的ListFragment中的函数以手动更新ListView。
非常感谢任何帮助!
10个回答

257
Barkside的答案适用于FragmentPagerAdapter,但不适用于FragmentStatePagerAdapter,因为它没有在传递给FragmentManager的片段上设置标签。
使用FragmentStatePagerAdapter似乎可以通过其instantiateItem(ViewGroup container, int position)方法来解决问题。它返回位置position处的片段的引用。如果FragmentStatePagerAdapter已经持有所需的片段引用,则instantiateItem只返回该片段的引用,并且不调用getItem()方法再次实例化它。
因此,假设我当前正在查看第50个片段,并想要访问第49个片段。由于它们非常接近,很可能#49已经被实例化,因此,
ViewPager pager = findViewById(R.id.viewpager);
FragmentStatePagerAdapter a = (FragmentStatePagerAdapter) pager.getAdapter();
MyFragment f49 = (MyFragment) a.instantiateItem(pager, 49)

5
我会尽量让翻译更加通俗易懂,但不改变原意。我会简化代码,只需要将"a"设为PagerAdapter(PagerAdapter a = pager.getAdapter();)。甚至可以这样写:MyFragment f49 = (MyFragment) pager.getAdapter().instantiateItem(pager, 49); instantiateItem() 是PagerAdapter里的方法,所以调用时不需要进行类型转换。 - Dandre Allison
13
如果有人想知道,ViewPager确实会跟踪查看的片段的索引(ViewPager.getCurrentItem()),这在上面的示例中是49...但我不得不说,我很惊讶ViewPager API不会直接返回对实际碎片的引用。 - mblackwell8
6
使用 FragmentStatePagerAdapter,按照 @mblackwell8 的建议,使用上面的代码,即 a.instantiateItem(viewPager, viewPager.getCurrentItem()); (来消除 49),可以完美地运行。 - Cedric Simon
1
等等,所以我必须将ViewPager传递给它包含的每个片段,才能对片段中的视图进行操作?目前,在当前页面的onCreate中执行的所有操作都会在下一页上发生。这听起来非常不可靠,可能会导致泄漏问题。 - G_V
重要的是要提到,任何对instantiateItem的调用都应该被startUpdate/finishUpdate调用所包围。详细信息请参见我对类似问题的回答:https://dev59.com/G2Yr5IYBdhLWcg3wAFg0#41345283 - morgwai

154

好的,我想我找到了一种方法来执行我自己问题中的请求b),所以我会分享给其他人受益。在ViewPager内部的片段的标记形式为"android:switcher:VIEWPAGER_ID:INDEX",其中VIEWPAGER_ID是XML布局中的R.id.viewpager,而INDEX是在viewpager中的位置。因此,如果已知位置(例如0),我可以在updateFragments()中执行:

      HomeListFragment fragment = 
          (HomeListFragment) getSupportFragmentManager().findFragmentByTag(
                       "android:switcher:"+R.id.viewpager+":0");
      if(fragment != null)  // could be null if not instantiated yet
      {
         if(fragment.getView() != null) 
         {
            // no need to call if fragment's onDestroyView() 
            //has since been called.
            fragment.updateDisplay(); // do what updates are required
         }
      }

我不确定这是一种有效的方法,但在有更好建议之前,它可以使用。


4
我登录只是为了给你的回答和问题点赞。 - SuitUp
5
既然这不在官方文档中,我就不会使用它。如果他们在未来的版本更改了标签,那该怎么办? - Thierry Roy
4
FragmentPagerAdapter含有一个静态方法makeFragmentName,用于生成标签,您可以使用该标签来替代稍微有些hacky的方法:HomeListFragment fragment = (HomeListFragment) getSupportFragmentManager().findFragmentByTag(FragmentPagerAdapter.makeFragmentName(R.id.viewpager, 0)); - dgmltn
8
@dgmltn,makeFragmentName 是一个 private static 方法,因此您无法访问它。 - StenaviN
1
这个可真是太棒了!我只能祈祷它一直有效。 - Bogdan Alexandru
显示剩余5条评论

58
尝试在每次实例化Fragment时记录标签。
public class MPagerAdapter extends FragmentPagerAdapter {
    private Map<Integer, String> mFragmentTags;
    private FragmentManager mFragmentManager;

    public MPagerAdapter(FragmentManager fm) {
        super(fm);
        mFragmentManager = fm;
        mFragmentTags = new HashMap<Integer, String>();
    }

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

    @Override
    public Fragment getItem(int position) {
        return Fragment.instantiate(mContext, AFragment.class.getName(), null);
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Object obj = super.instantiateItem(container, position);
        if (obj instanceof Fragment) {
            // record the fragment tag here.
            Fragment f = (Fragment) obj;
            String tag = f.getTag();
            mFragmentTags.put(position, tag);
        }
        return obj;
    }

    public Fragment getFragment(int position) {
        String tag = mFragmentTags.get(position);
        if (tag == null)
            return null;
        return mFragmentManager.findFragmentByTag(tag);
    }
}

1
它对我有效,我在几个项目中使用过它。你可以尝试阅读FragmentPagerAdapter的源代码并打印一些日志进行调试。 - faylon
对我来说非常有效,我很困惑为什么比起依赖于makeFragmentName()内部实现的解决方案,接受这个解决方案的人要少得多。 - Trevor
5
它适用于FragmentPagerAdaper,但对于FragmentStatePagerAdaper失败了(Fragment.getTag()始终返回null)。 - Engine Bai
你能评论一下 getItem 吗?我认为 getItem 是由 instantiateItem 调用的。在这种情况下,position 如何使用? - Antonio Sesto
1
我尝试了这个解决方案,创建了一个更新片段数据的方法。但现在我无法看到片段的视图。我可以看到数据传递到了片段,但视图不可见。有什么帮助吗? - Arun Badole
显示剩余3条评论

35
如果您问我,下面页面上的第二种解决方案——跟踪所有“活动”的碎片页面是更好的选择:http://tamsler.blogspot.nl/2011/11/android-viewpager-and-fragments-part-ii.html barkside给出的答案对我来说太繁琐了。
你需要跟踪所有“活动”的碎片页面。在这种情况下,您需要在FragmentStatePagerAdapter中跟踪碎片页面,该适配器由ViewPager使用。
private final SparseArray<Fragment> mPageReferences = new SparseArray<Fragment>();

public Fragment getItem(int index) {
    Fragment myFragment = MyFragment.newInstance();
    mPageReferences.put(index, myFragment);
    return myFragment;
}

为了避免对“非活动”片段页面的引用,我们需要实现FragmentStatePagerAdapter的destroyItem(...)方法:
public void destroyItem(View container, int position, Object object) {
    super.destroyItem(container, position, object);
    mPageReferences.remove(position);
}

...当你需要访问当前可见页面时,可以调用以下代码:

int index = mViewPager.getCurrentItem();
MyAdapter adapter = ((MyAdapter)mViewPager.getAdapter());
MyFragment fragment = adapter.getFragment(index);

在这里,MyAdapter的getFragment(int)方法看起来像这样:

public MyFragment getFragment(int key) {
    return mPageReferences.get(key);
}

"


@Frank 链接中的第二种解决方案很简单...!谢谢.. :) - TheFlash
3
最好使用SparseArray而不是Map。 - GaRRaPeTa
我更新了我的答案,所以现在它使用SparseArray。如果你的目标小于11,则应该使用SparseArrayCompat,因为这个类在版本11中被更新了。 - Frank
2
这将在生命周期事件上中断。请参见https://dev59.com/vGoy5IYBdhLWcg3wFqFT#24662049。 - Dori

13

好的,经过测试,使用@barkside上面的方法后,我无法使其在我的应用程序中工作。然后我想起了IOSched2012应用程序也使用了一个viewpager,而这就是我找到解决方案的地方。它不使用任何片段ID或标签,因为这些在viewpager中并不容易被访问。

这是IOSched应用程序HomeActivity中的重要部分。请特别注意注释,因为其中包含关键点:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    // Since the pager fragments don't have known tags or IDs, the only way to persist the
    // reference is to use putFragment/getFragment. Remember, we're not persisting the exact
    // Fragment instance. This mechanism simply gives us a way to persist access to the
    // 'current' fragment instance for the given fragment (which changes across orientation
    // changes).
    //
    // The outcome of all this is that the "Refresh" menu button refreshes the stream across
    // orientation changes.
    if (mSocialStreamFragment != null) {
        getSupportFragmentManager().putFragment(outState, "stream_fragment",
                mSocialStreamFragment);
    }
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    if (mSocialStreamFragment == null) {
        mSocialStreamFragment = (SocialStreamFragment) getSupportFragmentManager()
                .getFragment(savedInstanceState, "stream_fragment");
    }
}

将您的Fragments实例存储在FragmentPagerAdapter中,如下所示:

    private class HomePagerAdapter extends FragmentPagerAdapter {
    public HomePagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        switch (position) {
            case 0:
                return (mMyScheduleFragment = new MyScheduleFragment());

            case 1:
                return (mExploreFragment = new ExploreFragment());

            case 2:
                return (mSocialStreamFragment = new SocialStreamFragment());
        }
        return null;
    }

另外,请记得保护您的Fragment调用,像这样:

    if (mSocialStreamFragment != null) {
        mSocialStreamFragment.refresh();
    }

Ryan,这是否意味着您正在覆盖ViewPager使用的函数来“保留”您的片段实例并将它们设置为可访问的公共变量?为什么在恢复实例时您有if(mSocialStreamFragment == null){? - Thomas Clowes
1
这是一个很好的答案,+1 为 io12 应用程序引用 - 不确定由于生命周期更改后父项重新创建后未调用 getItem() 是否会跨越生命周期更改而起作用。请参见 https://dev59.com/vGoy5IYBdhLWcg3wFqFT#24662049。在示例中,对于一个片段部分受到保护,但对于其他两个片段则没有。 - Dori

2
你可以复制 FragmentPagerAdapter 并修改一些源代码,添加 getTag() 方法。
例如:
public abstract class AppFragmentPagerAdapter extends PagerAdapter {
private static final String TAG = "FragmentPagerAdapter";
private static final boolean DEBUG = false;

private final FragmentManager mFragmentManager;
private FragmentTransaction mCurTransaction = null;
private Fragment mCurrentPrimaryItem = null;

public AppFragmentPagerAdapter(FragmentManager fm) {
    mFragmentManager = fm;
}


public abstract Fragment getItem(int position);

@Override
public void startUpdate(ViewGroup container) {
}

@Override
public Object instantiateItem(ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    final long itemId = getItemId(position);


    String name = getTag(position);
    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);

        mCurTransaction.add(container.getId(), fragment,
                getTag(position));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
            + " v=" + ((Fragment) object).getView());
    mCurTransaction.detach((Fragment) object);
}

@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
    Fragment fragment = (Fragment) object;
    if (fragment != mCurrentPrimaryItem) {
        if (mCurrentPrimaryItem != null) {
            mCurrentPrimaryItem.setMenuVisibility(false);
            mCurrentPrimaryItem.setUserVisibleHint(false);
        }
        if (fragment != null) {
            fragment.setMenuVisibility(true);
            fragment.setUserVisibleHint(true);
        }
        mCurrentPrimaryItem = fragment;
    }
}

@Override
public void finishUpdate(ViewGroup container) {
    if (mCurTransaction != null) {
        mCurTransaction.commitAllowingStateLoss();
        mCurTransaction = null;
        mFragmentManager.executePendingTransactions();
    }
}

@Override
public boolean isViewFromObject(View view, Object object) {
    return ((Fragment) object).getView() == view;
}

@Override
public Parcelable saveState() {
    return null;
}

@Override
public void restoreState(Parcelable state, ClassLoader loader) {
}


public long getItemId(int position) {
    return position;
}

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

protected abstract String getTag(int position);
}

然后扩展它,覆盖这些抽象方法,不需要担心Android Group在未来更改。未来的FragmentPageAdapter源代码。
 class TimeLinePagerAdapter extends AppFragmentPagerAdapter {


    List<Fragment> list = new ArrayList<Fragment>();


    public TimeLinePagerAdapter(FragmentManager fm) {
        super(fm);
        list.add(new FriendsTimeLineFragment());
        list.add(new MentionsTimeLineFragment());
        list.add(new CommentsTimeLineFragment());
    }


    public Fragment getItem(int position) {
        return list.get(position);
    }

    @Override
    protected String getTag(int position) {
        List<String> tagList = new ArrayList<String>();
        tagList.add(FriendsTimeLineFragment.class.getName());
        tagList.add(MentionsTimeLineFragment.class.getName());
        tagList.add(CommentsTimeLineFragment.class.getName());
        return tagList.get(position);
    }


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


}

1

或者你可以像这样覆盖FragmentPagerAdaptersetPrimaryItem方法:

public void setPrimaryItem(ViewGroup container, int position, Object object) { 
    if (mCurrentFragment != object) {
        mCurrentFragment = (Fragment) object; //Keep reference to object
        ((MyInterface)mCurrentFragment).viewDidAppear();//Or call a method on the fragment
    }

    super.setPrimaryItem(container, position, object);
}

public Fragment getCurrentFragment(){
    return mCurrentFragment;
}

1

也可以顺利运行:

在页面片段的布局中的某个位置:

<FrameLayout android:layout_width="0dp" android:layout_height="0dp" android:visibility="gone" android:id="@+id/fragment_reference">
     <View android:layout_width="0dp" android:layout_height="0dp" android:visibility="gone"/>
</FrameLayout>

在片段的onCreateView()方法中:
...
View root = inflater.inflate(R.layout.fragment_page, container, false);
ViewGroup ref = (ViewGroup)root.findViewById(R.id.fragment_reference);
ref.setTag(this);
ref.getChildAt(0).setTag("fragment:" + pageIndex);
return root;

如果存在的话,从ViewPager返回Fragment的方法:
public Fragment getFragment(int pageIndex) {        
        View w = mViewPager.findViewWithTag("fragment:" + pageIndex);
        if (w == null) return null;
        View r = (View) w.getParent();
        return (Fragment) r.getTag();
}

0

这是我的解决方案,因为我不需要跟踪我的标签页并且需要刷新它们所有。

    Bundle b = new Bundle();
    b.putInt(Constants.SharedPreferenceKeys.NUM_QUERY_DAYS,numQueryDays);
    for(android.support.v4.app.Fragment f:getSupportFragmentManager().getFragments()){
        if(f instanceof HomeTermFragment){
            ((HomeTermFragment) f).restartLoader(b);
        }
    }

0
我想分享我的方法,如果有人需要的话可以参考一下:
这是我的分页适配器:
 public class CustomPagerAdapter extends FragmentStatePagerAdapter{
    private Fragment[] fragments;

    public CustomPagerAdapter(FragmentManager fm) {
        super(fm);
        fragments = new Fragment[]{
                new FragmentA(),
                new FragmentB()
        };
    }

    @Override
    public Fragment getItem(int arg0) {
        return fragments[arg0];
    }

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

}

在我的活动中,我有:

public class MainActivity {
        private ViewPager view_pager;
        private CustomPagerAdapter adapter;


        @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        adapter = new CustomPagerAdapter(getSupportFragmentManager());
        view_pager = (ViewPager) findViewById(R.id.pager);
        view_pager.setAdapter(adapter);
        view_pager.setOnPageChangeListener(this);
          ...
         }

}

然后获取当前片段的方法是:

int index = view_pager.getCurrentItem();
Fragment currentFragment = adapter.getItem(index);

1
这会在活动/片段的生命周期方法之后出错。 - Dori

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