使用Fragments为Android中的每个选项卡分离后退堆栈


158

我正在尝试在Android应用中实现导航标签。由于不推荐使用TabActivity和ActivityGroup,因此我想使用Fragments来实现它。

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

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

同样,在旋转设备时保持每个标签的状态也很重要。

马丁

Answers:


23

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

老实说,这似乎是一件值得怀疑的事情。我无法想象它会导致一个不错的用户界面-如果后退键将根据我的选项卡执行不同的操作,特别是如果后退键也具有正常的行为,即当它位于顶部时,将关闭整个活动堆栈...听起来很讨厌。

如果您尝试构建类似Web浏览器UI的内容,则要获得用户自然的UX,将涉及到许多细微的行为调整,具体取决于上下文,因此您肯定需要自己做一个后台堆栈管理而不是依赖框架中的某些默认实现。例如,请尝试注意后退键如何以各种方式进入和退出标准浏览器。(浏览器中的每个“窗口”本质上都是一个标签。)


7
不要那样做 而且该框架几乎没有用。它不会自动为您提供这种支持,正如我说过的那样,我无法想象会带来令人满意的用户体验,除非在非常特殊的情况下,您仍然需要仔细控制背部的行为。
hackbod 2011年

9
例如,这种导航类型具有选项卡,并且每个选项卡上的页面层次结构对于iPhone应用程序来说非常常见(例如,您可以检查App Store和iPod应用程序)。我发现他们的用户体验相当不错。
德米特里·里亚德年科2011年

13
疯了吧。iPhone甚至没有后退按钮。有一些API演示显示了非常简单的代码来实现选项卡中的片段。提出的问题是每个选项卡具有不同的后退堆栈,我的回答是该框架不会自动提供此功能,因为从语义上说,后退按钮的作用很可能会带来糟糕的用户体验。如果需要,您可以自己轻松实现后语义。
hackbod 2011年

4
同样,iPhone没有后退按钮,因此它在语义上没有像Android这样的后退行为。同样,“更好地坚持活动并节省很多时间” 在这里没有任何意义,因为活动不允许您将维护选项卡放置在具有各自不同后置堆栈的UI中。实际上,活动的后栈管理不如Fragment框架所提供的灵活。
hackbod 2011年

22
@hackbod我正在尝试遵循您的观点,但是在实现自定义后向堆栈行为时遇到了麻烦。我意识到参与此设计后,您将对它的简单程度有深刻的了解。是否有针对OP用例的演示应用程序,因为这确实是非常普遍的情况,尤其是对于那些必须为提出这些请求的客户编写和移植iOS应用程序的我们来说。每个FragmentActivity中的片段堆栈。
理查德·勒·马苏里尔

138

这个问题我迟到了。但是,由于该线程对我很有帮助,因此我认为我最好在这里贴上两便士。

我需要这样的屏幕流程(一个简约的设计,其中包含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){
    }
}

应用程序中的所有片段都可以扩展此Base类。如果您想使用特殊的片段,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 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可以管理堆栈,方向更改或带有多个片段的制表符很容易),这使我们回到了本Q /中的原始讨论一个线程。现在
没事

1
@Renjith这是因为每次切换选项卡时都会重新创建片段。即使这样一次,也不要认为片段会在选项卡切换之间重复使用。当我从A选项卡切换到B时,A选项卡已从内存中释放。因此,将数据保存在活动中,并且每次尝试从服务器获取活动之前都要检查活动是否有数据。
Krishnabhadra

2
@Krishnabhadra好的,听起来好多了。让我更正,以防万一我错了。根据您的示例,只有一项活动,因此只有一个捆绑包。在BaseFragment中创建适配器实例(引用您的项目)并在其中保存数据。只要要构建视图,就使用它们。
伦吉斯

1
得到它的工作。非常感谢。上传整个项目是一个好主意!:-)
Vinay W

96

我们必须实现与您最近为应用描述的行为完全相同的行为。应用程序的屏幕和整体流程已经定义,因此我们必须坚持使用(这是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;如果按下Back,则应该返回屏幕2;再次返回,他进入屏幕1;切换至标签3,他又进入屏幕6。

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

MainTabActivity
  ActivityInTab1
    Fragment1 -> Fragment2 -> Fragment3
  ActivityInTab2
    Fragment4
  ActivityInTab3
    Fragment5 -> Fragment6

每个ActivityInTab一次仅保存一个片段,并且知道如何将一个片段替换为另一个片段(与ActvityGroup几乎相同)。很酷的事情是,以这种方式为每个选项卡保留单独的后堆栈很容易。

每个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_width="fill_parent"
    android:layout_height="fill_parent"
    android:isScrollContainer="true">
</RelativeLayout>

如您所见,每个选项卡的视图布局都是相同的。那是因为它只是一个称为content的FrameLayout,它将保存每个片段。片段是具有每个屏幕视图的片段。

只是为了获得奖励积分,我们还添加了一些小代码以在用户按下Back(返回)时显示一个确认对话框,没有更多片段可以返回:

// 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的方式。随意在评论中解开这个答案:)

一个迹象表明,该解决方案不是很好,最近我不得不向该应用程序添加一些导航功能。某种怪异的按钮,应该使用户从一个选项卡进入另一个选项卡并进入嵌套屏幕。以编程方式进行此操作非常麻烦,因为谁知道谁的问题并处理片段和活动的实际实例化和初始化时间。我认为,如果这些屏幕和选项卡全都只是Views,那会容易得多。


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


13
很好的答案,但我认为很少有人会看到这一点。我选择了完全相同的路径(正如您从上一个答案的对话中看到的那样),我对它不满意,就像您一样。我认为Google确实搞砸了这些片段,因为该API并未涵盖主要用例。您可能会遇到的另一个问题是无法将片段嵌入到另一个片段中。
德米特里·里亚德年科2011年

感谢您的留言。是的,我完全同意片段API。我已经遇到了嵌套片段的问题(这就是为什么我们采用“用一个片段替换另一个片段”的方法)。
流行病2011年

1
我已经通过所有活动实现了这一点。我不喜欢自己得到的东西,我将尝试Fragments。那与您的经历相反!活动中有很多实现,可以处理每个选项卡中子视图的生命周期,也可以实现自己的后退按钮。另外,您不能仅保留对所有视图的引用,否则将消耗大量内存。我希望Fragments能够:1)通过清晰的内存支持Fragments的生命周期,以及2)帮助实现后退按钮功能另外,如果您在此过程中使用Fragments,那么在Tablets上运行会不会更容易?
gregm 2011年

用户切换标签时会发生什么?Fragment堆栈会被删除吗?如何确保后盖仍然存在?
gregm

1
@gregm如果像我一样进行1个<-> 1个活动,则切换选项卡时将保留每个选项卡的后退堆栈,因为这些活动实际上一直处于活动状态。他们只是暂停和恢复。我不知道在TabActivity中切换选项卡时是否有办法使活动被破坏并重新创建。但是,如果您按照我的建议替换了活动中的片段,它们将销毁(并在弹出Backstack时重新创建)。因此,您随时可以在每个标签页中最多保留一个片段。
流行病2011年


6

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

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

任一个都足以实现Backstack。


使用 putFragment,而不是替换Fragment,而是分离旧的片段并添加新的片段。这就是框架对添加到Backstack中的替换事务所做的工作。putFragment将索引存储到活动片段的当前列表中,这些片段在方向更改时由框架保存。

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


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

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];
            }
    };
}

2

免责声明:


我觉得这是发布有关类似问题的最佳解决方案的最佳地点,该解决方案似乎是非常标准的Android内容。它不会为每个人解决问题,但可能会有所帮助。


如果片段之间的主要区别仅是备份它们的数据(即,没有太大的布局差异),则您可能不需要实际替换片段,而只需交换基础数据并刷新视图。

这是此方法的一个可能示例的说明:

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

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

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

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

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

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使用的部分列表。有一种解决方法,请参阅:问题解决方法的说明
courtf 2012年


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一样实现它。

主要优点:

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

谢谢,这对您很有帮助。ListFragment除了s之外,我还需要使用s,Fragment所以我将BaseTabFragment.java复制到BaseTabListFragment.java并扩展了ListFragment。然后,我不得不更改代码中的各个部分,在这些部分中,人们总是假定它们期望有BaseTabFragment。有没有更好的办法?
primehalo

不幸的是,没有想到ListFragment。从技术上讲,这是正确的解决方案,但将需要对TabFragment及其instanceOf BaseTabListFragment进行附加检查。另一种使用带有内部ListView的Fragment的方法(与实现的ListFragment完全相同)。我会考虑的。感谢您指出我的意思!
卡苏德(Kasurd),2016年

1

一个简单的解决方案:

每次更改标签页/根视图调用时:

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


-1

这个线程非常非常有趣和有用。
感谢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不可序列化或不可打包,当用户返回到您的Activity时,它将崩溃。
sergio91pt 2014年

-1

我建议不要使用基于HashMap的Backstack>“请勿保留活动”模式下存在很多错误。如果您深入片段的堆栈,它将无法正确恢复状态。并且还将嵌套在嵌套的地图片段中(例如:未找到ID的片段)。Coz HashMap> background \ foreground应用程序之后为null

我优化了上面的代码以使用fragment的backstack

它是底部的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_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_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:background="@drawable/bg_main_app_gradient"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"/>
        <TabWidget
            android:id="@android:id/tabs"
            android:background="#EAE7E1"
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            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_width="fill_parent"
    android:layout_height="fill_parent"
    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_width="wrap_content"
        android:layout_height="wrap_content" />
    <TextView 
        android:id="@+id/tab_text"
        android:layout_marginBottom="3dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/tab_text_color"/>

</LinearLayout>

在此处输入图片说明

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.