在ViewPager中替换Fragment

278
我正在尝试使用Fragment和ViewPager,使用FragmentPagerAdapter。我想要实现的是将位于ViewPager第一页上的一个片段替换为另一个片段。
这个ViewPager由两个页面组成。第一个页面是FirstPagerFragment,第二个页面是SecondPagerFragment。在第一个页面上点击按钮时,我想用NextFragment替换FirstPagerFragment。
以下是我的代码。
public class FragmentPagerActivity extends FragmentActivity {

    static final int NUM_ITEMS = 2;

    MyAdapter mAdapter;
    ViewPager mPager;

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

        mAdapter = new MyAdapter(getSupportFragmentManager());

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

    }


    /**
     * Pager Adapter
     */
    public static class MyAdapter extends FragmentPagerAdapter {
        public MyAdapter(FragmentManager fm) {
            super(fm);
        }

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

        @Override
        public Fragment getItem(int position) {

            if(position == 0) {
                return FirstPageFragment.newInstance();
            } else {
                return SecondPageFragment.newInstance();
            }

        }
    }


    /**
     * Second Page FRAGMENT
     */
    public static class SecondPageFragment extends Fragment {

        public static SecondPageFragment newInstance() {
            SecondPageFragment f = new SecondPageFragment();
            return f;
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            //Log.d("DEBUG", "onCreateView");
            return inflater.inflate(R.layout.second, container, false);

        }
    }

    /**
     * FIRST PAGE FRAGMENT
     */
    public static class FirstPageFragment extends Fragment {

        Button button;

        public static FirstPageFragment newInstance() {
            FirstPageFragment f = new FirstPageFragment();
            return f;
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            //Log.d("DEBUG", "onCreateView");
            View root = inflater.inflate(R.layout.first, container, false);
            button = (Button) root.findViewById(R.id.button);
            button.setOnClickListener(new OnClickListener() {

                @Override
                public void onClick(View v) {
                    FragmentTransaction trans = getFragmentManager().beginTransaction();
                                    trans.replace(R.id.first_fragment_root_id, NextFragment.newInstance());
                    trans.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
                    trans.addToBackStack(null);
                    trans.commit();

                }

            });

            return root;
        }

        /**
     * Next Page FRAGMENT in the First Page
     */
    public static class NextFragment extends Fragment {

        public static NextFragment newInstance() {
            NextFragment f = new NextFragment();
            return f;
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            //Log.d("DEBUG", "onCreateView");
            return inflater.inflate(R.layout.next, container, false);

        }
    }
}

......并且这里是xml文件

fragment_pager.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical" android:padding="4dip"
        android:gravity="center_horizontal"
        android:layout_width="match_parent" android:layout_height="match_parent">

    <android.support.v4.view.ViewPager
            android:id="@+id/pager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1">
    </android.support.v4.view.ViewPager>

</LinearLayout>

first.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/first_fragment_root_id"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <Button android:id="@+id/button"
     android:layout_width="wrap_content" android:layout_height="wrap_content"
     android:text="to next"/>

</LinearLayout>

现在问题是...我应该使用哪个ID

trans.replace(R.id.first_fragment_root_id, NextFragment.newInstance());

如果我使用R.id.first_fragment_root_id,替换工作正常,但Hierarchy Viewer表现出奇怪的行为,如下所示。

开始时情况是

替换后情况变成了

可以看到有些问题,我期望在替换片段后找到与第一张图片中显示相同的状态。


你是通过某种方式生成了那个布局图还是手动完成的? - Jeroen Vannevel
@Noodles:我也需要做同样的事情。你的问题引发了一个很长的帖子,有许多不同的答案。最终哪个答案效果最好?非常感谢! - narb
1
@JeroenVannevel 你可能已经弄明白了,但那个布局图是 https://developer.android.com/tools/performance/hierarchy-viewer/index.html - Ankit Popli
遇到了一些问题,需要您的帮助。http://stackoverflow.com/questions/40149039/viewpager-recyclerview-issue - albert
18个回答

167

有另一种解决方案,不需要修改 ViewPagerFragmentStatePagerAdapter 的源代码,而且它可以与作者使用的 FragmentPagerAdapter 基类一起使用。

首先,我想回答作者关于应该使用哪个 ID 的问题;它是容器的 ID,即视图页面本身的 ID。但是,正如您可能自己注意到的那样,在您的代码中使用该 ID 不会发生任何事情。我将解释原因:

首先,要使 ViewPager 重新装载页面,您需要调用适配器基类中的 notifyDataSetChanged()

其次,ViewPager 使用抽象方法 getItemPosition() 来检查哪些页面应该被销毁,哪些应该被保留。该函数的默认实现始终返回 POSITION_UNCHANGED,这导致 ViewPager 保留所有当前页面,并因此不附加您的新页面。因此,为使片段替换工作,必须在适配器中重写 getItemPosition() 并在调用旧片段时返回 POSITION_NONE,以便隐藏它。

这也意味着您的适配器始终需要知道应在位置0显示哪个片段,FirstPageFragment 还是 NextFragment。一种做法是在创建 FirstPageFragment 时提供一个监听器,在切换片段时将调用它。我认为这是一个好方法,可以让您的片段适配器处理所有片段切换和对 ViewPagerFragmentManager 的调用。

第三,FragmentPagerAdapter 根据位置派生的名称缓存所使用的片段,因此,如果在位置0处有一个片段,则即使类是新的,也不会被替换。有两种解决方案,但最简单的方法是使用 FragmentTransactionremove() 函数,该函数也将删除其标记。

那是一大堆文本,下面是代码,应该适用于您的情况:

public class MyAdapter extends FragmentPagerAdapter
{
    static final int NUM_ITEMS = 2;
    private final FragmentManager mFragmentManager;
    private Fragment mFragmentAtPos0;

    public MyAdapter(FragmentManager fm)
    {
        super(fm);
        mFragmentManager = fm;
    }

    @Override
    public Fragment getItem(int position)
    {
        if (position == 0)
        {
            if (mFragmentAtPos0 == null)
            {
                mFragmentAtPos0 = FirstPageFragment.newInstance(new FirstPageFragmentListener()
                {
                    public void onSwitchToNextFragment()
                    {
                        mFragmentManager.beginTransaction().remove(mFragmentAtPos0).commit();
                        mFragmentAtPos0 = NextFragment.newInstance();
                        notifyDataSetChanged();
                    }
                });
            }
            return mFragmentAtPos0;
        }
        else
            return SecondPageFragment.newInstance();
    }

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

    @Override
    public int getItemPosition(Object object)
    {
        if (object instanceof FirstPageFragment && mFragmentAtPos0 instanceof NextFragment)
            return POSITION_NONE;
        return POSITION_UNCHANGED;
    }
}

public interface FirstPageFragmentListener
{
    void onSwitchToNextFragment();
}

24
对我也起作用,但如何实现返回按钮以显示第一个片段? - user1159819
7
很好的解释,但您能否发布完整的源代码,尤其是FirstPageFragment和SecondPageFragment类。 - georgiecasey
6
在newInstance()中如何向Bundle添加FirstPageFragmentListener? - miguel
4
你能否添加代码来处理FirstPageFragment.newInstance()的监听器参数? - MiguelHincapieC
8
仅当我使用commitNow()而非commit()来移除片段时,这才对我起作用。不确定为什么我的使用情况不同,但希望这能节省其他人的时间。 - Ian Lovejoy
显示剩余14条评论

37
截至2012年11月13日,在ViewPager中替换片段似乎变得更加容易了。Google发布了支持嵌套片段的Android 4.2,并且在新的Android Support Library v11中也受到支持,因此这将适用于1.6版本及以上的所有设备。
它与替换片段的常规方式非常相似,只是要使用getChildFragmentManager。它似乎可以工作,但是当用户点击“返回”按钮时,嵌套片段回退栈不会弹出。根据链接问题中的解决方案,您需要在片段的子管理器上手动调用popBackStackImmediate()。因此,您需要覆盖ViewPager活动的onBackPressed()方法,在其中获取ViewPager的当前片段并调用getChildFragmentManager().popBackStackImmediate()。
获取当前显示的片段有些棘手,我使用了这个dirty "android:switcher:VIEWPAGER_ID:INDEX" solution解决方案,但你也可以像第二个解决方案on this page中所解释的那样跟踪ViewPager自己的所有片段。
因此,这是我的代码,用于具有4个ListView的ViewPager,当用户单击行时,在ViewPager中显示详细视图,并使返回按钮工作。为了简洁起见,我尝试只包含相关代码,如果您想要完整的应用程序上传到GitHub,请留下评论。

HomeActivity.java

 public class HomeActivity extends SherlockFragmentActivity {
FragmentAdapter mAdapter;
ViewPager mPager;
TabPageIndicator mIndicator;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    mAdapter = new FragmentAdapter(getSupportFragmentManager());
    mPager = (ViewPager)findViewById(R.id.pager);
    mPager.setAdapter(mAdapter);
    mIndicator = (TabPageIndicator)findViewById(R.id.indicator);
    mIndicator.setViewPager(mPager);
}

// This the important bit to make sure the back button works when you're nesting fragments. Very hacky, all it takes is some Google engineer to change that ViewPager view tag to break this in a future Android update.
@Override
public void onBackPressed() {
    Fragment fragment = (Fragment) getSupportFragmentManager().findFragmentByTag("android:switcher:" + R.id.pager + ":"+mPager.getCurrentItem());
    if (fragment != null) // could be null if not instantiated yet
    {
        if (fragment.getView() != null) {
            // Pop the backstack on the ChildManager if there is any. If not, close this activity as normal.
            if (!fragment.getChildFragmentManager().popBackStackImmediate()) {
                finish();
            }
        }
    }
}

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

    @Override
    public Fragment getItem(int position) {
        switch (position) {
        case 0:
            return ListProductsFragment.newInstance();
        case 1:
            return ListActiveSubstancesFragment.newInstance();
        case 2:
            return ListProductFunctionsFragment.newInstance();
        case 3:
            return ListCropsFragment.newInstance();
        default:
            return null;
        }
    }

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

 }
}

ListProductsFragment.java

public class ListProductsFragment extends SherlockFragment {
private ListView list;

public static ListProductsFragment newInstance() {
    ListProductsFragment f = new ListProductsFragment();
    return f;
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View V = inflater.inflate(R.layout.list, container, false);
    list = (ListView)V.findViewById(android.R.id.list);
    list.setOnItemClickListener(new OnItemClickListener() {
        public void onItemClick(AdapterView<?> parent, View view,
            int position, long id) {
          // This is important bit
          Fragment productDetailFragment = FragmentProductDetail.newInstance();
          FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
          transaction.addToBackStack(null);
          transaction.replace(R.id.products_list_linear, productDetailFragment).commit();
        }
      });       
    return V;
}
}

7
您能否将您的代码上传至GitHub?谢谢。 - Gautham
4
@georgiecasey说:我下载了API 17,即Android 4.2,这使我能够开始使用getChildFragmentManager()。但是,我可能错过了你的解决方案中的某些内容,因为现在屏幕上出现了重叠的旧和新片段。注意:我正在尝试将您的解决方案与来自http://developer.android.com/reference/android/support/v4/view/ViewPager.html的Google示例TabsAdapter代码一起使用。感谢任何建议和/或参考代码。 - gcl1
28
R.id.products_list_linear是什么意思?请提供您提供的代码链接。(注意:这里需要您提供链接,我会将其翻译成中文) - howettl
1
@howettl ID 引用了外部片段成员。解决这个问题的关键是使用嵌套片段(这样你就不必操作 viewpager 适配器)。 - Indrek Kõue
2
这个项目最终有没有上传到Github上? - Baruch
显示剩余6条评论

31

根据@wize的答案,我发现它很有帮助和优雅,但我只能部分地实现我想要的,因为我想要在替换后返回到第一个碎片。通过修改他的代码,我成功地实现了它。

这是FragmentPagerAdapter:

public static class MyAdapter extends FragmentPagerAdapter {
    private final class CalendarPageListener implements
            CalendarPageFragmentListener {
        public void onSwitchToNextFragment() {
            mFragmentManager.beginTransaction().remove(mFragmentAtPos0)
                    .commit();
            if (mFragmentAtPos0 instanceof FirstFragment){
                mFragmentAtPos0 = NextFragment.newInstance(listener);
            }else{ // Instance of NextFragment
                mFragmentAtPos0 = FirstFragment.newInstance(listener);
            }
            notifyDataSetChanged();
        }
    }

    CalendarPageListener listener = new CalendarPageListener();;
    private Fragment mFragmentAtPos0;
    private FragmentManager mFragmentManager;

    public MyAdapter(FragmentManager fm) {
        super(fm);
        mFragmentManager = fm;
    }

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

    @Override
    public int getItemPosition(Object object) {
        if (object instanceof FirstFragment && mFragmentAtPos0 instanceof NextFragment)
            return POSITION_NONE;
        if (object instanceof NextFragment && mFragmentAtPos0 instanceof FirstFragment)
            return POSITION_NONE;
        return POSITION_UNCHANGED;
    }

    @Override
    public Fragment getItem(int position) {
        if (position == 0)
            return Portada.newInstance();
        if (position == 1) { // Position where you want to replace fragments
            if (mFragmentAtPos0 == null) {
                mFragmentAtPos0 = FirstFragment.newInstance(listener);
            }
            return mFragmentAtPos0;
        }
        if (position == 2)
            return Clasificacion.newInstance();
        if (position == 3)
            return Informacion.newInstance();

        return null;
    }
}

public interface CalendarPageFragmentListener {
    void onSwitchToNextFragment();
}

要执行替换,只需定义一个静态字段,类型为CalendarPageFragmentListener并通过相应片段的newInstance方法进行初始化,并分别调用FirstFragment.pageListener.onSwitchToNextFragment()NextFragment.pageListener.onSwitchToNextFragment()


你如何执行替换?如果不重写onSwitchToNext方法,就无法初始化Listener。 - Leandros
1
如果您旋转设备,是否会丢失保存在mFragmentAtPos0中的状态? - seb
@seb,我实际上改进了这个解决方案,以便在保存活动状态时保存mFragmentAtPos0引用。这不是最优雅的解决方案,但它有效。 - mdelolmo
您可以查看我的答案。它替换片段并返回到第一个片段。 - Lyd
@ Ian Lovejoy,使用commitNow()可以节省我的时间,commit()对我无效! - crazygit
显示剩余2条评论

21

我已经实现了以下解决方案:

  • 在选项卡内动态替换碎片
  • 每个选项卡维护历史记录
  • 处理方向变化

实现这些技巧的方法如下:

  • 使用notifyDataSetChanged()方法应用碎片替换
  • 仅将Fragment管理器用于后台,而不是碎片替换
  • 使用备忘录模式(堆栈)维护历史记录

适配器代码如下:

public class TabsAdapter extends FragmentStatePagerAdapter implements ActionBar.TabListener, ViewPager.OnPageChangeListener {

/** The sherlock fragment activity. */
private final SherlockFragmentActivity mActivity;

/** The action bar. */
private final ActionBar mActionBar;

/** The pager. */
private final ViewPager mPager;

/** The tabs. */
private List<TabInfo> mTabs = new LinkedList<TabInfo>();

/** The total number of tabs. */
private int TOTAL_TABS;

private Map<Integer, Stack<TabInfo>> history = new HashMap<Integer, Stack<TabInfo>>();

/**
 * Creates a new instance.
 *
 * @param activity the activity
 * @param pager    the pager
 */
public TabsAdapter(SherlockFragmentActivity activity, ViewPager pager) {
    super(activity.getSupportFragmentManager());
    activity.getSupportFragmentManager();
    this.mActivity = activity;
    this.mActionBar = activity.getSupportActionBar();
    this.mPager = pager;
    mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
}

/**
 * Adds the tab.
 *
 * @param image         the image
 * @param fragmentClass the class
 * @param args          the arguments
 */
public void addTab(final Drawable image, final Class fragmentClass, final Bundle args) {
    final TabInfo tabInfo = new TabInfo(fragmentClass, args);
    final ActionBar.Tab tab = mActionBar.newTab();
    tab.setTabListener(this);
    tab.setTag(tabInfo);
    tab.setIcon(image);

    mTabs.add(tabInfo);
    mActionBar.addTab(tab);

    notifyDataSetChanged();
}

@Override
public Fragment getItem(final int position) {
    final TabInfo tabInfo = mTabs.get(position);
    return Fragment.instantiate(mActivity, tabInfo.fragmentClass.getName(), tabInfo.args);
}

@Override
public int getItemPosition(final Object object) {
    /* Get the current position. */
    int position = mActionBar.getSelectedTab().getPosition();

    /* The default value. */
    int pos = POSITION_NONE;
    if (history.get(position).isEmpty()) {
        return POSITION_NONE;
    }

    /* Checks if the object exists in current history. */
    for (Stack<TabInfo> stack : history.values()) {
        TabInfo c = stack.peek();
        if (c.fragmentClass.getName().equals(object.getClass().getName())) {
            pos = POSITION_UNCHANGED;
            break;
        }
    }
    return pos;
}

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

@Override
public void onPageScrollStateChanged(int arg0) {
}

@Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
}

@Override
public void onPageSelected(int position) {
    mActionBar.setSelectedNavigationItem(position);
}

@Override
public void onTabSelected(final ActionBar.Tab tab, final FragmentTransaction ft) {
    TabInfo tabInfo = (TabInfo) tab.getTag();
    for (int i = 0; i < mTabs.size(); i++) {
        if (mTabs.get(i).equals(tabInfo)) {
            mPager.setCurrentItem(i);
        }
    }
}

@Override
public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) {
}

@Override
public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) {
}

public void replace(final int position, final Class fragmentClass, final Bundle args) {
    /* Save the fragment to the history. */
    mActivity.getSupportFragmentManager().beginTransaction().addToBackStack(null).commit();

    /* Update the tabs. */
    updateTabs(new TabInfo(fragmentClass, args), position);

    /* Updates the history. */
    history.get(position).push(new TabInfo(mTabs.get(position).fragmentClass, mTabs.get(position).args));

    notifyDataSetChanged();
}

/**
 * Updates the tabs.
 *
 * @param tabInfo
 *          the new tab info
 * @param position
 *          the position
 */
private void updateTabs(final TabInfo tabInfo, final int position) {
    mTabs.remove(position);
    mTabs.add(position, tabInfo);
    mActionBar.getTabAt(position).setTag(tabInfo);
}

/**
 * Creates the history using the current state.
 */
public void createHistory() {
    int position = 0;
    TOTAL_TABS = mTabs.size();
    for (TabInfo mTab : mTabs) {
        if (history.get(position) == null) {
            history.put(position, new Stack<TabInfo>());
        }
        history.get(position).push(new TabInfo(mTab.fragmentClass, mTab.args));
        position++;
    }
}

/**
 * Called on back
 */
public void back() {
    int position = mActionBar.getSelectedTab().getPosition();
    if (!historyIsEmpty(position)) {
        /* In case there is not any other item in the history, then finalize the activity. */
        if (isLastItemInHistory(position)) {
            mActivity.finish();
        }
        final TabInfo currentTabInfo = getPrevious(position);
        mTabs.clear();
        for (int i = 0; i < TOTAL_TABS; i++) {
            if (i == position) {
                mTabs.add(new TabInfo(currentTabInfo.fragmentClass, currentTabInfo.args));
            } else {
                TabInfo otherTabInfo = history.get(i).peek();
                mTabs.add(new TabInfo(otherTabInfo.fragmentClass, otherTabInfo.args));
            }
        }
    }
    mActionBar.selectTab(mActionBar.getTabAt(position));
    notifyDataSetChanged();
}

/**
 * Returns if the history is empty.
 *
 * @param position
 *          the position
 * @return  the flag if empty
 */
private boolean historyIsEmpty(final int position) {
    return history == null || history.isEmpty() || history.get(position).isEmpty();
}

private boolean isLastItemInHistory(final int position) {
    return history.get(position).size() == 1;
}

/**
 * Returns the previous state by the position provided.
 *
 * @param position
 *          the position
 * @return  the tab info
 */
private TabInfo getPrevious(final int position) {
    TabInfo currentTabInfo = history.get(position).pop();
    if (!history.get(position).isEmpty()) {
        currentTabInfo = history.get(position).peek();
    }
    return currentTabInfo;
}

/** The tab info class */
private static class TabInfo {

    /** The fragment class. */
    public Class fragmentClass;

    /** The args.*/
    public Bundle args;

    /**
     * Creates a new instance.
     *
     * @param fragmentClass
     *          the fragment class
     * @param args
     *          the args
     */
    public TabInfo(Class fragmentClass, Bundle args) {
        this.fragmentClass = fragmentClass;
        this.args = args;
    }

    @Override
    public boolean equals(final Object o) {
        return this.fragmentClass.getName().equals(o.getClass().getName());
    }

    @Override
    public int hashCode() {
        return fragmentClass.getName() != null ? fragmentClass.getName().hashCode() : 0;
    }

    @Override
    public String toString() {
        return "TabInfo{" +
                "fragmentClass=" + fragmentClass +
                '}';
    }
}

第一次添加所有选项卡时,我们需要调用方法 createHistory() 来创建初始的历史记录。

public void createHistory() {
    int position = 0;
    TOTAL_TABS = mTabs.size();
    for (TabInfo mTab : mTabs) {
        if (history.get(position) == null) {
            history.put(position, new Stack<TabInfo>());
        }
        history.get(position).push(new TabInfo(mTab.fragmentClass, mTab.args));
        position++;
    }
}

当您想将一个片段替换到特定的标签页时,可以调用以下方法:replace(final int position, final Class fragmentClass, final Bundle args)

/* Save the fragment to the history. */
    mActivity.getSupportFragmentManager().beginTransaction().addToBackStack(null).commit();

    /* Update the tabs. */
    updateTabs(new TabInfo(fragmentClass, args), position);

    /* Updates the history. */
    history.get(position).push(new TabInfo(mTabs.get(position).fragmentClass, mTabs.get(position).args));

    notifyDataSetChanged();

在按下返回键时,您需要调用back()方法:

public void back() {
    int position = mActionBar.getSelectedTab().getPosition();
    if (!historyIsEmpty(position)) {
        /* In case there is not any other item in the history, then finalize the activity. */
        if (isLastItemInHistory(position)) {
            mActivity.finish();
        }
        final TabInfo currentTabInfo = getPrevious(position);
        mTabs.clear();
        for (int i = 0; i < TOTAL_TABS; i++) {
            if (i == position) {
                mTabs.add(new TabInfo(currentTabInfo.fragmentClass, currentTabInfo.args));
            } else {
                TabInfo otherTabInfo = history.get(i).peek();
                mTabs.add(new TabInfo(otherTabInfo.fragmentClass, otherTabInfo.args));
            }
        }
    }
    mActionBar.selectTab(mActionBar.getTabAt(position));
    notifyDataSetChanged();
}

这个解决方案适用于Sherlock操作栏以及滑动手势。


太好了!你能放到GitHub上吗? - Nicramus
这个实现是所有提供的中最干净的一个,并且解决了每一个问题。它应该被接受为答案。 - dazedviper
有人对这个实现有问题吗?我有两个问题。1.第一个后退没有注册——我必须点击两次后退才能注册。2.在我在选项卡a上点击两次后退并转到选项卡b后,当我返回选项卡a时,它只显示选项卡b的内容。 - yaronbic

7

tl;dr: 使用一个主机片段,负责替换其托管内容并跟踪后退导航历史记录(类似于浏览器)。

由于您的用例包括固定数量的选项卡,因此我的解决方案非常有效:想法是使用自定义类 HostFragment 的实例来填充 ViewPager,该类能够替换其托管的内容并保留自己的后退导航历史记录。要替换托管的片段,请调用方法 hostfragment.replaceFragment()

public void replaceFragment(Fragment fragment, boolean addToBackstack) {
    if (addToBackstack) {
        getChildFragmentManager().beginTransaction().replace(R.id.hosted_fragment, fragment).addToBackStack(null).commit();
    } else {
        getChildFragmentManager().beginTransaction().replace(R.id.hosted_fragment, fragment).commit();
    }
}

所有这个方法要做的事情就是用方法中提供的片段替换id为R.id.hosted_fragment的框架布局。

查看我的有关此主题的教程,以获取更多详细信息和完整的在GitHub上工作示例!


虽然看起来比较笨拙,但我认为这实际上是做这件事情最好(从结构和易维护性方面考虑)的方式。 - Sira Lam
我最终撞上了NPE。:/ 找不到要替换的视图。 - Fariz

6
要在 ViewPager 中替换一个片段,您可以将 ViewPagerPagerAdapterFragmentStatePagerAdapter 类的源代码移动到您的项目中,并添加以下代码:

ViewPager 中添加以下代码:

public void notifyItemChanged(Object oldItem, Object newItem) {
    if (mItems != null) {
            for (ItemInfo itemInfo : mItems) {
                        if (itemInfo.object.equals(oldItem)) {
                                itemInfo.object = newItem;
                        }
                    }
       }
       invalidate();
    }

转换为FragmentStatePagerAdapter:

public void replaceFragmetns(ViewPager container, Fragment oldFragment, Fragment newFragment) {
       startUpdate(container);

       // remove old fragment

       if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
       int position = getFragmentPosition(oldFragment);
        while (mSavedState.size() <= position) {
            mSavedState.add(null);
        }
        mSavedState.set(position, null);
        mFragments.set(position, null);

        mCurTransaction.remove(oldFragment);

        // add new fragment

        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        mFragments.set(position, newFragment);
        mCurTransaction.add(container.getId(), newFragment);

       finishUpdate(container);

       // ensure getItem returns newFragemtn after calling handleGetItemInbalidated()
       handleGetItemInbalidated(container, oldFragment, newFragment);

       container.notifyItemChanged(oldFragment, newFragment);
    }

protected abstract void handleGetItemInbalidated(View container, Fragment oldFragment, Fragment newFragment);
protected abstract int  getFragmentPosition(Fragment fragment);

handleGetItemInvalidated() 确保在下一次调用 getItem() 时它会返回新的 fragment。getFragmentPosition() 返回您的适配器中该 fragment 的位置。

现在,要替换 fragments,请调用

mAdapter.replaceFragmetns(mViewPager, oldFragment, newFragment);

如果你感兴趣了解一个示例项目,请向我索要源代码。

getFragmentPosition()和handleGetItemInbalidated()这两个方法应该怎么写呢? 我无法更新getItem()以返回NewFragment。 请帮忙。 - user1069391
您好!请在以下链接中找到测试项目:http://files.mail.ru/eng?back=%2FEZU6G4 - AndroidTeam At Mail.Ru
1
如果您旋转设备,您的测试项目将失败。 - seb
1
@AndroidTeamAtMail.Ru,链接似乎已经过期了。你能否分享一下代码? - Sunny
您可以查看我的答案。它替换了片段并返回到第一个片段。 - Lyd

6
一些提出的解决方案在部分情况下帮助了我解决问题,但是这些解决方案中仍然缺少一个重要的东西,在某些情况下会产生意外异常和黑屏内容,而不是片段内容。
问题在于FragmentPagerAdapter类使用项ID将缓存的片段存储到FragmentManager中。因此,您还需要覆盖getItemId(int position)方法,以便对于顶级页面返回例如position,对于详细页面返回100 + position。否则,之前创建的顶级片段将从缓存中返回,而不是详细级别片段。
此外,我在此分享一个完整的示例,如何使用ViewPager实现类似选项卡的活动,并使用RadioGroup作为选项卡按钮,允许用详细页替换顶级页面,并支持后退按钮。该实现仅支持一级后退堆栈(项目列表-项目详细信息),但多级后退堆栈实现很简单。除了在切换到例如第二个页面时,更改第一页的片段(而不可见)并返回到第一页时抛出NullPointerException之外,此示例在正常情况下运行得非常好。我会在解决此问题后发布解决方案。
public class TabsActivity extends FragmentActivity {

  public static final int PAGE_COUNT = 3;
  public static final int FIRST_PAGE = 0;
  public static final int SECOND_PAGE = 1;
  public static final int THIRD_PAGE = 2;

  /**
   * Opens a new inferior page at specified tab position and adds the current page into back
   * stack.
   */
  public void startPage(int position, Fragment content) {
    // Replace page adapter fragment at position.
    mPagerAdapter.start(position, content);
  }

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Initialize basic layout.
    this.setContentView(R.layout.tabs_activity);

    // Add tab fragments to view pager.
    {
      // Create fragments adapter.
      mPagerAdapter = new PagerAdapter(pager);
      ViewPager pager = (ViewPager) super.findViewById(R.id.tabs_view_pager);
      pager.setAdapter(mPagerAdapter);

      // Update active tab in tab bar when page changes.
      pager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled(int index, float value, int nextIndex) {
          // Not used.
        }

        @Override
        public void onPageSelected(int index) {
          RadioGroup tabs_radio_group = (RadioGroup) TabsActivity.this.findViewById(
            R.id.tabs_radio_group);
          switch (index) {
            case 0: {
              tabs_radio_group.check(R.id.first_radio_button);
            }
            break;
            case 1: {
              tabs_radio_group.check(R.id.second_radio_button);
            }
            break;
            case 2: {
              tabs_radio_group.check(R.id.third_radio_button);
            }
            break;
          }
        }

        @Override
        public void onPageScrollStateChanged(int index) {
          // Not used.
        }
      });
    }

    // Set "tabs" radio group on checked change listener that changes the displayed page.
    RadioGroup radio_group = (RadioGroup) this.findViewById(R.id.tabs_radio_group);
    radio_group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
      @Override
      public void onCheckedChanged(RadioGroup radioGroup, int id) {
        // Get view pager representing tabs.
        ViewPager view_pager = (ViewPager) TabsActivity.this.findViewById(R.id.tabs_view_pager);
        if (view_pager == null) {
          return;
        }

        // Change the active page.
        switch (id) {
          case R.id.first_radio_button: {
            view_pager.setCurrentItem(FIRST_PAGE);
          }
          break;
          case R.id.second_radio_button: {
            view_pager.setCurrentItem(SECOND_PAGE);
          }
          break;
          case R.id.third_radio_button: {
            view_pager.setCurrentItem(THIRD_PAGE);
          }
          break;
        }
      });
    }
  }

  @Override
  public void onBackPressed() {
    if (!mPagerAdapter.back()) {
      super.onBackPressed();
    }
  }

  /**
   * Serves the fragments when paging.
   */
  private class PagerAdapter extends FragmentPagerAdapter {

    public PagerAdapter(ViewPager container) {
      super(TabsActivity.this.getSupportFragmentManager());

      mContainer = container;
      mFragmentManager = TabsActivity.this.getSupportFragmentManager();

      // Prepare "empty" list of fragments.
      mFragments = new ArrayList<Fragment>(){};
      mBackFragments = new ArrayList<Fragment>(){};
      for (int i = 0; i < PAGE_COUNT; i++) {
        mFragments.add(null);
        mBackFragments.add(null);
      }
    }

    /**
     * Replaces the view pager fragment at specified position.
     */
    public void replace(int position, Fragment fragment) {
      // Get currently active fragment.
      Fragment old_fragment = mFragments.get(position);
      if (old_fragment == null) {
        return;
      }

      // Replace the fragment using transaction and in underlaying array list.
      // NOTE .addToBackStack(null) doesn't work
      this.startUpdate(mContainer);
      mFragmentManager.beginTransaction().setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
        .remove(old_fragment).add(mContainer.getId(), fragment)
        .commit();
      mFragments.set(position, fragment);
      this.notifyDataSetChanged();
      this.finishUpdate(mContainer);
    }

    /**
     * Replaces the fragment at specified position and stores the current fragment to back stack
     * so it can be restored by #back().
     */
    public void start(int position, Fragment fragment) {
      // Remember current fragment.
      mBackFragments.set(position, mFragments.get(position));

      // Replace the displayed fragment.
      this.replace(position, fragment);
    }

    /**
     * Replaces the current fragment by fragment stored in back stack. Does nothing and returns
     * false if no fragment is back-stacked.
     */
    public boolean back() {
      int position = mContainer.getCurrentItem();
      Fragment fragment = mBackFragments.get(position);
      if (fragment == null) {
        // Nothing to go back.
        return false;
      }

      // Restore the remembered fragment and remove it from back fragments.
      this.replace(position, fragment);
      mBackFragments.set(position, null);
      return true;
    }

    /**
     * Returns fragment of a page at specified position.
     */
    @Override
    public Fragment getItem(int position) {
      // If fragment not yet initialized, create its instance.
      if (mFragments.get(position) == null) {
        switch (position) {
          case FIRST_PAGE: {
            mFragments.set(FIRST_PAGE, new DefaultFirstFragment());
          }
          break;
          case SECOND_PAGE: {
            mFragments.set(SECOND_PAGE, new DefaultSecondFragment());
          }
          break;
          case THIRD_PAGE: {
            mFragments.set(THIRD_PAGE, new DefaultThirdFragment());
          }
          break;
        }
      }

      // Return fragment instance at requested position.
      return mFragments.get(position);
    }

    /**
     * Custom item ID resolution. Needed for proper page fragment caching.
     * @see FragmentPagerAdapter#getItemId(int).
     */
    @Override
    public long getItemId(int position) {
      // Fragments from second level page hierarchy have their ID raised above 100. This is
      // important to FragmentPagerAdapter because it is caching fragments to FragmentManager with
      // this item ID key.
      Fragment item = mFragments.get(position);
      if (item != null) {
        if ((item instanceof NewFirstFragment) || (item instanceof NewSecondFragment) ||
          (item instanceof NewThirdFragment)) {
          return 100 + position;
        }
      }

      return position;
    }

    /**
     * Returns number of pages.
     */
    @Override
    public int getCount() {
      return mFragments.size();
    }

    @Override
    public int getItemPosition(Object object)
    {
      int position = POSITION_UNCHANGED;
      if ((object instanceof DefaultFirstFragment) || (object instanceof NewFirstFragment)) {
        if (object.getClass() != mFragments.get(FIRST_PAGE).getClass()) {
          position = POSITION_NONE;
        }
      }
      if ((object instanceof DefaultSecondragment) || (object instanceof NewSecondFragment)) {
        if (object.getClass() != mFragments.get(SECOND_PAGE).getClass()) {
          position = POSITION_NONE;
        }
      }
      if ((object instanceof DefaultThirdFragment) || (object instanceof NewThirdFragment)) {
        if (object.getClass() != mFragments.get(THIRD_PAGE).getClass()) {
          position = POSITION_NONE;
        }
      }
      return position;
    }

    private ViewPager mContainer;
    private FragmentManager mFragmentManager;

    /**
     * List of page fragments.
     */
    private List<Fragment> mFragments;

    /**
     * List of page fragments to return to in onBack();
     */
    private List<Fragment> mBackFragments;
  }

  /**
   * Tab fragments adapter.
   */
  private PagerAdapter mPagerAdapter;
}

哦,这段代码还有一个问题。当我想从一个已经被移除和添加回来的片段启动一个活动时,我会收到一个错误信息:"Failure saving state: active NewFirstFragment{405478a0} has cleared index: -1"(完整堆栈跟踪:http://nopaste.info/547a23dfea.html)。查看Android源代码后,我发现它与返回栈有关,但目前不知道如何修复它。有人有头绪吗? - Blackhex
2
很酷,使用<code>mFragmentManager.beginTransaction().detach(old_fragment).attach(fragment).commit();</code>而不是在上面的示例中使用<pre>mFragmentManager.beginTransaction().setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN).remove(old_fragment).add(mContainer.getId(), fragment).commit();</pre>似乎可以解决这两个问题 :-)。 - Blackhex
1
如果您使用此方法,请确保在旋转设备时不要让Android重新创建您的Activity。否则,PagerAdapter将被重新创建,您将失去mBackFragments。这也会严重困扰FragmentManager。使用BackStack可以避免此问题,但代价是更复杂的PagerAdapter实现(即无法继承自FragmentPagerAdapter)。 - Norman

6
我创建了一个包含3个元素和2个子元素(针对索引2和3)的ViewPager,以下是我的想法...

enter image description here

我已经在StackOverFlow上查找了先前的问题和答案,并在此链接中实现了它。 ViewPagerChildFragments

如果您旋转设备,您的项目将失败。 - Dory

4

使用AndroidTeam的解决方案效果很好,但是我发现我需要能够像 FrgmentTransaction.addToBackStack(null)那样返回上一步。 但是仅仅添加这个功能会导致Fragment被替换而不通知ViewPager。将提供的解决方案与此次小的增强结合起来,您只需覆盖activity的onBackPressed()方法就可以返回到以前的状态了。最大的缺点是它一次只能返回一个,这可能会导致多次点击返回按钮。

private ArrayList<Fragment> bFragments = new ArrayList<Fragment>();
private ArrayList<Integer> bPosition = new ArrayList<Integer>();

public void replaceFragmentsWithBackOut(ViewPager container, Fragment oldFragment, Fragment newFragment) {
    startUpdate(container);

    // remove old fragment

    if (mCurTransaction == null) {
         mCurTransaction = mFragmentManager.beginTransaction();
     }
    int position = getFragmentPosition(oldFragment);
     while (mSavedState.size() <= position) {
         mSavedState.add(null);
     }

     //Add Fragment to Back List
     bFragments.add(oldFragment);

     //Add Pager Position to Back List
     bPosition.add(position);

     mSavedState.set(position, null);
     mFragments.set(position, null);

     mCurTransaction.remove(oldFragment);

     // add new fragment

     while (mFragments.size() <= position) {
         mFragments.add(null);
     }
     mFragments.set(position, newFragment);
     mCurTransaction.add(container.getId(), newFragment);

    finishUpdate(container);

    // ensure getItem returns newFragemtn after calling handleGetItemInbalidated()
    handleGetItemInvalidated(container, oldFragment, newFragment);

    container.notifyItemChanged(oldFragment, newFragment);
 }


public boolean popBackImmediate(ViewPager container){
    int bFragSize = bFragments.size();
    int bPosSize = bPosition.size();

    if(bFragSize>0 && bPosSize>0){
        if(bFragSize==bPosSize){
            int last = bFragSize-1;
            int position = bPosition.get(last);

            //Returns Fragment Currently at this position
            Fragment replacedFragment = mFragments.get(position);               
            Fragment originalFragment = bFragments.get(last);

            this.replaceFragments(container, replacedFragment, originalFragment);

            bPosition.remove(last);
            bFragments.remove(last);

            return true;
        }
    }

    return false;       
}

希望这能帮到某些人。
另外,就 getFragmentPosition() 而言,它基本上是反向的 getItem()。你知道哪些片段放在哪里,只需确保返回正确的位置即可。以下是一个示例:
    @Override
    protected int getFragmentPosition(Fragment fragment) {
            if(fragment.equals(originalFragment1)){
                return 0;
            }
            if(fragment.equals(replacementFragment1)){
                return 0;
            }
            if(fragment.equals(Fragment2)){
                return 1;
            }
        return -1;
    }

3
在你的onCreateView方法中,container实际上是一个ViewPager实例。
因此,只需调用
ViewPager vpViewPager = (ViewPager) container;
vpViewPager.setCurrentItem(1);

将会更改您的ViewPager中的当前片段。


1
经过数小时的搜索,我终于找到了这一行代码 vpViewPager.setCurrentItem(1);,它让我的选项卡正常工作。在尝试了每个人的示例后,每次都没有任何反应,直到我最终找到了你的示例。非常感谢你。 - Brandon
1
我的脑袋炸了...这太神奇了!!我没有意识到容器变量就是viewpager本身。在探索其他冗长的答案之前,必须先探索这个答案。此外,在Android自动重新创建片段时使用默认的无参构造函数时,没有将侦听器变量传递到片段构造函数中,这简化了所有实现。 - Kevin Lee
4
这不仅仅是切换到现有选项卡吗?这与替换片段有什么关系? - behelit
2
同意@behelit的观点,这并没有回答问题,它只会将“ViewPager”移动到另一页,而不会替换任何片段。 - Yoann Hercouet

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