使用 Fragments 为 Android 中的每个选项卡单独返回堆栈

Posted

技术标签:

【中文标题】使用 Fragments 为 Android 中的每个选项卡单独返回堆栈【英文标题】:Separate Back Stack for each tab in Android using Fragments 【发布时间】:2011-10-22 16:10:11 【问题描述】:

我正在尝试在 android 应用中实现导航标签。由于 TabActivity 和 ActivityGroup 已被弃用,我想改用 Fragments 来实现它。

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

例如,Fragment A 和 B 将位于 Tab 1 下,Fragment C 和 D 在 Tab 2 下。当应用程序启动时,会显示 Fragment A 并选择 Tab 1。 Then Fragment A might be replaced with Fragment B. When Tab 2 is selected Fragment C should be displayed.如果选项卡 1 被选中,那么应该再次显示片段 B。此时应该可以使用返回按钮来显示 Fragment A。

此外,在设备旋转时保持每个选项卡的状态也很重要。

BR 马丁

【问题讨论】:

【参考方案1】:

这个问题我来得太晚了。但是由于这个帖子对我很有帮助,我想我最好把我的两便士贴在这里。

我需要这样的屏幕流(一个简约的设计,每个标签有 2 个选项卡和 2 个视图),

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

我过去也有同样的要求,我使用TabActivityGroup(当时也已弃用)和活动来做到这一点。这次我想使用 Fragments。

所以我就是这样做的。

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. 创建一些 Tab 标识符,在项目中随处可访问

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。好的,主选项卡活动 - 请通过代码中的 cmets..

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 version="1.0" encoding="utf-8"?>
<TabHost
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/tabhost"
    android:layout_
    android:layout_>

    <LinearLayout
        android:orientation="vertical"
        android:layout_
        android:layout_>

        <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_
            android:layout_
            android:layout_weight="0"/>

        <FrameLayout
            android:id="@+android:id/realtabcontent"
            android:layout_
            android:layout_
            android:layout_weight="1"/>

        <TabWidget
            android:id="@android:id/tabs"
            android:orientation="horizontal"
            android:layout_
            android:layout_
            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。

【讨论】:

为每个片段存储数据,重新创建每个片段,重新构建堆栈......为了一个简单的方向改变做了大量的工作。 @omegatai 完全同意你的看法。所有问题都出现了,因为 Android 不为我们管理堆栈(ios 可以做到这一点,方向更改或带有多个片段的选项卡是轻而易举的事),这让我们回到了这个 Q/A 线程中的原始讨论。现在回到那个不好.. @Renjith 这是因为每次切换选项卡时都会重新创建片段。不要想一想,您的片段会在选项卡切换中重用。当我从 A 选项卡切换到 B 时,A 选项卡从内存中释放。因此,将您的数据保存在活动中,并且每次尝试从服务器获取数据之前检查活动是否有数据。 @Krishnabhadra 好的,听起来好多了。让我纠正,以防我错了。根据您的示例,只有一个活动,因此只有一个捆绑包。在 BaseFragment 中创建适配器实例(引用您的项目)并在其中保存数据。每当要构建视图时使用它们。 让它工作。非常感谢。上传整个项目是个好主意! :-)【参考方案2】:

我们必须实现与您最近为应用描述的完全相同的行为。应用程序的屏幕和整体流程已经定义,所以我们必须坚持使用它(它是一个 iOS 应用程序克隆......)。幸运的是,我们设法摆脱了屏幕上的后退按钮 :)

我们使用 TabActivity、FragmentActivities(我们使用 Fragment 的支持库)和 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 一次只保存一个片段,并且知道如何将一个片段替换为另一个片段(与 ActivityGroup 几乎相同)。很酷的是,通过这种方式为每个选项卡维护单独的后台堆栈非常容易。

每个 ActivityInTab 的功能都是相同的:知道如何从一个片段导航到另一个片段并维护一个回栈,因此我们将它放在一个基类中。让我们简称为 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_
    android:layout_
    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 中完成”,如果您能指出一些链接或材料来解释这是 Android 处理此问题的方式(选项卡、选项卡中的嵌套屏幕等),我将不胜感激。随意在 cmets 中撕开这个答案:)

这个解决方案不是很好的一个标志是,最近我不得不向应用程序添加一些导航功能。一些奇怪的按钮,应该将用户从一个选项卡带到另一个选项卡并进入嵌套屏幕。以编程方式执行此操作很麻烦,因为谁知道谁的问题以及处理何时实际实例化和初始化的片段和活动。我认为如果这些屏幕和选项卡都只是视图,那会容易得多。


最后,如果您需要在方向变化中幸存下来,请务必使用 setArguments/getArguments 创建片段。如果您在片段的构造函数中设置实例变量,您将被搞砸。但幸运的是,这很容易解决:只需将所有内容保存在构造函数中的 setArguments 中,然后在 onCreate 中使用 getArguments 检索这些内容以使用它们。

【讨论】:

很好的答案,但我认为很少有人会看到这一点。我选择了完全相同的路径(正如您从先前答案中的对话中看到的那样),我和您一样对它不满意。我认为谷歌真的搞砸了这些片段,因为这个 API 没有涵盖主要的用例。您可能遇到的另一个问题是无法将片段嵌入到另一个片段中。 感谢博尔德的评论。是的,我完全同意 Fragments API。我已经遇到了嵌套片段的问题(这就是为什么我们采用“用另一个片段替换一个片段”的方法呵呵)。 我已经通过所有活动实现了这一点。我不喜欢我得到的东西,我要试试 Fragments。这与你的经历相反!活动有很多实现来处理每个选项卡中子视图的生命周期,还可以实现您自己的后退按钮。此外,您不能只保留对所有视图的引用,否则会破坏内存。我希望 Fragments 能够:1)通过明确的内存分离来支持 Fragments 的生命周期;2)帮助实现后退按钮功能另外,如果您在此过程中使用 Fragments 会不会更容易在平板电脑上运行? 当用户切换标签时会发生什么? Fragment backstack 是否被删除?如何确保 backstack 保留? @gregm 如果你像我一样去 1 个选项卡 1 个活动,每个选项卡的后台堆栈将在选项卡切换时保留,因为活动实际上保持活动状态;他们只是暂停和恢复。我不知道在 TabActivity 中切换选项卡时是否有办法使活动被销毁并重新创建。但是,如果您像我建议的那样替换活动中的片段,它们销毁(并在弹出回栈时重新创建)。因此,任何时候每个选项卡最多只有一个片段。【参考方案3】:

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

说实话,这似乎是一件非常值得怀疑的事情。我无法想象它会产生一个不错的用户界面——如果后退键会根据我所在的选项卡做不同的事情,特别是如果后退键在顶部也有关闭整个活动的正常行为堆栈...听起来很讨厌。

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

【讨论】:

不要那样做。该框架几乎没有用处。它不会为您提供对此类事情的自动支持,正如我所说,我无法想象会产生良好的用户体验,除非在非常特殊的情况下,无论如何您都需要仔细控制后退行为。 这种类型的导航,然后你有标签和每个标签上的页面层次结构对于 iPhone 应用程序来说是非常常见的(你可以查看 App Store 和 iPod 应用程序)。我发现他们的用户体验相当不错。 这太疯狂了。 iPhone 甚至没有后退按钮。有 API 演示展示了非常简单的代码来实现选项卡中的片段。被问到的问题是关于每个选项卡有不同的后退堆栈,我的回答是框架不提供这个自动,因为从语义上讲,后退按钮的作用很可能是一个蹩脚的用户经验。如果你愿意的话,你可以很容易地自己实现返回语义。 同样,iPhone 没有返回按钮,因此它在语义上没有 Android 那样的返回堆栈行为。此外,“最好坚持活动并为自己节省大量时间”在这里没有任何意义,因为活动不允许您将维护选项卡放在具有自己不同后栈的 UI 中;事实上,活动的后台管理不如 Fragment 框架提供的灵活。 @hackbod 我正在尝试遵循您的观点,但在实现自定义回栈行为时遇到了麻烦。我意识到参与了这个设计,你会对它的简单程度有深刻的了解。是否有可能有一个用于 OP 用例的演示应用程序,因为这确实是一种非常常见的情况,特别是对于我们这些必须为提出这些请求的客户编写和移植 iOS 应用程序的人......每个 FragmentActivity 中的片段 backstacks。【参考方案4】:

这可以通过 ChildFragmentManager 轻松实现

这是与相关项目相关的帖子。看看吧,

http://tausiq.wordpress.com/2014/06/06/android-multiple-fragments-stack-in-each-viewpager-tab/

【讨论】:

【参考方案5】:

存储对片段的强引用不是正确的方法。

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

任何一个都足以实现一个backstack。


使用putFragment,不是替换片段,而是分离旧片段并添加新片段。这就是框架对添加到后台堆栈的替换事务所做的事情。 putFragment 存储当前活动片段列表的索引,这些片段在方向更改期间由框架保存。

第二种方式,使用saveFragmentInstanceState,将整个片段状态保存到一个Bundle中,让您真正删除它,而不是分离。使用这种方法可以更轻松地操作后台堆栈,因为您可以随时弹出 Fragment。


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

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

我不希望用户通过按返回按钮从第三个屏幕返回注册屏幕。我也在它们之间做翻转动画(使用onCreateAnimation),所以hacky解决方案不会起作用,至少没有用户清楚地注意到有什么不对。

这是自定义 backstack 的一个有效用例,可以满足用户的期望......

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】:

免责声明:


我觉得这是发布相关解决方案的最佳位置,我为类似类型的问题工作过,这似乎是非常标准的 Android 问题。它不会为每个人解决问题,但它可能会帮助一些人。


如果您的片段之间的主要区别只是支持它们的数据(即,没有很多大的布局差异),那么您可能不需要实际替换片段,而只需换出底层数据并刷新查看。

以下是此方法的一个可能示例的描述:

我有一个使用 ListViews 的应用程序。列表中的每个项目都是具有一定数量子级的父级。当您点击该项目时,需要在与原始列表相同的 ActionBar 选项卡中打开一个包含这些子项的新列表。这些嵌套列表具有非常相似的布局(可能会在这里和那里进行一些条件调整),但数据不同。

此应用在初始父列表下方有几层后代,当用户尝试访问第一层之外的任何特定深度时,我们可能会或可能不会从服务器获得数据。因为列表是从数据库游标构造的,并且片段使用游标加载器和游标适配器来使用列表项填充列表视图,所以在注册点击时需要发生的一切是:

1) 使用适当的“to”和“from”字段创建一个新适配器,这些字段将匹配添加到列表中的新项目视图和新光标返回的列。

2) 将此适配器设置为 ListView 的新适配器。

3) 基于被单击的项目构建一个新的 URI,并使用新的 URI(和投影)重新启动光标加载器。在此示例中,URI 映射到特定查询,其中选择参数从 UI 向下传递。

4) 当新数据从 URI 加载完毕后,将与适配器关联的光标交换到新光标,然后列表将刷新。

由于我们不使用事务,因此没有与此关联的 backstack,因此您必须自己构建,或者在退出层次结构时反向播放查询。当我尝试这个时,查询速度足够快,我只需在 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 使用的部分列表。有一个解决方法,请参阅:description of problem 和workaround【参考方案7】:

我遇到了完全相同的问题,并实施了一个开源 github 项目,该项目涵盖了堆叠选项卡、后退和向上导航,并且经过了良好的测试和记录:

https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs

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

【讨论】:

【参考方案8】:

这是一个复杂的问题,因为 Android 只处理 1 个回栈,但这是可行的。我花了几天时间创建一个名为 Tab Stacker 的库,它完全符合您的要求:每个选项卡的片段历史记录。它是开源的并且有完整的文档,并且可以很容易地包含在 gradle 中。你可以在github上找到这个库:https://github.com/smart-fun/TabStacker

您也可以下载示例应用程序以查看行为是否符合您的需求:

https://play.google.com/apps/testing/fr.arnaudguyon.tabstackerapp

如果您有任何问题,请随时发送邮件。

【讨论】:

【参考方案9】:

我想提出我自己的解决方案,以防有人正在寻找并想尝试选择最适合他/她需要的解决方案。

https://github.com/drusak/tabactivity

创建库的目的很普通——像 iPhone 一样实现它。

主要优点:

使用带有 TabLayout 的 android.support.design 库; 每个选项卡都有自己使用 FragmentManager 的堆栈(不保存片段的引用); 支持深度链接(当您需要在其中打开特定选项卡和特定片段的级别时); 保存/恢复标签状态; 选项卡中片段的自适应生命周期方法; 很容易根据您的需要实施。

【讨论】:

谢谢,这很有帮助。除了Fragments 之外,我还需要使用ListFragments,所以我将BaseTabFragment.java 复制到BaseTabListFragment.java 并让它扩展ListFragment。然后我不得不更改代码中的各个部分,它总是假设需要一个 BaseTabFragment。有没有更好的办法? 不幸的是,没有考虑 ListFragment。从技术上讲,这是正确的解决方案,但需要对 TabFragment 及其 instanceOf BaseTabListFragment 进行额外检查。另一种在内部使用带有 ListView 的 Fragment 的方法(与实现的 ListFragment 完全相同)。我会考虑的。谢谢你指点我!【参考方案10】:

一个简单的解决方案:

每次更改选项卡/根视图调用时:

fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);

它将清除 BackStack。请记住在更改根片段之前调用它。

并用这个添加片段:

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

【讨论】:

【参考方案11】:

这个话题非常有趣和有用。 感谢 Krishnabhadra 的解释和代码,我使用了您的代码并进行了一些改进,允许从更改配置(主要是旋转)中保留堆栈、currentTab 等。 在真实的 4.0.4 和 2.3.6 设备上测试,未在模拟器上测试

我在“AppMainTabActivity.java”上更改了这部分代码,其余保持不变。 也许 Krishnabhadra 会在他的代码中添加这个。

在创建时恢复数据:

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
     */

  //if we are recreating this activity...
    if (savedInstanceState!=null) 
         mStacks = (HashMap<String, Stack<Fragment>>) savedInstanceState.get("stack");
         mCurrentTab = savedInstanceState.getString("currentTab");
    
    else 
    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.setup();

    initializeTabs();

  //set the listener the last, to avoid overwrite mCurrentTab everytime we add a new Tab
    mTabHost.setOnTabChangedListener(listener);

保存变量并放入Bundle:

 //Save variables while recreating
@Override
public void onSaveInstanceState(Bundle outState) 
    super.onSaveInstanceState(outState);
    outState.putSerializable("stack", mStacks);
    outState.putString("currentTab", mCurrentTab);
    //outState.putInt("tabHost",mTabHost);

如果存在以前的 CurrentTab,则设置此项,否则创建一个新 Tab_A:

public void initializeTabs()
    /* Setup your tab icons and content views.. Nothing special in this..*/
    TabHost.TabSpec spec    =   mTabHost.newTabSpec(AppConstants.TAB_A);

    spec.setContent(new TabHost.TabContentFactory() 
        public View createTabContent(String tag) 
            return findViewById(R.id.realtabcontent);
        
    );
    spec.setIndicator(createTabView(R.drawable.tab_a_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_b_state_btn));
    mTabHost.addTab(spec);

//if we have non default Tab as current, change it
    if (mCurrentTab!=null) 
        mTabHost.setCurrentTabByTag(mCurrentTab);
     else 
        mCurrentTab=AppConstants.TAB_A;
        pushFragments(AppConstants.TAB_A, new AppTabAFirstFragment(), false,true);
    

我希望这对其他人有所帮助。

【讨论】:

这是错误的。当使用 Bundle 调用 onCreate 时,这些片段将与屏幕上显示的片段不同,并且您正在泄漏旧片段,除非您使用的是 setRetainInstance。如果 ActivityManager “保存”了你的 Activity,因为 Fragment 既不是 Serializable 也不是 Parcelable,当用户返回到你的 Activity 时,它会崩溃。【参考方案12】:

我建议不要使用基于 HashMap 的 backstack> “不保留活动”模式中有很多错误。 如果您深入片段堆栈,它将无法正确恢复状态。 并且也将在嵌套的地图片段中出现(带有例外:片段未找到 ID 视图)。 因为后台\前台应用程序后的 HashMap> 将为空

我优化了上面的代码以使用片段的后台堆栈

底部TabView

主要活动类

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.widget.ImageView;
import android.widget.TabHost;
import android.widget.TextView;

import com.strikersoft.nida.R;
import com.strikersoft.nida.abstractActivity.BaseActivity;
import com.strikersoft.nida.screens.tags.mapTab.MapContainerFragment;
import com.strikersoft.nida.screens.tags.searchTab.SearchFragment;
import com.strikersoft.nida.screens.tags.settingsTab.SettingsFragment;

public class TagsActivity extends BaseActivity 
    public static final String M_CURRENT_TAB = "M_CURRENT_TAB";
    private TabHost mTabHost;
    private String mCurrentTab;

    public static final String TAB_TAGS = "TAB_TAGS";
    public static final String TAB_MAP = "TAB_MAP";
    public static final String TAB_SETTINGS = "TAB_SETTINGS";

    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
        getActionBar().hide();
        setContentView(R.layout.tags_activity);

        mTabHost = (TabHost) findViewById(android.R.id.tabhost);

        mTabHost.setup();

        if (savedInstanceState != null) 
            mCurrentTab = savedInstanceState.getString(M_CURRENT_TAB);
            initializeTabs();
            mTabHost.setCurrentTabByTag(mCurrentTab);
            /*
            when resume state it's important to set listener after initializeTabs
            */
            mTabHost.setOnTabChangedListener(listener);
         else 
            mTabHost.setOnTabChangedListener(listener);
            initializeTabs();
        
    

    private View createTabView(final int id, final String text) 
        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));
        TextView textView = (TextView) view.findViewById(R.id.tab_text);
        textView.setText(text);
        return view;
    

    /*
    create 3 tabs with name and image
    and add it to TabHost
     */
    public void initializeTabs() 

        TabHost.TabSpec spec;

        spec = mTabHost.newTabSpec(TAB_TAGS);
        spec.setContent(new TabHost.TabContentFactory() 
            public View createTabContent(String tag) 
                return findViewById(R.id.realtabcontent);
            
        );
        spec.setIndicator(createTabView(R.drawable.tab_tag_drawable, getString(R.string.tab_tags)));
        mTabHost.addTab(spec);

        spec = mTabHost.newTabSpec(TAB_MAP);
        spec.setContent(new TabHost.TabContentFactory() 
            public View createTabContent(String tag) 
                return findViewById(R.id.realtabcontent);
            
        );
        spec.setIndicator(createTabView(R.drawable.tab_map_drawable, getString(R.string.tab_map)));
        mTabHost.addTab(spec);


        spec = mTabHost.newTabSpec(TAB_SETTINGS);
        spec.setContent(new TabHost.TabContentFactory() 
            public View createTabContent(String tag) 
                return findViewById(R.id.realtabcontent);
            
        );
        spec.setIndicator(createTabView(R.drawable.tab_settings_drawable, getString(R.string.tab_settings)));
        mTabHost.addTab(spec);

    

    /*
    first time listener will be trigered immediatelly after first: mTabHost.addTab(spec);
    for set correct Tab in setmTabHost.setCurrentTabByTag ignore first call of listener
    */
    TabHost.OnTabChangeListener listener = new TabHost.OnTabChangeListener() 
        public void onTabChanged(String tabId) 

            mCurrentTab = tabId;

            if (tabId.equals(TAB_TAGS)) 
                pushFragments(SearchFragment.getInstance(), false,
                        false, null);
             else if (tabId.equals(TAB_MAP)) 
                pushFragments(MapContainerFragment.getInstance(), false,
                        false, null);
             else if (tabId.equals(TAB_SETTINGS)) 
                pushFragments(SettingsFragment.getInstance(), false,
                        false, null);
            

        
    ;

/*
Example of starting nested fragment from another fragment:

Fragment newFragment = ManagerTagFragment.newInstance(tag.getMac());
                TagsActivity tAct = (TagsActivity)getActivity();
                tAct.pushFragments(newFragment, true, true, null);
 */
    public void pushFragments(Fragment fragment,
                              boolean shouldAnimate, boolean shouldAdd, String tag) 
        FragmentManager manager = getFragmentManager();
        FragmentTransaction ft = manager.beginTransaction();
        if (shouldAnimate) 
            ft.setCustomAnimations(R.animator.fragment_slide_left_enter,
                    R.animator.fragment_slide_left_exit,
                    R.animator.fragment_slide_right_enter,
                    R.animator.fragment_slide_right_exit);
        
        ft.replace(R.id.realtabcontent, fragment, tag);

        if (shouldAdd) 
            /*
            here you can create named backstack for realize another logic.
            ft.addToBackStack("name of your backstack");
             */
            ft.addToBackStack(null);
         else 
            /*
            and remove named backstack:
            manager.popBackStack("name of your backstack", FragmentManager.POP_BACK_STACK_INCLUSIVE);
            or remove whole:
            manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
             */
            manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
        
        ft.commit();
    

    /*
    If you want to start this activity from another
     */
    public static void startUrself(Activity context) 
        Intent newActivity = new Intent(context, TagsActivity.class);
        newActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(newActivity);
        context.finish();
    

    @Override
    public void onSaveInstanceState(Bundle outState) 
        outState.putString(M_CURRENT_TAB, mCurrentTab);
        super.onSaveInstanceState(outState);
    

    @Override
    public void onBackPressed()
        super.onBackPressed();
    

tags_activity.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_
    android:layout_>

    <LinearLayout
        android:orientation="vertical"
        android:layout_
        android:layout_>

        <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_
            android:layout_
            android:layout_weight="0"/>
        <FrameLayout
            android:id="@+android:id/realtabcontent"
            android:background="@drawable/bg_main_app_gradient"
            android:layout_
            android:layout_
            android:layout_weight="1"/>
        <TabWidget
            android:id="@android:id/tabs"
            android:background="#EAE7E1"
            android:orientation="horizontal"
            android:layout_
            android:layout_
            android:layout_weight="0"/>
    </LinearLayout>
</TabHost>

tags_icon.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/tabsLayout"
    android:layout_
    android:layout_
    android:background="@drawable/bg_tab_gradient"
    android:gravity="center"
    android:orientation="vertical"
    tools:ignore="contentDescription" >

    <ImageView
        android:id="@+id/tab_icon"
        android:layout_marginTop="4dp"
        android:layout_
        android:layout_ />
    <TextView 
        android:id="@+id/tab_text"
        android:layout_marginBottom="3dp"
        android:layout_
        android:layout_
        android:textColor="@color/tab_text_color"/>

</LinearLayout>

【讨论】:

以上是关于使用 Fragments 为 Android 中的每个选项卡单独返回堆栈的主要内容,如果未能解决你的问题,请参考以下文章

Android11.5 创建和管理Fragments

Android Navigation Drawer Show Up Indicator for Lower Level Fragments

Fragments 中的成员变量与 setArguments

是 Android Fragments View 还是 ViewGroup

Android Fragment使用 嵌套Fragments (Nested Fragments) 的使用及常见错误

Android Fragment使用 嵌套Fragments (Nested Fragments) 的使用及常见错误