如何在后台堆栈中仅保留第一个添加的片段(片段重叠)?

9

我想实现的场景:

  1. 加载带有两个框架容器(用于列表项和详细信息)的活动。
  2. 在应用启动时,在listFrame中添加listFragment和一些初始的infoFragment在detailsFrame容器中。
  3. 浏览列表项时,不需要将每个详细事务添加到返回堆栈中(只想保留infoFragment在堆栈中)。
  4. 当用户按下返回按钮(后退导航),他会回到在启动时添加的初始infoFragment。
  5. 如果顺序的后退导航,则退出应用程序。

我的代码:

        protected override void OnCreate(Bundle savedInstanceState)
        {
...
            var listFrag = new ListFragment();
            var infoFrag = new InfoFragment();
            var trans = FragmentManager.BeginTransaction();
            trans.Add(Resource.Id.listFrame, listFrag);
            trans.Add(Resource.Id.detailsFrame, infoFrag);
            trans.Commit();
...
        }

        public void OnItemSelected(int id)
        {
            var detailsFrag = DetailFragment.NewInstance(id);
            var trans = FragmentManager.BeginTransaction();
            trans.Replace(Resource.Id.detailsFrame, detailsFrag);
            if (FragmentManager.BackStackEntryCount == 0)
                {
                    trans.AddToBackStack(null);
                }
            trans.Commit();
        }

我的问题:

点击返回按钮后,infoFrag 与之前的 detailFrag 重叠了!为什么会这样?


这个答案:https://dev59.com/rW3Xa4cB1Zd3GeqPahFj#28115271 是最简单和最好的解决方法!在我的情况下,它可以处理多个片段(可以按任何顺序启动第一个片段之后),而按返回键将始终返回到第一个创建的片段。 非常重要的一点是,在创建第一个片段时不要添加到backstack中,但在添加所有其他片段时添加到backstack中。由于声望点数较少,我无法直接评论或赞同该答案。 - Jemshid
这是一个简单的一行代码,请到以下链接查看:https://dev59.com/j2Up5IYBdhLWcg3w5Kk6。 - MohammedAli
5个回答

12
你可以这样做:
if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
   getSupportFragmentManager().popBackStack(getSupportFragmentManager().getBackStackEntryAt(0).getId(), getSupportFragmentManager().POP_BACK_STACK_INCLUSIVE);
} else {
   super.onBackPressed();}

在您的活动中,需要保留第一个片段。

您不应该在第一个片段中添加addToBackStack。 但是,在其他片段中可以添加。


“你不应该在第一个片段中使用addToBackStack”,箭头指向它并以大写字母闪烁!非常感谢!! - Schwesi
所以,我将这个添加到容器活动的 onBackPressed 中。然后,我使用了 addFragment 来添加 Frag A,然后在前往 Frag B 时使用了 replaceFragment + addToBackStack 来保留 Frag A,最后在前往 Frag C 时使用了 replaceFragment。当在 Frag C 上按返回键时,会回到 Frag A。(松了一口气,又有些疲惫地躺在床上) - Aba

7
非常好的Budius解释。我阅读了他的建议并实施了类似的导航,现在想与大家分享。
不要像这样替换片段:
Transaction.remove(detail1).add(detail2)
Transaction.remove(detail2).add(detail3)
Transaction.remove(detail3).add(detail4)

我在activity布局文件中添加了一个fragment容器布局。它可以是LinearLayoutRelativeLayoutFrameLayout等。因此,在activity创建时我有以下代码:

transaction.replace(R.id.HomeInputFragment, mainHomeFragment).commit();

mainHomeFragment是我想在按下返回按钮时返回到的片段,就像infoFrag一样。然后,在每次进行下一个事务之前,我都要放置以下代码:

fragmentManager.popBackStackImmediate();
transaction.replace(R.id.HomeInputFragment, frag2).addToBackStack(null).commit();

或者

fragmentManager.popBackStackImmediate();
transaction.replace(R.id.HomeInputFragment, frag3).addToBackStack(null).commit();

那样你就不需要跟踪当前显示的片段了。

这是解决问题的最简单答案。对我有效。 - Alezis

6
问题在于您从中返回的交易有两个步骤:
  • 删除infoFrag
  • 添加detailsFrag(即添加的第一个详细容器)
(我们知道这一点是因为文档上说:这与调用remove(Fragment)以删除使用相同containerViewId添加的所有当前添加的片段,然后使用此处给出的相同参数执行add(int,Fragment,String)基本相同。 ))
所以每当系统还原一个事务时,恰好还原这两个步骤,并且它不会对最后一个detailFrag做任何操作。
我可以想到两个可能的解决方法:
  1. 在您的活动中保留对上一个使用过的detailsFrag的引用,并使用BackStackChange侦听器,每当该值从1更改为0(您必须跟踪先前的值)时,您也要删除该剩余片段
  2. 在每个单击侦听器上,您都必须立即popBackStack(以删除上一个事务)并向所有事务添加addToBackStack()。在此解决方法中,您还可以使用一些setCustomAnimation魔法,以确保它在屏幕上看起来很好(例如使用从0到0持续时间为1的alpha动画,以避免上一个片段再次出现和消失。
ps。我同意片段管理器/事务应该对它处理.replace()操作的后堆栈更聪明一些,但这就是它的方式。
编辑:
正在发生的情况就像这样(我正在添加数字以使其更清晰)。请记住 .replace()= .remove().add()
Transaction.remove(info).add(detail1).addToBackStack(null) // 1st time
Transaction.remove(detail1).add(detail2) // 2nd time
Transaction.remove(detail2).add(detail3) // 3rd time
Transaction.remove(detail3).add(detail4) // 4th time

现在我们在布局中有了detail4:

< Press back button >
     System pops the back stack and find the following back entry to be reversed
         remove(info).add(detail1);
     so the system makes that transaction backward.
     tries to remove detail1 (is not there, so it ignores)
     re-add(info) // OVERLAP !!!

所以问题在于系统没有意识到存在detail4并且.replace()操作应该替换其中任何内容。

让我在我的回答中加入一个事务流程。给我几分钟。 - Budius
所以问题是,系统没有意识到有一个 detail4,这就解释了一切。现在情况是可以理解的,谢谢!只是好奇,你是怎么达到这种领悟的呢 ;)? - Arvis
我刚刚为我所在公司开发了一个小控制器,作为我们的内部库的一部分,用于一些片段事务的处理,就像这个问题和我的答案一样 https://dev59.com/Y2Yq5IYBdhLWcg3w-FTQ#14155204 ,所以直到我到达正确的位置,我进行了很多试错和阅读源代码,在此过程中,我基本上是按需以不同的方式重新执行后退堆栈。你理解我提出的可能解决方法吗?如果需要,我可以进一步解释。 - Budius
1
似乎文档中关于删除所有当前片段的部分是误导性的 - 只删除了一个片段!“这本质上与为具有相同containerViewId的所有当前添加的片段调用remove(Fragment)相同,然后使用此处给出的相同参数调用add(int,Fragment,String)。” - Arvis
它在执行事务时会删除所有内容,但是后退堆栈仍然停留在之前在事务期间使用的相同片段上,它不会检查该视图ID上的所有片段在弹出堆栈时。正如我之前所说,管理器在这里的操作有点愚蠢,但这就是我们必须处理的,因此我提出了这两种可能的解决方法。 - Budius
显示剩余3条评论

0
你可以覆盖 onBackPressed 并提交到初始片段的事务。

实际上,我不想与“Back Pressed”回调交互,因为我希望该按钮按照预期工作,除了从信息片段导航回来之外,应用程序必须退出(即第一个Beck堆栈事务)。 - Arvis

0

我猜测:

你已经添加了一个事务,将infoFrag替换为1st detailsFrag并将其放入后退栈中。

但是,然后你又用2nd detailsFrag替换了1st detailsFrag。

此时,当你点击后退按钮时,片段管理器无法将1st detailsFrag干净地替换为infoFrag,因为1st detailsFrag已经被删除并替换了。

无论这种重叠行为是否符合预期,我都不知道。

我建议调试Android核心代码以查看它正在做什么。

我不确定是否可以在没有覆盖 Activity :: onBackPressed()并自己执行所有事务的情况下实现上述操作并将它们添加到后退栈中。


从我个人的角度来看,重叠行为是不被期望的 :) 但是关于BackStack管理,它的整个目的就是手动(显式地)添加或弹出事务到/从后退堆栈中,难道不是吗?!如果我将第一个事务放入后退堆栈中,我期望在后退导航后它仍然存在,但实际上却不止一个。 - Arvis
也许这里的内容有用:(https://dev59.com/WWcs5IYBdhLWcg3w1nhq) - PJL

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接