使用片段为 Android 中的每个标签分离回栈

我试图在一个 Android 应用程序中实现导航标签。由于 TabActivity 和 ActivityGroup 不被推荐,我想用片段来实现它。

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

例如,片段 A 和 B 在表1下,片段 C 和 D 在表2下。当应用程序启动时,会显示片段 A,并选择 Tab 1。那么片段 A 可能会被片段 B 所取代。当选择 Tab 2时,应显示片段 C。如果选择了 Tab 1,那么应该再次显示片段 B。此时,应该可以使用后退按钮显示片段 A。

此外,当设备旋转时,维护每个选项卡的状态也很重要。

BR 马丁

78528 次浏览

框架当前不会自动为您完成这项工作。您需要为每个选项卡构建和管理自己的后台堆栈。

说实话,这看起来真的是件值得怀疑的事。我无法想象它会产生一个像样的 UI ——如果后退键根据我所在的选项卡做不同的事情,特别是如果后退键在堆栈顶部也有关闭整个活动的正常行为... 听起来很恶心。

如果你试图构建一个类似于 web 浏览器 UI 的东西,要获得一个对用户来说很自然的用户体验,需要根据上下文进行许多微妙的行为调整,所以你肯定需要自己进行后台堆栈管理,而不是依赖框架中的某些默认实现。举例来说,请注意后退键如何与标准浏览器以各种方式进出交互。(浏览器中的每个“窗口”基本上都是一个标签页。)

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

我们使用 TabActivity、 FragmentActivity (我们使用了片段的支持库)和片段的混合体对解决方案进行了破解。回顾过去,我很肯定这不是最好的架构决策,但我们设法让它工作。如果我不得不再次这样做,我可能会尝试做一个更基于活动的解决方案(没有片段) ,或者尝试只有一个活动的选项卡,让所有其余的视图(我发现这是更多的重用比活动整体)。

所以需要在每个选项卡中都有一些选项卡和可嵌套的屏幕:

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。

应用程序中的主要活动是 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,将保存每个片段。片段是每个屏幕的视图。

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

// 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();
}
}

差不多就是这样。正如您所看到的,每个片段活动(或者仅仅是 Android > 3中的活动)使用它自己的片段管理器来处理所有的后堆栈。

像 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 的方式来处理这个(标签,嵌套的屏幕在标签,等等)。你可以在评论中把这个答案撕碎:)

这个解决方案不太好的一个迹象是,最近我不得不向应用程序添加一些导航功能。一些奇怪的按钮,应该把用户从一个标签到另一个进入一个嵌套的屏幕。以编程方式执行这些操作是一件痛苦的事情,因为谁也不知道什么时候会出现问题,什么时候会有片段和活动被实例化和初始化。我认为,如果那些屏幕和标签都只是视图的话,事情会简单得多。


最后,如果您需要在方向更改中存活下来,使用 setArguments/getArguments 创建片段是非常重要的。如果你在片段的构造函数中设置了实例变量,你就完蛋了。但幸运的是,这个问题很容易解决: 只需在构造函数中保存 setArguments 中的所有内容,然后在 onCreate 中使用 getArguments 检索这些内容。

免责声明:


我觉得这是一个最好的地方张贴一个相关的解决方案,我已经工作了类似的问题,似乎是相当标准的 Android 的东西。这并不能解决所有人的问题,但可能对某些人有所帮助。


如果片段之间的主要区别只是备份的数据(即,没有很大的布局差异) ,那么您可能不需要实际替换片段,只需要交换底层数据并刷新视图。

下面是这种方法的一个可能的例子:

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

这个应用程序在最初的父列表下面有几层后代,当用户试图访问超过第一层的任何特定深度时,我们可能有也可能没有来自服务器的数据。因为列表是由数据库游标构造的,而且片段使用游标加载程序和游标适配器用列表项填充列表视图,所以注册单击时需要做的就是:

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

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

3)根据单击的项目构建一个新的 URI,并使用新的 URI (和投影)重新启动光标加载程序。在这个例子中,URI 被映射到特定的查询,选择参数从 UI 传递下来。

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

由于我们没有使用事务,因此没有与此相关的回栈,所以您必须要么构建自己的事务,要么在退出层次结构时反向运行查询。当我尝试这样做时,查询的速度足够快,我只需要在 oNBackPress ()中再次执行它们,直到我处于层次结构的顶部,这时框架再次接管后退按钮。

如果你发现自己处于类似的情况,请务必阅读以下文件: Http://developer.android.com/guide/topics/ui/layout/listview.html

Http://developer.android.com/reference/android/support/v4/app/loadermanager

我希望这对谁有帮助!

在使用此解决方案之前请阅读此文件

哇,我还是不敢相信这个答案是这个帖子里得票最多的。请不要盲目地遵循这个实现。我在2012年写了这个解决方案(当时我还是 Android 的新手)。十年过去了,我发现这个解决方案存在严重的问题。

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

如果需要,我的答案可以通过上面的修改来使用。但是我不认为我们需要从头开始编写一个多层叠的导航实现。肯定有一个更好的现成的解决方案。我现在对 Android 不是很感兴趣,所以不能指出任何问题。

为了完整起见,我保留了原始答案。

原始答案

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

我需要一个这样的屏幕流程(一个简约的设计,两个标签和两个视图在每个标签) ,

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. 创建一些 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..
}

没什么好解释的。

好的,主标签活动-请通过代码注释..。

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 _ 嘛片段 _ 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 中的第一个片段,与所有 Tab 类似)

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

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

Https://github.com/sebastianbaltesobjectcode/persistentfragmenttabs

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

这个帖子很有意思,也很有用。
谢谢 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);
}
}

我希望这能帮到其他人。

我建议不要使用基于 HashMap > 的回溯 在“不保持活动”模式下有很多错误。 如果您深入到片段的堆栈中,它将无法正确地还原状态。 并且也将在嵌套的映射片段中进行抓取(例外情况: 片段没有找到 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();
}
}

Tag _ 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>

Tag _ 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>

enter image description here

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

片段管理器提供 putFragment(Bundle, String, Fragment)saveFragmentInstanceState(Fragment)

任何一个都足以实现一个回栈。


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

第二种方法,使用 saveFragmentInstanceState,将整个片段状态保存到一个 Bundle 中,从而允许您真正地删除它,而不是分离它。使用这种方法可以使回栈更容易操作,因为您可以随时弹出一个片段。


对于这个用例,我使用了第二种方法:

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

我不希望用户返回到注册屏幕,从第三个,通过按后退按钮。我还在它们之间做翻转动画(使用 onCreateAnimation) ,所以黑客的解决方案不会起作用,至少在用户没有清楚地注意到有些不对劲的情况下。

这是一个自定义回栈的有效用例,正如用户所期望的那样..。

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

这可以很容易地实现与儿童碎片管理器

这里有一篇关于这个相关项目的文章,

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

一个简单的解决办法:

每次更改 tab/root 视图调用时:

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改变。

我想建议我自己的解决方案,以防有人正在寻找,并想尝试选择最好的一个为他/她的需要。

Https://github.com/drusak/tabactivity

创建这个库的目的很平庸——像 iPhone 一样实现它。

主要优点:

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

这是一个复杂的问题,因为 Android 只能处理1个回栈,但这是可行的。我花了几天时间创建了一个名为 Tab Stacker 的库,它完全满足您的要求: 为每个选项卡创建一个片段历史记录。它是开放源代码和完整的文档,可以很容易地包括与梯度。您可以在 github: https://github.com/smart-fun/TabStacker上找到这个库

你也可以下载示例应用程序,看看它的行为是否符合你的需要:

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

如果你有任何问题,不要犹豫,投递邮件。