使用 Fragments 为 Android 中的每个选项卡单独的返回堆栈
我正在尝试在 Android 应用程序中实现导航选项卡。由于 TabActivity 和 ActivityGroup 已被弃用,我想使用 Fragments 来实现它。
我知道如何为每个选项卡设置一个片段,然后在单击选项卡时切换片段。但是如何为每个选项卡提供单独的返回堆栈呢?
例如,片段 A 和 B 将位于选项卡 1 下,片段 C 和 D 位于选项卡 2 下。应用程序启动时,将显示片段 A,并选择选项卡 1。然后片段 A 可能会替换为片段 B。选择选项卡 2 时,应显示片段 C。如果选择选项卡 1,则应再次显示片段 B。此时应该可以使用后退按钮来显示片段 A。
此外,在设备旋转时保持每个选项卡的状态也很重要。
BR 马丁
I'm trying to implement tabs for navigation in an Android app. Since TabActivity and ActivityGroup are deprecated I would like to implement it using Fragments instead.
I know how to set up one fragment for each tab and then switch fragments when a tab is clicked. But how can I have a separate back stack for each tab?
For an example Fragment A and B would be under Tab 1 and Fragment C and D under Tab 2. When the app is started Fragment A is shown and Tab 1 is selected. Then Fragment A might be replaced with Fragment B. When Tab 2 is selected Fragment C should be displayed. If Tab 1 is then selected Fragment B should once again be displayed. At this point it should be possible to use the back button to show Fragment A.
Also, it is important that the state for each tab is maintained when the device is rotated.
BR
Martin
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(12)
在使用此解决方案之前阅读此内容
哇,我仍然不敢相信这个答案是该线程中得票最多的答案。请不要盲目遵循此实施。我在 2012 年写了这个解决方案(当时我只是 Android 新手)。十年后,我发现这个解决方案存在一个严重的问题。
我正在存储对片段的硬引用以实现导航堆栈。这是一种糟糕的做法,会导致内存泄漏。让 FragmentManager 保存对片段的引用。如果需要,只需存储片段标识符即可。
如果需要的话可以使用我的答案进行上述修改。但我认为我们不需要从头开始编写多堆栈导航实现。肯定有一个更好的现成解决方案。我现在对Android不太感兴趣,所以不能指出任何。
为了完整起见,我保留原来的答案。
原始答案
我对这个问题太晚了。但由于这个帖子内容丰富,对我很有帮助,我想我最好在这里贴上我的两便士。
我需要这样的屏幕流程(每个选项卡中有 2 个选项卡和 2 个视图的简约设计),
我过去也有相同的要求,我使用
TabActivityGroup
(当时已弃用)来完成它时间)和活动。这次我想使用 Fragment。我就是这样做的。
1. 创建一个基础 Fragment 类
应用程序中的所有片段都可以扩展此基类。如果您想使用像
ListFragment
这样的特殊片段,您也应该为其创建一个基类。如果你完整阅读了这篇文章,你就会清楚onBackPressed()
和onActivityResult()
的用法。2. 创建一些 Tab 标识符,在项目中任何地方都可以
访问在这里解释一下..
3. 好的,主选项卡活动 - 请查看代码中的注释..
4. app_main_tab_fragment_layout.xml (如果有人感兴趣。)
5. AppTabAFirstFragment.java (第一个选项卡 A 中的片段,所有选项卡均类似)
这可能不是最完美和最正确的方法。但它对我来说效果很好。而且我只在纵向模式下有这个要求。我从来没有必要在支持两个方向的项目中使用这段代码。所以不能说我在那里面临什么样的挑战。
如果有人想要一个完整的项目,我已经将一个示例项目推送到
Read this before using this solution
Wow, I still can't believe this answer is the one with most votes in this thread. Please don't blindly follow this implementation. I wrote this solution in 2012 (when I was just a novice in Android). Ten years down the line, I can see there is a terrible issue with this solution.
I am storing hard reference to fragments to implement the navigation stack. It is a terrible practice and would result in memory leak. Let the FragmentManager saves the reference to fragments. Just store the fragment identifier if needed.
My answer can be used with above modification if needed. But I don't think we need to write a multi stacked navigation implementation from scratch. There is surely a much better readymade solution for this. I am not much into Android nowadays, so can't point to any.
I am keeping the original answer for the sake of completeness.
Original answer
I am terribly late to this question . But since this thread has been very informative and helpful to me I thought I better post my two pence here.
I needed a screen flow like this (A minimalistic design with 2 tabs and 2 views in each tab),
I had the same requirements in the past, and I did it using
TabActivityGroup
(which was deprecated at that time too) and Activities. This time I wanted to use Fragments.So this is how I done it.
1. Create a base Fragment Class
All fragments in your app can extend this Base class. If you want to use special fragments like
ListFragment
you should create a base class for that too. You will be clear about the usage ofonBackPressed()
andonActivityResult()
if you read the post in full..2. Create some Tab identifiers, accessible everywhere in project
nothing to explain here..
3. Ok, Main Tab Activity- Please go through comments in code..
4. app_main_tab_fragment_layout.xml (In case anyone interested.)
5. AppTabAFirstFragment.java (First fragment in Tab A, simliar for all Tabs)
This might not be the most polished and correct way. But it worked beautifully in my case. Also I only had this requirement in portrait mode. I never had to use this code in a project supporting both orientation. So can't say what kind of challenges I face there..
If anyone want a full project, I have pushed a sample project to github.
我们必须实现您最近为应用程序描述的完全相同的行为。应用程序的屏幕和整体流程已经定义,因此我们必须坚持使用它(它是 iOS 应用程序克隆......)。幸运的是,我们设法摆脱了屏幕上的后退按钮:)
我们使用 TabActivity、FragmentActivities(我们使用片段的支持库)和 Fragment 的混合来破解该解决方案。回想起来,我很确定这不是最好的架构决策,但我们设法让它发挥作用。如果我必须再做一次,我可能会尝试做一个更加基于活动的解决方案(没有片段),或者尝试为选项卡只有一个活动,而让所有其余的都是视图(我发现这更多)比整个活动可重用)。
因此,要求是每个选项卡中都有一些选项卡和可嵌套屏幕:
等等...
也就是说:用户从选项卡 1 开始,从屏幕 1 导航到屏幕 2,然后导航到屏幕 3,然后切换到选项卡 3 并从屏幕导航4至6;如果切换回选项卡 1,他应该再次看到屏幕 3,如果按下“后退”,他应该返回屏幕 2;再回来,他就在屏幕 1 中;切换到选项卡 3,他又回到屏幕 6。
应用程序中的主要 Activity 是 MainTabActivity,它扩展了 TabActivity。每个选项卡都与一个活动相关联,例如 ActivityInTab1、2 和 3。然后每个屏幕将是一个片段:
每个 ActivityInTab 一次仅保存一个片段,并且知道如何将一个片段替换为另一个片段(几乎相同)作为一个活动组)。最酷的是,通过这种方式可以很容易地为每个选项卡维护单独的后堆栈。
每个 ActivityInTab 的功能完全相同:知道如何从一个片段导航到另一个片段并维护返回堆栈,因此我们将其放入基类中。我们简单地称其为 ActivityInTab:
activity_in_tab.xml 就是这样:
如您所见,每个选项卡的视图布局都是相同的。这是因为它只是一个名为 content 的 FrameLayout,它将保存每个片段。片段是具有每个屏幕视图的片段。
只是为了奖励点,我们还添加了一些小代码,以便在用户按“后退”并且没有更多片段可返回时显示确认对话框:
这就是设置。正如您所看到的,每个 FragmentActivity(或者只是 Android >3 中的 Activity)都使用它自己的 FragmentManager 来处理所有的后退堆栈。
像 ActivityInTab1 这样的活动非常简单,它只会显示它的第一个片段(即屏幕):
然后,如果一个片段需要导航到另一个片段,它必须进行一些令人讨厌的转换...但它并没有那么糟糕:
那就差不多了。我很确定这不是一个非常规范的(而且大多数情况下肯定不是很好)解决方案,所以我想询问经验丰富的 Android 开发人员,实现此功能的更好方法是什么,如果这不是“它是如何”在 Android 中完成”,如果您能给我指出一些链接或材料来解释 Android 处理此问题的方法(选项卡、选项卡中的嵌套屏幕等),我将不胜感激。请随意在评论中撕开这个答案:)
最近我不得不向应用程序添加一些导航功能,这表明这个解决方案不是很好。一些奇怪的按钮应该将用户从一个选项卡带到另一个选项卡并进入嵌套屏幕。以编程方式执行此操作是一件很痛苦的事情,因为谁知道谁的问题以及处理片段和活动何时实际实例化和初始化的问题。我认为如果这些屏幕和选项卡都只是视图的话,事情会容易得多。
最后,如果您需要承受方向变化,那么使用 setArguments/getArguments 创建片段非常重要。如果你在片段的构造函数中设置实例变量,你就会被搞砸了。但幸运的是,这很容易解决:只需将所有内容保存在构造函数中的 setArguments 中,然后使用 onCreate 中的 getArguments 检索这些内容即可使用它们。
We had to implement exactly that same behaviour that you describe for an app recently. The screens and overall flow of the application were already defined so we had to stick with it (it's an iOS app clone...). Luckily, we managed to get rid of the on-screen back buttons :)
We hacked the solution using a mixture of TabActivity, FragmentActivities (we were using the support library for fragments) and Fragments. In retrospective, I'm pretty sure it wasn't the best architecture decision, but we managed to get the thing working. If I had to do it again, I'd probably try to do a more activity-based solution (no fragments), or try and have only one Activity for the tabs and let all the rest be views (which I find are much more reusable than activities overall).
So the requirements were to have some tabs and nestable screens in each tab:
etc...
So say: user starts in tab 1, navigates from screen 1 to screen 2 then to screen 3, he then switches to tab 3 and navigates from screen 4 to 6; if the switched back to tab 1, he should see screen 3 again and if he pressed Back he should return to screen 2; Back again and he is in screen 1; switch to tab 3 and he's in screen 6 again.
The main Activity in the application is MainTabActivity, which extends TabActivity. Each tab is associated with an activity, lets say ActivityInTab1, 2 and 3. And then each screen will be a fragment:
Each ActivityInTab holds only one fragment at a time, and knows how to replace one fragment for another one (pretty much the same as an ActvityGroup). The cool thing is that it's quite easy to mantain separate back stacks for each tab this way.
The functionality for each ActivityInTab was quite the same: know how to navigate from one fragment to another and maintain a back stack, so we put that in a base class. Let's call it simply ActivityInTab:
The activity_in_tab.xml is just this:
As you can see, the view layout for each tab was the same. That's because it's just a FrameLayout called content that will hold each fragment. The fragments are the ones that have each screen's view.
Just for the bonus points, we also added some little code to show a confirm dialog when the user presses Back and there are no more fragments to go back to:
That's pretty much the setup. As you can see, each FragmentActivity (or just simply Activity in Android >3) is taking care of all the back-stacking with it's own FragmentManager.
An activity like ActivityInTab1 will be really simple, it'll just show it's first fragment (i.e. screen):
Then, if a fragment needs to navigate to another fragment, it has to do a little nasty casting... but it's not that bad:
So that's pretty much it. I'm pretty sure this is not a very canonical (and mostly sure not very good) solution, so I'd like to ask seasoned Android developers what would be a better approach to acheive this functionality, and if this is not "how it's done" in Android, I'd appreciate if you could point me to some link or material that explains which is the Android way to approach this (tabs, nested screens in tabs, etc). Feel free to tear apart this answer in the comments :)
As a sign that this solution is not very good is that recently I had to add some navigation functionality to the application. Some bizarre button that should take the user from one tab into another and into a nested screen. Doing that programmatically was a pain in the butt, because of who-knows-who problems and dealing with when are fragments and activities actually instantiated and initialized. I think it would have been much easier if those screens and tabs were all just Views really.
Finally, if you need to survive orientation changes, it's important that your fragments are created using setArguments/getArguments. If you set instance variables in your fragments' constructors you'll be screwed. But fortunately that's really easy to fix: just save everything in setArguments in the constructor and then retrieve those things with getArguments in onCreate to use them.
该框架目前不会自动为您执行此操作。您将需要为每个选项卡构建和管理自己的后堆栈。
老实说,这似乎是一件非常值得怀疑的事情。我无法想象它会产生一个像样的用户界面 - 如果后退键将根据我所在的选项卡执行不同的操作,特别是如果后退键也具有在顶部时关闭整个活动的正常行为堆栈...听起来很糟糕。
如果您正在尝试构建类似 Web 浏览器 UI 之类的东西,为了获得用户自然的 UX,将涉及根据上下文对行为进行大量微妙的调整,因此您肯定需要执行自己的后台堆栈管理而不是依赖于框架中的某些默认实现。例如,尝试注意后退键如何以各种方式与标准浏览器交互,您可以进入和退出它。 (浏览器中的每个“窗口”本质上都是一个选项卡。)
The framework won't currently do this for you automatically. You will need to build and manage your own back stacks for each tab.
To be honest, this seems like a really questionable thing to do. I can't imagine it resulting in a decent UI -- if the back key is going to do different things depending on the tab I am, especially if the back key also has its normal behavior of closing the entire activity when at the top of the stack... sounds nasty.
If you are trying to build something like a web browser UI, to get a UX that is natural to the user is going to involve a lot of subtle tweaks of behavior depending on context, so you'll definitely need to do your own back stack management rather than rely on some default implementation in the framework. For an example try paying attention to how the back key interacts with the standard browser in the various ways you can go in and out of it. (Each "window" in the browser is essentially a tab.)
这可以使用 ChildFragmentManager 轻松实现,
这里有一篇关于此问题的文章以及相关项目。看看,
http: //tausiq.wordpress.com/2014/06/06/android-multiple-fragments-stack-in-each-viewpager-tab/
This can be easily achieved with ChildFragmentManager
Here is post about this with associated project. take a look,
http://tausiq.wordpress.com/2014/06/06/android-multiple-fragments-stack-in-each-viewpager-tab/
存储对片段的强引用并不是正确的方法。
FragmentManager 提供
putFragment(Bundle, String, Fragment)
和saveFragmentInstanceState(Fragment)
。任一者都足以实现后退堆栈。
使用
putFragment
,您无需替换 Fragment,而是分离旧的 Fragment 并添加新的 Fragment。这就是框架对添加到后台堆栈的替换事务所做的操作。 putFragment 存储当前活动片段列表的索引,并且框架在方向更改期间保存这些片段。第二种方法是使用 saveFragmentInstanceState 将整个片段状态保存到 Bundle 中,从而允许您真正删除它,而不是分离。使用这种方法使返回堆栈更易于操作,因为您可以随时弹出片段。
对于此用例,我使用了第二种方法:
我不希望用户通过按后退按钮从第三个方法返回到“注册”屏幕。我还会在它们之间翻转动画(使用
onCreateAnimation
),因此黑客解决方案将不起作用,至少在用户没有明确注意到某些事情不对劲的情况下。这是自定义 backstack 的有效用例,执行用户期望的操作......
Storing strong references to fragments is not the correct way.
FragmentManager provides
putFragment(Bundle, String, Fragment)
andsaveFragmentInstanceState(Fragment)
.Either one is enough to implement a backstack.
Using
putFragment
, instead of replacing a Fragment, you detach the old one and add the new one. This is what the framework does to a replace transaction that is added to the backstack.putFragment
stores an index to the current list of active Fragments and those Fragments are saved by the framework during orientation changes.The second way, using
saveFragmentInstanceState
, saves the whole fragment state to a Bundle allowing you to really remove it, rather than detaching. Using this approach makes the back stack easier to manipulate, as you can pop a Fragment whenever you want.I used the second method for this usecase:
I don't want the user to return to the Sign Up screen, from the third one, by pressing the back button. I also do flip animations between them (using
onCreateAnimation
), so hacky solutions won't work, atleast without the user clearly noticing something is not right.This is a valid use case for a custom backstack, doing what the user expects...
免责声明:
我觉得这是发布我曾针对类似类型问题(似乎是相当标准的 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
我希望这对某人有帮助!
Disclaimer:
I feel this is the best place to post a related solution I have worked on for a similar type of problem that seems to be pretty standard Android stuff. It's not going to solve the problem for everyone, but it may help some.
If the primary difference between your fragments is only the data backing them up (ie, not a lot of big layout differences), then you may not need to actually replace the fragment, but merely swap out the underlying data and refresh the view.
Here's a description of one possible example for this approach:
I have an app that uses ListViews. Each item in the list is a parent with some number of children. When you tap the item, a new list needs to open with those children, within the same ActionBar tab as the original list. These nested lists have a very similar layout (some conditional tweaks here and there perhaps), but the data is different.
This app has several layers of offspring beneath the initial parent list and we may or may not have data from the server by the time a user attempts to access any certain depth beyond the first. Because the list is constructed from a database cursor, and the fragments use a cursor loader and cursor adapter to populate the list view with list items, all that needs to happen when a click is registered is:
1) Create a new adapter with the appropriate 'to' and 'from' fields that will match new item views being added to the list and the columns returned by the new cursor.
2) Set this adapter as the new adapter for the ListView.
3) Build a new URI based on the item that was clicked and restart the cursor loader with the new URI (and projection). In this example, the URI is mapped to specific queries with the selection args passed down from the UI.
4) When the new data has been loaded from the URI, swap the cursor associated with the adapter to the new cursor, and the list will then refresh.
There is no backstack associated with this since we aren't using transactions, so you will have to either build your own, or play the queries in reverse when backing out of the hierarchy. When I tried this, the queries were fast enough that I just perform them again in oNBackPressed() up until I am at the top of hierarchy, at which point the framework takes over the back button again.
If you find yourself in a similar situation, make sure to read the docs:
http://developer.android.com/guide/topics/ui/layout/listview.html
http://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html
I hope this helps someone!
我遇到了完全相同的问题,并实现了一个开源 github 项目,该项目涵盖堆叠选项卡、后退和向上导航,并且经过充分测试和记录
: ">https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs
I had exactly the same problem and implemented an open source github project that covers stacked tab, back and up navigation and is well tested and documented:
https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs
这是一个复杂的问题,因为 Android 只处理 1 个返回堆栈,但这是可行的。我花了几天时间创建了一个名为 Tab Stacker 的库,它完全可以满足您的需求:每个选项卡的片段历史记录。它是开源的并且有完整的文档,并且可以轻松地包含在 gradle 中。您可以在 github 上找到该库: https://github.com/smart-fun/TabStacker
您还可以下载示例应用程序以查看其行为是否符合您的需求:
https://play.google.com/apps/testing/fr.arnaudguyon.tabstackerapp
如果您有任何疑问,请随时发送邮件。
This is a complex problem as Android only handles 1 back stack, but this is feasible. It took me days to create a library called Tab Stacker that does exactly what you are looking for: a fragment history for each tab. It is open source and fully documented, and can be included easily with gradle. You can find the library on github: https://github.com/smart-fun/TabStacker
You can also download the sample app to see that the behaviour corresponds to your needs:
https://play.google.com/apps/testing/fr.arnaudguyon.tabstackerapp
If you have any question don't hesitate to drop a mail.
我想提出我自己的解决方案,以防有人正在寻找并想要尝试选择最适合他/她需求的解决方案。
https://github.com/drusak/tabactivity
创建库的目的非常平庸 - 实现就像iPhone一样。
主要优点:
I'd like to suggest my own solution in case somebody is looking and want to try and choose the best one for his/her needs.
https://github.com/drusak/tabactivity
The purpose of creating the library is quite banal - implement it like iPhone.
The main advantages:
一个简单的解决方案:
每次更改选项卡/根视图调用时:
它将清除 BackStack。请记住在更改根片段之前调用此函数。
并使用以下内容添加片段:
请注意
.addToBackStack(null)
和transaction.add
可以使用transaction.replace
进行更改。A simple solution:
Every time you change tab/root view call:
It will clear the BackStack. Remember to call this before you change the root fragment.
And add fragments with this:
Note the
.addToBackStack(null)
and thetransaction.add
could e.g. be changed withtransaction.replace
.该线程非常非常有趣且有用。
感谢 Krishnabhadra 的解释和代码,我使用你的代码并进行了一些改进,允许从更改配置(主要是旋转)中保留堆栈、currentTab 等。
在真正的 4.0.4 和 2.3.6 设备上测试,未在模拟器上测试
我更改了“AppMainTabActivity.java”上的这部分代码,其余部分保持不变。
也许克里希那巴德拉会在他的代码中添加这个。
在创建时恢复数据:
保存变量并放入捆绑包:
如果存在以前的CurrentTab,则设置它,否则创建一个新的Tab_A:
我希望这对其他人有帮助。
This thread was very very interesting and useful.
Thanks Krishnabhadra for your explanation and code, I use your code and improved a bit, allowing to persist the stacks, currentTab, etc... from change configuration (rotating mainly).
Tested on a real 4.0.4 and 2.3.6 devices, not tested on emulator
I change this part of code on "AppMainTabActivity.java", the rest stay the same.
Maybe Krishnabhadra will add this on his code.
Recover data onCreate:
Save the variables and put to Bundle:
If exist a previous CurrentTab, set this, else create a new Tab_A:
I hope this helps other people.
我建议不要使用基于HashMap的backstack>
“不保留活动”模式存在很多错误。
如果您深入片段堆栈,它将无法正确恢复状态。
并且也会在嵌套的地图片段中被抓取(例外:片段没有找到 ID 的视图)。
Coz HashMap>在background\foreground应用程序将为空之后,
我优化了上面的代码以与片段的backstack一起使用
它是底部TabView
主要活动类
tags_activity.xml
tags_icon.xml
I would recommend do not use backstack based on HashMap>
there is lots of bugs in "do not keep activities" mode.
It will not correctly restore the state in case you deeply in fragment's stack.
And also will be crached in nested map fragment (with exeption: Fragment no view found for ID) .
Coz HashMap> after background\foreground app will be null
I optimize code above for work with fragment's backstack
It is bottom TabView
Main activity Class
tags_activity.xml
<
tags_icon.xml