在Android中使用Fragments为每个选项卡单独创建返回栈

161

我正在尝试在Android应用程序中实现选项卡导航。由于TabActivity和ActivityGroup已经过时,因此我想改用Fragment来实现。

我知道如何为每个选项卡设置一个碎片,然后在单击选项卡时切换碎片。但是,如何为每个选项卡设置单独的返回堆栈呢?

例如,Fragment A和B将位于选项卡1下,Fragment C和D将位于选项卡2下。当启动应用程序时,将显示Fragment A并选择选项卡1。然后,Fragment A可能会被Fragment B替换。选择选项卡2时,应显示Fragment C。如果再次选择选项卡1,则应再次显示Fragment B。此时,可以使用后退按钮显示Fragment A。

此外,在旋转设备时需要维护每个选项卡的状态。

敬礼 马丁

12个回答

140

在使用此解决方案之前,请仔细阅读本文

哇,我仍然无法相信这个答案是这个帖子中得票最高的答案。请不要盲目地跟随此实现。我在2012年编写了这个解决方案(当时我只是一个Android新手)。十年过去了,我可以看到这个解决方案存在严重问题。

我正在存储硬引用以实现导航堆栈。这是一种可怕的做法,会导致内存泄漏。让FragmentManager保存对片段的引用。如果需要,只需存储片段标识符。

如果需要,可以在上述修改的基础上使用我的答案。但是我认为我们不需要从头开始编写多层导航实现。肯定有更好的现成解决方案。我现在不太了解Android,所以无法指出任何一个。

出于完整性考虑,我将保留原始答案。

原始答案

我非常晚才回答这个问题。但是由于这个线程对我非常有帮助和启示,我想我最好在这里发表我的意见。

我需要类似于这样的屏幕流程(一个最小化的设计,其中有2个选项卡和每个选项卡中的2个视图),

tabA
    ->  ScreenA1, ScreenA2
tabB
    ->  ScreenB1, ScreenB2

我曾经也有过同样的需求,当时我使用了TabActivityGroup(那时已被弃用)和活动来实现。这次我想使用片段。

所以我是这样做的。

1. 创建一个基础片段类

public class BaseFragment extends Fragment {
    AppMainTabActivity mActivity;
  
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mActivity = (AppMainTabActivity) this.getActivity();
    }
  
    public void onBackPressed(){
    }
  
    public void onActivityResult(int requestCode, int resultCode, Intent data){
    }
}

你的应用程序中的所有片段都可以扩展此基类。如果您想使用特殊的片段,如ListFragment,则也应创建一个基类。如果您完整阅读帖子,您将清楚了解onBackPressed()onActivityResult()的用法。

2. 创建一些选项卡标识符,可在整个项目中访问

public class AppConstants{
    public static final String TAB_A  = "tab_a_identifier";
    public static final String TAB_B  = "tab_b_identifier";
   
    //Your other constants, if you have them..
}

这里没有什么需要解释的。

3. 好的,主选项卡活动- 请查看代码中的注释..

public class AppMainFragmentActivity extends FragmentActivity{
    /* Your Tab host */
    private TabHost mTabHost;
  
    /* A HashMap of stacks, where we use tab identifier as keys..*/
    private HashMap<String, Stack<Fragment>> mStacks;
  
    /*Save current tabs identifier in this..*/
    private String mCurrentTab;
  
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.app_main_tab_fragment_layout);
    
        /*  
         *  Navigation stacks for each tab gets created.. 
         *  tab identifier is used as key to get respective stack for each tab
         */
        mStacks             =   new HashMap<String, Stack<Fragment>>();
        mStacks.put(AppConstants.TAB_A, new Stack<Fragment>());
        mStacks.put(AppConstants.TAB_B, new Stack<Fragment>());
    
        mTabHost                =   (TabHost)findViewById(android.R.id.tabhost);
        mTabHost.setOnTabChangedListener(listener);
        mTabHost.setup();
    
        initializeTabs();
    }
  
  
    private View createTabView(final int id) {
        View view = LayoutInflater.from(this).inflate(R.layout.tabs_icon, null);
        ImageView imageView =   (ImageView) view.findViewById(R.id.tab_icon);
        imageView.setImageDrawable(getResources().getDrawable(id));
        return view;
    }
  
    public void initializeTabs(){
        /* Setup your tab icons and content views.. Nothing special in this..*/
        TabHost.TabSpec spec    =   mTabHost.newTabSpec(AppConstants.TAB_A);
        mTabHost.setCurrentTab(-3);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_home_state_btn));
        mTabHost.addTab(spec);

  
        spec                    =   mTabHost.newTabSpec(AppConstants.TAB_B);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_status_state_btn));
        mTabHost.addTab(spec);
    }
  
  
    /*Comes here when user switch tab, or we do programmatically*/
    TabHost.OnTabChangeListener listener    =   new TabHost.OnTabChangeListener() {
      public void onTabChanged(String tabId) {
        /*Set current tab..*/
        mCurrentTab                     =   tabId;
        
        if(mStacks.get(tabId).size() == 0){
          /*
           *    First time this tab is selected. So add first fragment of that tab.
           *    Dont need animation, so that argument is false.
           *    We are adding a new fragment which is not present in stack. So add to stack is true.
           */
          if(tabId.equals(AppConstants.TAB_A)){
            pushFragments(tabId, new AppTabAFirstFragment(), false,true);
          }else if(tabId.equals(AppConstants.TAB_B)){
            pushFragments(tabId, new AppTabBFirstFragment(), false,true);
          }
        }else {
          /*
           *    We are switching tabs, and target tab is already has atleast one fragment. 
           *    No need of animation, no need of stack pushing. Just show the target fragment
           */
          pushFragments(tabId, mStacks.get(tabId).lastElement(), false,false);
        }
      }
    };


    /* Might be useful if we want to switch tab programmatically, from inside any of the fragment.*/
    public void setCurrentTab(int val){
          mTabHost.setCurrentTab(val);
    }
  
  
    /* 
     *      To add fragment to a tab. 
     *  tag             ->  Tab identifier
     *  fragment        ->  Fragment to show, in tab identified by tag
     *  shouldAnimate   ->  should animate transaction. false when we switch tabs, or adding first fragment to a tab
     *                      true when when we are pushing more fragment into navigation stack. 
     *  shouldAdd       ->  Should add to fragment navigation stack (mStacks.get(tag)). false when we are switching tabs (except for the first time)
     *                      true in all other cases.
     */
    public void pushFragments(String tag, Fragment fragment,boolean shouldAnimate, boolean shouldAdd){
      if(shouldAdd)
          mStacks.get(tag).push(fragment);
      FragmentManager   manager         =   getSupportFragmentManager();
      FragmentTransaction ft            =   manager.beginTransaction();
      if(shouldAnimate)
          ft.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left);
      ft.replace(R.id.realtabcontent, fragment);
      ft.commit();
    }
  
  
    public void popFragments(){
      /*    
       *    Select the second last fragment in current tab's stack.. 
       *    which will be shown after the fragment transaction given below 
       */
      Fragment fragment             =   mStacks.get(mCurrentTab).elementAt(mStacks.get(mCurrentTab).size() - 2);
      
      /*pop current fragment from stack.. */
      mStacks.get(mCurrentTab).pop();
      
      /* We have the target fragment in hand.. Just show it.. Show a standard navigation animation*/
      FragmentManager   manager         =   getSupportFragmentManager();
      FragmentTransaction ft            =   manager.beginTransaction();
      ft.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right);
      ft.replace(R.id.realtabcontent, fragment);
      ft.commit();
    }   


    @Override
    public void onBackPressed() {
        if(mStacks.get(mCurrentTab).size() == 1){
          // We are already showing first fragment of current tab, so when back pressed, we will finish this activity..
          finish();
          return;
        }
      
        /*  Each fragment represent a screen in application (at least in my requirement, just like an activity used to represent a screen). So if I want to do any particular action
         *  when back button is pressed, I can do that inside the fragment itself. For this I used AppBaseFragment, so that each fragment can override onBackPressed() or onActivityResult()
         *  kind of events, and activity can pass it to them. Make sure just do your non navigation (popping) logic in fragment, since popping of fragment is done here itself.
         */
        ((AppBaseFragment)mStacks.get(mCurrentTab).lastElement()).onBackPressed();
        
        /* Goto previous fragment in navigation stack of this tab */
            popFragments();
    }


    /*
     *   Imagine if you wanted to get an image selected using ImagePicker intent to the fragment. Ofcourse I could have created a public function
     *  in that fragment, and called it from the activity. But couldn't resist myself.
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if(mStacks.get(mCurrentTab).size() == 0){
            return;
        }

        /*Now current fragment on screen gets onActivityResult callback..*/
        mStacks.get(mCurrentTab).lastElement().onActivityResult(requestCode, resultCode, data);
    }
}

4. app_main_tab_fragment_layout.xml(如果有人感兴趣的话)


这是一个XML布局文件,用于显示主要的应用选项卡。
<?xml version="1.0" encoding="utf-8"?>
<TabHost
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/tabhost"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">

        <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_weight="0"/>

        <FrameLayout
            android:id="@+android:id/realtabcontent"
            android:layout_width="fill_parent"
            android:layout_height="0dp"
            android:layout_weight="1"/>
            
        <TabWidget
            android:id="@android:id/tabs"
            android:orientation="horizontal"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_weight="0"/>
                        
    </LinearLayout>
</TabHost>

5. AppTabAFirstFragment.java(选项卡 A 中的第一个片段,所有选项卡类似)

public class AppTabAFragment extends BaseFragment {
    private Button mGotoButton;
    
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View view       =   inflater.inflate(R.layout.fragment_one_layout, container, false);
        
        mGoToButton =   (Button) view.findViewById(R.id.goto_button);
        mGoToButton.setOnClickListener(listener);
    
        return view;
    }

    private OnClickListener listener        =   new View.OnClickListener(){
        @Override
        public void onClick(View v){
            /* Go to next fragment in navigation stack*/
            mActivity.pushFragments(AppConstants.TAB_A, new AppTabAFragment2(),true,true);
        }
    }
}

这可能不是最完美和正确的方法,但在我的情况下效果非常好。此外,我仅在纵向模式下具有此要求。我从未在支持两个方向的项目中使用过此代码。因此无法确定我将面临哪些挑战。

如果任何人需要一个完整的项目,我已经将示例项目推送到github上。


2
为每个片段存储数据,重新创建每一个片段,重建堆栈... 对于一个简单的方向更改来说,这是如此繁琐的工作。 - Michael Eilers Smith
3
完全同意@omegatai的观点。所有的问题都是由于Android没有为我们管理堆栈而产生的(* iOS可以处理,无论是方向变化还是带有多个片段的选项卡都很轻松),这让我们回到了这个问答主题的原始讨论。现在不要回到那里了... - Krishnabhadra
2
@Krishnabhadra 好的,这听起来好多了。如果我理解有误,请让我纠正。根据您的示例,只有一个活动,因此只有一个包。在BaseFragment(参考您的项目)中创建适配器实例并在其中保存数据。每当需要构建视图时使用它们。 - Renjith
1
搞定了,非常感谢。上传整个项目是个好主意! :-) - Vinay W
1
@Vijay,你不需要调用 popFragment()。只需清除该选项卡的堆栈,如 mStacks.get(mCurrentTab).clear()。然后将主页片段推送。所有不可见的片段(返回堆栈)已经弹出,它们的唯一引用驻留在我们管理的堆栈中。您只需要从堆栈中删除它即可。 - Krishnabhadra
显示剩余34条评论

98
我们最近需要为一个应用程序实现与您描述的完全相同的行为。 应用程序的屏幕和整体流程已经定义,因此我们必须坚持使用它(这是一个iOS应用程序克隆...)。幸运的是,我们成功地摆脱了屏幕上的后退按钮 :)
我们通过混合使用TabActivity、FragmentActivities(我们正在使用片段的支持库)和Fragments来实现解决方案。 回顾起来,我非常确定这不是最好的架构决策,但我们设法使其正常工作。 如果我必须再次执行此操作,则可能尝试使用更多基于活动的解决方案(没有片段),或者尝试仅将选项卡的所有其他内容作为视图(相对于活动而言,我认为视图更具可重复性)。
因此,要求在每个选项卡中具有一些选项卡和可嵌套的屏幕:
tab 1
  screen 1 -> screen 2 -> screen 3
tab 2
  screen 4
tab 3
  screen 5 -> 6

比如说,用户从选项卡1开始,在屏幕1中导航到屏幕2再到屏幕3,然后切换到选项卡3,在屏幕4中导航到6;如果再次切换回选项卡1,他应该再次看到屏幕3,如果按下“返回”按钮,他应该返回到屏幕2;再按一次“返回”按钮,他就会回到屏幕1;切换到选项卡3,他会再次进入屏幕6。

这个应用程序的主Activity是MainTabActivity,它扩展了TabActivity。每个选项卡都与一个活动相关联,假设为ActivityInTab1、2和3。然后每个屏幕将是一个片段:

MainTabActivity
  ActivityInTab1
    Fragment1 -> Fragment2 -> Fragment3
  ActivityInTab2
    Fragment4
  ActivityInTab3
    Fragment5 -> Fragment6
每个ActivityInTab只持有一个fragment,并且知道如何替换另一个fragment(与ActivityGroup基本相同)。很酷的是,以这种方式很容易为每个选项卡维护单独的后退堆栈。
每个ActivityInTab的功能都相当相似:知道如何从一个fragment导航到另一个并维护后退堆栈,因此我们将其放在一个基类中。让我们简单地称之为ActivityInTab:
abstract class ActivityInTab extends FragmentActivity { // FragmentActivity is just Activity for the support library.

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

    /**
     * Navigates to a new fragment, which is added in the fragment container
     * view.
     * 
     * @param newFragment
     */
    protected void navigateTo(Fragment newFragment) {
        FragmentManager manager = getSupportFragmentManager();
        FragmentTransaction ft = manager.beginTransaction();

        ft.replace(R.id.content, newFragment);

        // Add this transaction to the back stack, so when the user presses back,
        // it rollbacks.
        ft.addToBackStack(null);
        ft.commit();
    }

}

activity_in_tab.xml就是这样:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/content"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:isScrollContainer="true">
</RelativeLayout>

正如您所看到的,每个选项卡的视图布局都是相同的。这是因为它只是一个名为"content"的FrameLayout,它将容纳每个片段。这些片段是拥有每个屏幕视图的部分。

仅仅为了获得额外加分,我们还添加了一些代码,以在用户按下返回键且没有更多片段可返回时显示确认对话框:

// In ActivityInTab.java...
@Override
public void onBackPressed() {
    FragmentManager manager = getSupportFragmentManager();
    if (manager.getBackStackEntryCount() > 0) {
        // If there are back-stack entries, leave the FragmentActivity
        // implementation take care of them.
        super.onBackPressed();
    } else {
        // Otherwise, ask user if he wants to leave :)
        showExitDialog();
    }
}

这就是大致的设置。正如您所见,每个FragmentActivity(或在Android >3中仅仅是Activity)都使用自己的FragmentManager来处理所有后退堆栈。

像ActivityInTab1这样的活动将非常简单,它只会显示它的第一个片段(即屏幕):

public class ActivityInTab1 extends ActivityInTab {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        navigateTo(new Fragment1());
    }
}

那么,如果一个片段需要导航到另一个片段,它必须进行一些不太好的转换...但这并不糟糕:

// In Fragment1.java for example...
// Need to navigate to Fragment2.
((ActivityIntab) getActivity()).navigateTo(new Fragment2());

这就是大致的内容。我相信这不是一个很规范(并且大部分也不是很好)的解决方案,因此我想请经验丰富的Android开发人员提供更好的方法来实现此功能。如果这不是Android中的“正确方式”,我希望您可以指点一些链接或材料,以解释应该如何处理它(选项卡、嵌套屏幕在选项卡中等)。欢迎在评论中批评这个答案 :)

作为这种解决方案不太好的标志,最近我不得不向应用程序添加一些导航功能。一些奇怪的按钮应该将用户从一个选项卡带到另一个选项卡并进入嵌套屏幕。通过编程完成这一点真是痛苦,因为要处理由谁知道引起的问题以及处理片段和活动实际上是何时实例化和初始化的。如果那些屏幕和选项卡只是视图,那么这将会容易得多。


最后,如果您需要在屏幕方向改变时保持数据不变,重要的是使用setArguments / getArguments创建您的片段。如果您在片段的构造函数中设置实例变量,那么您将遇到问题。但幸运的是,这很容易解决:只需在构造函数中使用setArguments保存所有内容,然后在onCreate中使用getArguments检索这些内容以使用它们。


13
回答很好,但我认为很少有人会看到。正如您从之前的回答中可以看到的那样,我选择了完全相同的路径,但我和您一样并不满意。我认为谷歌真的在这些片段上搞砸了,因为这个API不能涵盖主要用例。另一个问题是您可能会遇到的无法将片段嵌入到另一个片段中。 - Dmitry Ryadnenko
1
我已经通过所有活动来实现这个。我不喜欢我得到的结果,我将尝试Fragments。这与您的经验相反!使用Activities有很多实现方式,以处理每个选项卡中子视图的生命周期,并实现自己的返回按钮。此外,您不能仅保留对所有视图的引用,否则会耗尽内存。我希望Fragments能够:1)支持Fragments的生命周期,并清晰地分离内存;2)帮助实现返回按钮功能。此外,如果您在此过程中使用Fragments,那么在平板电脑上运行会更容易吗? - gregm
1
@gregm 如果你像我一样使用1个标签页<->1个活动,那么当切换标签时,每个标签的后退栈将保留,因为这些活动实际上是被保持活动状态的;它们只是暂停和恢复。我不知道是否有一种方法可以使活动在TabActivity中切换标签时被销毁并重新创建。然而,如果你像我建议的那样替换活动内部的片段,它们就会被销毁(并在后退栈弹出时重新创建)。因此,在任何时候,每个标签最多只有一个片段处于活动状态。 - epidemian
@Reham,是的,好久不见了 :). 如果问题足够适合作为此答案的评论,请继续提问(我不能保证我会真正记得这段代码的太多细节呵呵)。也许,如果它足够普遍可以帮助其他人,你可以把它作为一个真正的 StackOverflow 的问题来问 :)。 - epidemian
好的,谢谢.. :)我使用了你的代码,并构建了一个带有菜单按钮的自定义操作栏,当我点击任何项目时,我只想在 ActivityIntab 中显示与该项目相关的片段,就像其他项目一样。但是我没有找到一个方法来做到这一点..我会试着问其他人,谢谢。 - Reham
显示剩余15条评论

23

该框架目前无法自动为您执行此操作。 您需要为每个选项卡构建和管理自己的后退堆栈。

老实说,这似乎是一个非常可疑的做法。我无法想象它会产生一个体验良好的用户界面——如果返回键将根据我所在的选项卡执行不同的操作,特别是当返回键在堆栈顶部时还具有关闭整个活动的正常行为时...听起来很恶心。

如果您尝试构建类似于Web浏览器的用户界面,为了获得对用户自然的UX,必须根据上下文进行许多微妙的行为调整,因此您肯定需要进行自己的后退堆栈管理,而不是依赖框架中的默认实现。例如,请尝试注意标准浏览器在各种进出方式中如何与返回键交互。(浏览器中的每个“窗口”本质上都是一个选项卡。)


7
不要那样做。这个框架几乎没有用处。它不能为这种情况提供自动支持,正如我所说,除非在非常特殊的情况下,您需要仔细控制返回行为,否则我无法想象会产生体面的用户体验。 - hackbod
9
在 iPhone 应用程序中,这种带有选项卡和每个选项卡页面层次结构的导航方式非常常见(例如,您可以在 App Store 和 iPod 应用程序中查看)。我认为它们的用户体验相当不错。 - Dmitry Ryadnenko
13
这太疯狂了。iPhone甚至没有后退按钮。有API演示展示了非常简单的代码来在选项卡中实现碎片。问题是关于每个选项卡有不同的后退堆栈,我的回答是框架不会自动提供这个功能,因为从语义上讲,后退按钮所做的事情很可能会导致糟糕的用户体验。不过,如果你想要的话,你可以相当容易地自己实现后退语义。 - hackbod
4
再次强调,iPhone没有后退按钮,因此它在语义上不像Android一样具有返回堆栈行为。此外,“最好坚持使用活动并节省大量时间”在这里根本没有意义,因为活动无法让您在具有自己不同后退堆栈的UI中保持选项卡;实际上,活动的后退堆栈管理比Fragment框架提供的灵活性更差。 - hackbod
22
@hackbod,我试图理解您的观点,但是在实现自定义返回栈行为方面遇到了一些困难。我意识到作为这个设计的参与者,您对其易用性有很深刻的见解。请问是否有演示应用程序适用于OP的情况,因为这确实是一个非常普遍的情况,特别是对于我们这些需要为客户编写和移植iOS应用程序的人...即在每个FragmentActivity中管理独立的片段返回栈。 - Richard Le Mesurier
显示剩余15条评论

6

存储Fragment的强引用并不是正确的方式。

FragmentManager提供了putFragment(Bundle, String, Fragment)saveFragmentInstanceState(Fragment)

任意一个都足以实现返回栈。


使用putFragment而不是替换一个Fragment,您会将旧的Fragment分离并添加新的Fragment。这就是框架对添加到返回堆栈的替换事务所做的操作。putFragment存储当前活动Fragment列表的索引,并且在方向更改期间由框架保存这些Fragments。
第二种方法是使用saveFragmentInstanceState,将整个Fragment状态保存到Bundle中,允许您真正地删除它,而不是分离它。使用这种方法可以更容易地操作后退堆栈,因为您可以随时弹出一个Fragment。

我在这个用例中使用了第二种方法:

SignInFragment ----> SignUpFragment ---> ChooseBTDeviceFragment
               \                          /
                \------------------------/

我不希望用户通过按返回按钮从第三个屏幕返回到注册屏幕。我还在它们之间使用翻转动画(使用onCreateAnimation),因此hacky解决方案行不通,至少用户会明显注意到有些地方不对。这是自定义后退堆栈的一个有效用例,可以满足用户的期望...
private static final String STATE_BACKSTACK = "SetupActivity.STATE_BACKSTACK";

private MyBackStack mBackStack;

@Override
protected void onCreate(Bundle state) {
    super.onCreate(state);

    if (state == null) {
        mBackStack = new MyBackStack();

        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction tr = fm.beginTransaction();
        tr.add(R.id.act_base_frg_container, new SignInFragment());
        tr.commit();
    } else {
        mBackStack = state.getParcelable(STATE_BACKSTACK);
    }
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putParcelable(STATE_BACKSTACK, mBackStack);
}

private void showFragment(Fragment frg, boolean addOldToBackStack) {
    final FragmentManager fm = getSupportFragmentManager();
    final Fragment oldFrg = fm.findFragmentById(R.id.act_base_frg_container);

    FragmentTransaction tr = fm.beginTransaction();
    tr.replace(R.id.act_base_frg_container, frg);
    // This is async, the fragment will only be removed after this returns
    tr.commit();

    if (addOldToBackStack) {
        mBackStack.push(fm, oldFrg);
    }
}

@Override
public void onBackPressed() {
    MyBackStackEntry entry;
    if ((entry = mBackStack.pop()) != null) {
        Fragment frg = entry.recreate(this);

        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction tr = fm.beginTransaction();
        tr.replace(R.id.act_base_frg_container, frg);
        tr.commit();

        // Pop it now, like the framework implementation.
        fm.executePendingTransactions();
    } else {
        super.onBackPressed();
    }
}

public class MyBackStack implements Parcelable {

    private final List<MyBackStackEntry> mList;

    public MyBackStack() {
        mList = new ArrayList<MyBackStackEntry>(4);
    }

    public void push(FragmentManager fm, Fragment frg) {
        push(MyBackStackEntry.newEntry(fm, frg);
    }

    public void push(MyBackStackEntry entry) {
        if (entry == null) {
            throw new NullPointerException();
        }
        mList.add(entry);
    }

    public MyBackStackEntry pop() {
        int idx = mList.size() - 1;
        return (idx != -1) ? mList.remove(idx) : null;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        final int len = mList.size();
        dest.writeInt(len);
        for (int i = 0; i < len; i++) {
            // MyBackStackEntry's class is final, theres no
            // need to use writeParcelable
            mList.get(i).writeToParcel(dest, flags);
        }
    }

    protected MyBackStack(Parcel in) {
        int len = in.readInt();
        List<MyBackStackEntry> list = new ArrayList<MyBackStackEntry>(len);
        for (int i = 0; i < len; i++) {
            list.add(MyBackStackEntry.CREATOR.createFromParcel(in));
        }
        mList = list;
    }

    public static final Parcelable.Creator<MyBackStack> CREATOR =
        new Parcelable.Creator<MyBackStack>() {

            @Override
            public MyBackStack createFromParcel(Parcel in) {
                return new MyBackStack(in);
            }

            @Override
            public MyBackStack[] newArray(int size) {
                return new MyBackStack[size];
            }
    };
}

public final class MyBackStackEntry implements Parcelable {

    public final String fname;
    public final Fragment.SavedState state;
    public final Bundle arguments;

    public MyBackStackEntry(String clazz, 
            Fragment.SavedState state,
            Bundle args) {
        this.fname = clazz;
        this.state = state;
        this.arguments = args;
    }

    public static MyBackStackEntry newEntry(FragmentManager fm, Fragment frg) {
        final Fragment.SavedState state = fm.saveFragmentInstanceState(frg);
        final String name = frg.getClass().getName();
        final Bundle args = frg.getArguments();
        return new MyBackStackEntry(name, state, args);
    }

    public Fragment recreate(Context ctx) {
        Fragment frg = Fragment.instantiate(ctx, fname);
        frg.setInitialSavedState(state);
        frg.setArguments(arguments);
        return frg;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(fname);
        dest.writeBundle(arguments);

        if (state == null) {
            dest.writeInt(-1);
        } else if (state.getClass() == Fragment.SavedState.class) {
            dest.writeInt(0);
            state.writeToParcel(dest, flags);
        } else {
            dest.writeInt(1);
            dest.writeParcelable(state, flags);
        }
    }

    protected MyBackStackEntry(Parcel in) {
        final ClassLoader loader = getClass().getClassLoader();
        fname = in.readString();
        arguments = in.readBundle(loader);

        switch (in.readInt()) {
            case -1:
                state = null;
                break;
            case 0:
                state = Fragment.SavedState.CREATOR.createFromParcel(in);
                break;
            case 1:
                state = in.readParcelable(loader);
                break;
            default:
                throw new IllegalStateException();
        }
    }

    public static final Parcelable.Creator<MyBackStackEntry> CREATOR =
        new Parcelable.Creator<MyBackStackEntry>() {

            @Override
            public MyBackStackEntry createFromParcel(Parcel in) {
                return new MyBackStackEntry(in);
            }

            @Override
            public MyBackStackEntry[] newArray(int size) {
                return new MyBackStackEntry[size];
            }
    };
}

6

2
这是一个复杂的问题,因为Android只处理1个返回堆栈,但这是可行的。用时数天创建了一个名为Tab Stacker的库,完全满足您的需求:为每个选项卡提供片段历史记录。该库是开源的,已有完整的文档,并且可以轻松地通过gradle进行添加。您可以在github上找到该库:https://github.com/smart-fun/TabStacker 您还可以下载示例应用程序以查看其行为是否符合您的需求:https://play.google.com/apps/testing/fr.arnaudguyon.tabstackerapp 如果您有任何疑问,请随时发送邮件。

2
我想提出我的解决方案,以防有人在寻找并希望尝试选择最适合自己需求的方案。

https://github.com/drusak/tabactivity

创建这个库的目的相当平凡——让它像 iPhone 一样实现。

主要优点包括:
  • 使用 android.support.design 库与 TabLayout;
  • 每个选项卡都使用 FragmentManager 拥有自己的堆栈(不保存片段的引用);
  • 支持深度链接(当您需要打开特定选项卡和其中特定片段的级别时);
  • 保存 / 恢复选项卡状态;
  • 自适应生命周期方法,适用于选项卡中的片段;
  • 非常容易根据您的需求进行实现。

谢谢,这非常有帮助。我需要使用ListFragment以及Fragment,所以我复制了BaseTabFragment.java并将其命名为BaseTabListFragment.java,然后让它扩展ListFragment。接着,我必须更改代码中的各个部分,因为它总是假定期望一个BaseTabFragment。有更好的方法吗? - primehalo
很遗憾,没有考虑到ListFragment。从技术上讲,这是正确的解决方案,但它需要为TabFragment及其instanceOf BaseTabListFragment添加额外的检查。 另一种方法是在Fragment中使用ListView(与实现ListFragment完全相同)。我会考虑一下。谢谢你指出这个问题! - kasurd

2
我有完全相同的问题,并实现了一个开源的Github项目,涵盖了堆叠选项卡、返回和上导航,并经过了充分测试和文档化。

https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs

这是一个简单且小巧的导航选项卡和片段切换框架,可以处理向上和返回导航。每个选项卡都有自己的片段堆栈。它使用ActionBarSherlock,并兼容至API级别8。

2
免责声明:
我认为这是发布我为类似问题制定的相关解决方案的最佳地点,这似乎是标准的 Android 问题。它不会为每个人解决问题,但可能对某些人有所帮助。
如果你的片段之间的主要区别仅在于它们支持的数据(即,没有太多大的布局差异),那么你可能不需要实际替换片段,而只需交换底层数据并刷新视图即可。
以下是一种可能的示例描述:
我有一个使用ListView的应用程序。列表中的每个项目都是一个父项,具有一些子项。当您点击该项时,需要打开一个新列表,并在原始列表的同一个ActionBar选项卡中显示这些子项。这些嵌套列表具有非常相似的布局(也许有一些条件性的调整),但数据是不同的。
这个应用程序有几个后代层次,除了第一个层次之外,我们可能没有来自服务器的数据。因为列表是从数据库游标构建的,并且片段使用游标加载器和游标适配器将列表视图填充为列表项,所以当注册点击时,需要完成以下操作:
1)创建一个新的适配器,具有匹配新项目视图添加到列表和新游标返回的列的适当“to”和“from”字段。
2)将此适配器设置为ListView的新适配器。

3) 基于被点击的项目构建一个新的URI,并使用来自UI的选择参数重新启动游标加载器,以及新的URI(和投影)。在这个例子中,URI被映射到具有特定查询的选择参数。

4) 当从URI加载了新数据时,将与适配器相关联的游标交换为新游标,然后列表将刷新。

由于我们不使用事务,因此没有与此关联的后退堆栈,所以您必须构建自己的后退堆栈,或者在回退层次结构时反向播放查询。当我尝试这样做时,查询足够快,以至于我只需在oNBackPressed()中再次执行它们,直到到达层次结构的顶部,此时框架再次接管返回按钮。

如果您发现自己处于类似的情况,请确保阅读文档: http://developer.android.com/guide/topics/ui/layout/listview.html

http://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html

我希望这能帮助到某个人!

如果有人正在这样做,并且还使用SectionIndexer(例如AlphabetIndexer),则可能会注意到在替换适配器后,快速滚动不起作用。 这是一个不幸的错误,但即使使用全新的索引器替换适配器,也不会更新FastScroll使用的部分列表。 有一个解决方法,请参见:[问题描述](https://groups.google.com/forum/?fromgroups#!topic/android-developers/t9xeOqSbDQY%5B1-25%5D)和[解决方法](https://dev59.com/xE7Sa4cB1Zd3GeqP0Ahg) - courtf

0
一个简单的解决方案:
每次更改选项卡/根视图时,请调用:
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);

这将清除后退栈。在更改根片段之前,请记得调用此函数。

使用以下代码添加片段:

FragmentTransaction transaction = getFragmentManager().beginTransaction();
NewsDetailsFragment newsDetailsFragment = NewsDetailsFragment.newInstance(newsId);
transaction.add(R.id.content_frame, newsDetailsFragment).addToBackStack(null).commit();

请注意,.addToBackStack(null)transaction.add 可以被替换为 transaction.replace。此段内容与编程有关。

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