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

我试图在Android应用中实现导航标签。 由于TabActivity和ActivityGroup已被弃用,我想使用Fragments来实现它。

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

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

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

BR Martin


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

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

如果你试图构建一个类似于Web浏览器UI的东西,为了获得对用户来说很自然的用户体验,将会根据上下文对行为进行很多细微的调整,所以你一定要做自己的后退堆栈而不是依赖框架中的一些默认实现。 举一个例子,请尝试关注后退键如何以各种方式与标准浏览器进行交互。 (浏览器中的每个“窗口”基本上都是一个选项卡。)


这个问题我迟到了。 但是因为这个线程对我来说很有帮助,所以我觉得我最好在这里发表我的两个便士。

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

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

过去我有相同的要求,而且我使用TabActivityGroup (当时已弃用)和Activities。 这次我想使用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.创建一些选项卡标识符,可在项目的任何地方访

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(Tab A中的第一个片段,与所有Tabs相似)

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。


我们必须实现与您最近为应用程序描述的行为完全相同的行为。 应用程序的屏幕和整体流程已经定义,所以我们必须坚持下去(这是一个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一次只保存一个片段,并知道如何替换另一个片段(几乎与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>

正如你所看到的,每个选项卡的视图布局是相同的。 这是因为它只是一个称为内容的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中的Activity> 3)正在利用它自己的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检索这些内容即可使用它们。

链接地址: http://www.djcxy.com/p/60583.html

上一篇: Separate Back Stack for each tab in Android using Fragments

下一篇: Switch activities using ActivityGroup in tab(Android)