如何防止BottomSheetDialogFragment在导航到另一个片段后关闭?

16

我在我的应用程序上使用NavigationComponent

我有一个特定的流程,在BottomSheetDialogFragment的按钮单击后,应用程序应该导航到另一个片段。 但是当该片段被弹出时,我需要返回到先前的BottomSheetDialogFragment

由于某种原因,BottomSheetDialogFragment会自动关闭。

Frag A : click on a button  
Frag A -> Dialog B : click on a button  
Frag A -> Dialog B -> Frag C : pop Frag C from the stack  
Frag A : Dialog B was automatically dismissed =;/  

如何防止 BottomSheetDialogFragment 被关闭?


问:为什么我需要 BottomSheetDialogFragment 不被关闭?
答:我通过 LiveData 监听已打开的 Fragment 的结果。由于 BottomSheetDialogFragment 被关闭,它永远不会接收到结果。

4个回答

12
这是不可能的。对话框目标实现了FloatingWindow interface,其中规定:
目标实现此接口时,当您导航到新目标时,它们将自动从后退堆栈中弹出。
因此,预期在导航到<fragment>目标时,对话框目标会自动从后退堆栈中弹出。但是,当在多个对话框目标之间导航时(这些目标可以叠加在彼此上面),情况并非如此。 This issue更详细地解释了这里的限制,即:
1. 对话框是始终位于活动窗口上方的单独窗口。这意味着无论底层FragmentManager处于什么状态或者您进行了什么FragmentTransactions,对话框都将继续拦截系统返回按钮。 2. 对片段容器(即您的正常目标)的操作不会影响对话框片段。如果在嵌套的FragmentManager上执行FragmentTransactions也是一样。
因此,一旦导航到<fragment>目标,系统返回按钮实际上起作用的唯一方法是弹出所有浮动窗口(否则它们会在其他任何事物之前拦截返回按钮),因为这些窗口始终浮动在内容上方。
这不是Navigation Component强加的限制——在任何使用BottomSheetDialogFragment有关Fragment后退堆栈和系统返回按钮的用法中,都存在同样的问题。

非常感谢您的解释 \o/。我会尝试解决那种情况 ^^。 - Augusto Carmo
那么,怎么解决这个问题呢?@ianhanniballake - Robin

2
这是不可能的,如@ianhanniballake所指出。
但是,这可以通过将片段C设置为DailogFragment,而不是普通的Fragment来实现,但需要付出一些努力,使其像普通片段一样运行。
在这种情况下,BC都是对话框,因此它们将共享相同的后退堆栈。因此,当从C返回到B时,您仍将看到BottomSheetDialgFragment B显示。
要修复C的受限窗口,请使用以下主题:
<style name="DialogTheme" parent="Theme.MyApp">
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowFullscreen">false</item>
    <item name="android:windowIsFloating">false</item>
</style>

其中Theme.MyApp是您的应用程序主题。

然后通过覆盖getTheme()将其应用于C

class FragmentC : DialogFragment() {

    //.....

    override fun getTheme(): Int = R.style.DialogTheme
    
}

您需要在导航图中将Cfragment更改为dialog:
<dialog
        android:id="@+id/fragmentC"
        android:name="....">
</dialog>

Preview:


1
你肯定不希望因为对话框一直停留在下一个目标的上方而忽略它。
如果你所指的“听取结果”是findNavController().currentBackStackEntry.savedStateHandle.getLiveData(MY_KEY)
那么你应该能够将你的结果设置为previousBackStackEntry,因为它会给你在对话框之前的目标。
Frag A : click on a button 
Frag A -> Dialog B : click on a button (automatically popped-off)
Frag A -> Dialog B -> Frag C : pop Frag C from the stack
  

那么

class FragA : Fragment() {
    
    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        
        ...

        findNavController().currentBackStackEntry.savedStateHandle?.getLiveData<MyResult>(MY_KEY).observe(viewLifecycleOwner) {
           // get your result here
           // show Dialog B again if you like ?
        }
    }
}

并且

class FragC : Fragment() {

    ...

    private fun setResultAndFinish(result: MyResult) {
        findNavController().apply { 
            previousBackStackEntry?.savedStateHandle?.set(MY_KEY, result)
            popBackStack()
        }
    }
    
}

0

@ianhanniballake提出了一个很好的观点,解释了为什么这不是完全可能的,以及它为什么能够按照它的方式工作,但实际上有一种解决方法。这种解决方法并不是最愉快的,所以请记住这一点。我在下面列出了它自己的缺点。

理论

这种解决方法涉及到一些“旧”的Android机制,这些机制早于导航控制器(我们并不总是有导航控制器)。这种解决方法围绕着几个事实:

  1. 所有片段都存在于某个FragmentManager中。导航控制器并不神奇,它仍然在幕后使用FragmentManager。实际上,您可以将导航控制器视为FragmentManager的包装器。
  2. 所有片段都带有自己的小FragmentManager。您可以通过片段内的childFragmentManager访问它。在childFragmentManager中启动的任何片段都被视为该片段的子级。
  3. 当片段移动到“后退栈”时,它的所有子级也会随之移动。
  4. 当片段被恢复时,它的子级也会被恢复。

有了这四个事实,我们就可以制定一个解决方法。

这个想法是,如果我们在一个片段的childFragmentManager上显示所有的DialogFragment,那么我们就可以在没有任何对话框相关问题的情况下导航到其他片段。这是因为当我们从FragA导航到FragC时,FragA的所有子项都被移动到后退堆栈中。由于我们使用childFragmentManager启动了DialogFragment,所以DialogFragment也会自动关闭。

现在,当用户返回到我们的片段(FragA)时,我们的DialogFragment会再次显示,因为FragA的childFragmentManager也被恢复了。而我们的DialogFragment则存在于该childFragmentManager中。

实现

现在我们知道如何解决这个问题,让我们开始实现它。

为了简单起见,让我们重用你提供的示例。也就是说,我们假设有片段FragAFragC以及对话框DialogB

首先,虽然导航组件很好用,但如果我们想要这样做,我们不能使用它来启动对话框。如果您使用安全参数,则仍然可以继续获得其好处,因为从技术上讲,安全参数并不与导航组件绑定。以下是启动对话框 B 的示例:

// inside FragA
fun onLaunchBClick() {
  val parentFragment = parentFragment ?: return
  
  DialogB()
    .apply {
        // we can still use safe args
        arguments = DialogBArgs(myArg1, myArg2).toBundle()
    }
    .show(parentFragment?.childFragmentManager, "DialogB")
}

现在我们可以让DialogB启动FragC,但有一个问题。因为我们使用了childFragmentManager,导航控制器实际上看不到DialogB。这意味着对于导航控制器来说,我们是从FragA启动FragC。如果导航图中有多个边缘指向DialogB,这可能会创建一个问题。解决此问题的方法是将DialogB的所有方向都设置为全局。这最终是此解决方法的缺点。在这种情况下,我们可以声明一个全局操作到FragC并通过它来启动它。
// inside DialogB
fun onLaunchCClick() {
  val direction = NavMainDirections.actionGlobalFragC()
  findNavController().navigate(direction)
}

缺点

这种方法显然存在一些明显的缺点。最大的一个是对话框可以导航到的所有片段都应声明为全局操作。唯一的例外是如果对话框只有一个边缘。如果对话框只有一个边缘,而且不太可能添加新的边缘,那么你可以在它唯一的父片段中添加操作。

例如,如果DialogC可以启动FragmentCFragmentD,并且DialogC可以从FragmentAFragmentZ(2个边缘)启动,则DialogC必须使用全局操作来启动FragmentCFragmentD

另一个缺点是我们不能再使用导航控制器来启动需要启动其他非对话框片段的对话框片段。这个缺点相对较轻,因为我们至少仍然可以使用安全参数。

最后一个缺点是性能可能会稍微差一些。考虑这样一个例子,我们有一个片段FragA启动DialogB启动FragC。现在如果用户点击返回,FragA将被恢复。但由于DialogBFragA的子级,DialogB也将被恢复。这意味着需要加载和恢复额外的片段,从而降低了返回操作的性能。实际上,只要您的片段没有保存大量状态,并且每个片段没有太多的子级,这种成本就应该很小。

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