我正在尝试在 Android 应用程序中实现导航选项卡。由于 TabActivity 和 ActivityGroup 已被弃用,我想改用 Fragments 来实现它。
我知道如何为每个选项卡设置一个片段,然后在单击选项卡时切换片段。但是我怎样才能为每个选项卡设置一个单独的后台堆栈呢?
例如,Fragment A 和 B 将位于 Tab 1 下,Fragment C 和 D 在 Tab 2 下。当应用程序启动时,会显示 Fragment A 并选择 Tab 1。然后,片段A可能被片段B替换。选择选项卡2时应显示片段C。如果选项卡 1 被选中,那么应该再次显示片段 B。此时应该可以使用后退按钮显示片段 A。
此外,重要的是在设备旋转时保持每个选项卡的状态。
BR马丁
我对这个问题太晚了。但是由于这个线程对我来说非常有用和有帮助,我想我最好把我的两便士贴在这里。
我需要这样的屏幕流程(一个简约的设计,每个选项卡中有 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.好的,主选项卡活动-请查看代码中的注释..
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 中的第一个片段,所有选项卡都类似)
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(我们使用 Fragment 的支持库)和 Fragments 的混合物破解了解决方案。回想起来,我很确定这不是最好的架构决策,但我们设法让事情顺利进行。如果我不得不再做一次,我可能会尝试做一个更基于活动的解决方案(没有片段),或者尝试只为选项卡设置一个活动,让所有其余的都是视图(我发现更多总体上比活动可重用)。
所以要求是在每个选项卡中都有一些选项卡和可嵌套的屏幕:
tab 1
screen 1 -> screen 2 -> screen 3
tab 2
screen 4
tab 3
screen 5 -> 6
ETC...
比如说:用户从标签 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_width="fill_parent"
android:layout_height="fill_parent"
android:isScrollContainer="true">
</RelativeLayout>
如您所见,每个选项卡的视图布局都是相同的。那是因为它只是一个名为 content 的 FrameLayout ,它将保存每个片段。片段是具有每个屏幕视图的片段。
只是为了加分,我们还添加了一些小代码来在用户按下返回并且没有更多片段可以返回时显示确认对话框:
// In ActivityInTab.java...
@Override
public void onBackPressed() {
FragmentManager manager = getSupportFragmentManager();
if (manager.getBackStackEntryCount() > 0) {
// If there are back-stack entries, leave the FragmentActivity
// implementation take care of them.
super.onBackPressed();
} else {
// Otherwise, ask user if he wants to leave :)
showExitDialog();
}
}
差不多就是这样的设置。如您所见,每个 FragmentActivity(或者只是 Android >3 中的 Activity)都使用自己的 FragmentManager 处理所有的回栈。
像 ActivityInTab1 这样的活动非常简单,它只会显示它的第一个片段(即屏幕):
public class ActivityInTab1 extends ActivityInTab {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
navigateTo(new Fragment1());
}
}
然后,如果一个片段需要导航到另一个片段,它必须做一些讨厌的转换......但这还不错:
// In Fragment1.java for example...
// Need to navigate to Fragment2.
((ActivityIntab) getActivity()).navigateTo(new Fragment2());
差不多就是这样。我很确定这不是一个非常规范(而且大部分肯定不是很好)的解决方案,所以我想问问经验丰富的 Android 开发人员,什么是实现此功能的更好方法,如果这不是“它是如何在 Android 中完成”,如果您能指出一些链接或材料来解释这是处理此问题的 Android 方式(选项卡、选项卡中的嵌套屏幕等),我将不胜感激。随意在评论中撕开这个答案:)
这个解决方案不是很好的一个标志是,最近我不得不向应用程序添加一些导航功能。一些奇怪的按钮,应该将用户从一个选项卡带到另一个选项卡并进入嵌套屏幕。以编程方式执行此操作很麻烦,因为谁知道谁的问题以及处理何时实际实例化和初始化的片段和活动。我认为如果这些屏幕和选项卡都只是视图,那会容易得多。
最后,如果您需要在方向变化中幸存下来,请务必使用 setArguments/getArguments 创建您的片段。如果您在片段的构造函数中设置实例变量,您将被搞砸。但幸运的是,这很容易解决:只需将所有内容保存在构造函数的 setArguments 中,然后在 onCreate 中使用 getArguments 检索这些内容以使用它们。
该框架目前不会自动为您执行此操作。您将需要为每个选项卡构建和管理自己的后台堆栈。
老实说,这似乎是一件非常值得怀疑的事情。我无法想象它会产生一个不错的用户界面——如果后退键会根据我所在的选项卡做不同的事情,特别是如果后退键在顶部也有关闭整个活动的正常行为堆栈...听起来很讨厌。
如果您正在尝试构建类似于 Web 浏览器 UI 的东西,要获得对用户而言自然的 UX,将涉及根据上下文对行为进行许多细微调整,因此您肯定需要自己做后台堆栈管理而不是依赖框架中的一些默认实现。例如,尝试注意后退键如何以各种方式与标准浏览器交互,您可以进出它。 (浏览器中的每个“窗口”本质上都是一个选项卡。)
这可以通过 ChildFragmentManager 轻松实现
这是与相关项目有关的帖子。看一看,
http://tausiq.wordpress.com/2014/06/06/android-multiple-fragments-stack-in-each-viewpager-tab/
存储对片段的强引用不是正确的方法。
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];
}
};
}
免责声明:
我觉得这是发布我为类似类型的问题工作过的相关解决方案的最佳位置,这似乎是非常标准的 Android 问题。它不会为每个人解决问题,但它可能会帮助一些人。
如果您的片段之间的主要区别只是支持它们的数据(即,没有很多大的布局差异),那么您可能不需要实际替换片段,而只需换出底层数据并刷新视图。
以下是此方法的一个可能示例的描述:
我有一个使用 ListViews 的应用程序。列表中的每个项目都是具有一定数量子级的父级。当您点击该项目时,需要在与原始列表相同的 ActionBar 选项卡中打开一个包含这些子项的新列表。这些嵌套列表具有非常相似的布局(可能会在这里和那里进行一些条件调整),但数据不同。
这个应用程序在初始父列表下方有几层后代,当用户尝试访问第一层之外的任何特定深度时,我们可能会或可能不会从服务器获得数据。因为列表是从数据库游标构造的,并且片段使用游标加载器和游标适配器来使用列表项填充列表视图,所以当点击注册时需要发生的一切是:
1) 创建一个新适配器,其中包含适当的“to”和“from”字段,这些字段将匹配添加到列表中的新项目视图和新光标返回的列。
2)将此适配器设置为ListView的新适配器。
3) 基于被单击的项目构建一个新的 URI,并使用新的 URI(和投影)重新启动光标加载器。在此示例中,URI 映射到具有从 UI 传递下来的选择参数的特定查询。
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
我希望这可以帮助别人!
我遇到了完全相同的问题,并实现了一个开源 github 项目,该项目涵盖了堆叠选项卡、后退和向上导航,并且经过了良好的测试和记录:
https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs
这是一个简单的小型框架,用于导航选项卡和片段切换以及向上和向后导航的处理。每个选项卡都有自己的片段堆栈。它使用 ActionBarSherlock 并且兼容回 API 级别 8。
这是一个复杂的问题,因为 Android 只处理 1 个回栈,但这是可行的。我花了几天时间创建一个名为 Tab Stacker 的库,它完全符合您的要求:每个选项卡的片段历史记录。它是开源的并且有完整的文档,并且可以很容易地包含在 gradle 中。您可以在 github 上找到该库:https://github.com/smart-fun/TabStacker
您还可以下载示例应用程序以查看该行为是否符合您的需求:
https://play.google.com/apps/testing/fr.arnaudguyon.tabstackerapp
如果您有任何问题,请随时发送邮件。
我想建议我自己的解决方案,以防有人正在寻找并想尝试选择最适合他/她需求的解决方案。
https://github.com/drusak/tabactivity
创建库的目的很普通——像 iPhone 一样实现它。
主要优点:
使用带有 TabLayout 的 android.support.design 库;
每个选项卡都有自己的使用 FragmentManager 的堆栈(不保存片段的引用);
支持深度链接(当您需要在其中打开特定选项卡和特定片段的级别时);
保存/恢复标签状态;
选项卡中片段的自适应生命周期方法;
很容易实现您的需求。
Fragment
之外,我还需要使用 ListFragment
,因此我将 BaseTabFragment.java 复制到 BaseTabListFragment.java 并让它扩展 ListFragment。然后我不得不更改代码中的各个部分,它总是假设需要一个 BaseTabFragment。有没有更好的办法?
一个简单的解决方案:
每次更改选项卡/根视图调用时:
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
更改。
这个线程非常非常有趣和有用。感谢 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 的 backstack> 在“不保留活动”模式中有很多错误。如果您深入片段堆栈,它将无法正确恢复状态。并且也将在嵌套的地图片段中出现(带有例外:片段未找到 ID 视图)。 Coz 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_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>
https://i.stack.imgur.com/B4UBg.png
不定期副业成功案例分享