I'm using jetpack navigation to transition from a fragment to a detail fragment.
I need help presenting the detail fragment over the fragment that displays it.
See photo below to look what I am trying to achieve:
I'm also using a basic fade_in / fade_out for Enter Exit on the Animations within the nav graph.
Here's the result I am getting:
How can I set up the transition so that the detail fragment displays over the presenting view?
Here's all my progress:
bottom_nav_menu
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/fragment_tab1"
android:title="Tab 1"/>
<item
android:id="@+id/fragment_tab2"
android:title="Tab 2"/>
<item
android:id="@+id/fragment_tab3"
android:title="Tab 3"/>
</menu>
nav_graph
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_graph"
app:startDestination="@id/fragment_tab1">
<fragment android:id="@+id/fragment_tab1" android:name="com.example.navgraphfragmentmodal.Tab1" android:label="fragment_tab1"
tools:layout="@layout/fragment_tab1">
<action android:id="@+id/toModal" app:destination="@id/modalFragment"/>
</fragment>
<fragment android:id="@+id/fragment_tab2" android:name="com.example.navgraphfragmentmodal.Tab2" android:label="fragment_tab2"
tools:layout="@layout/fragment_tab2"/>
<fragment android:id="@+id/fragment_tab3" android:name="com.example.navgraphfragmentmodal.Tab3"
android:label="fragment_tab3" tools:layout="@layout/fragment_tab3"/>
<fragment android:id="@+id/modalFragment" android:name="com.example.navgraphfragmentmodal.ModalFragment"
android:label="fragment_modal" tools:layout="@layout/fragment_modal"/>
</navigation>
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.navigation_host_fragment) as NavHostFragment?
NavigationUI.setupWithNavController(bottom_navigation_view, navHostFragment!!.navController)
supportActionBar?.setDisplayShowHomeEnabled(true)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
supportActionBar?.setDisplayShowHomeEnabled(false)
if (item.itemId === android.R.id.home) {
//Title bar back press triggers onBackPressed()
onBackPressed()
return true
}
return super.onOptionsItemSelected(item)
}
//Both navigation bar back press and title bar back press will trigger this method
override fun onBackPressed() {
supportActionBar?.setDisplayShowHomeEnabled(false)
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack()
} else {
super.onBackPressed()
}
}
}
Tab1 Fragment (This is the fragment with the Eiffel Tower image & button)
class Tab1 : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_tab1, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
learnMoreButton.setOnClickListener {
NavHostFragment.findNavController(this).navigate(R.id.modalFragment)
}
}
}
Bounty edit:
I am looking to display a fragment over a fragment in a navigation graph, as seen in the Eiffel Tower image. In iOS this would be similar to presentation: Over Full Screen. I do not wish to use a DialogFragment, as I have far more control over animating the views in a normal Fragment.
You are trying to add a modal dialog to a navigation graph as a normal destination. NavController will always only show one destination at a time and they are not stacked. A fragment is removed from the view when an other one is shown by NavController. It's not intended to function in the way you want it to do.
You need to create a AlertDialog
or an DialogFragment
with your UI. In the case shown above a AlertDialog
will already be sufficient. You can add a custom view to an AlertDialog
like this:
val dialog = AlertDialog.Builder(context)
.setView(R.layout.your_layout)
.show()
dialog.findViewById<View>(R.id.button).setOnClickListener {
// Do somehting
dialog.dismiss()
}
This allows you to create a dialog with your custom view. It will work like one would want it to work: Clicking the button closes the dialog as well as pressing the back button. The background will automatically be dimmed.
The dialog will have the default shape but you can follow this answer to create the rounded corners of the dialog shown in your question.
You can add animations to the AlertDialog
with window animations and a custom style, refer to this answer.
For more complex views requiring a lifecycle DialogFragment
should be used (e.g. when showing a map in the dialog). Starting with navigation 2.1.0-alpha03 <dialog>
destinations are supported. This is still in alpha/beta though.
Edit:
If you want extremely fine control over animations you can add a view to your layout like this:
<FrameLayout>
<!-- Remove the defaultNavHost from this! -->
<fragment android:id="@+id/navHost" android:name="...NavHost" />
<FrameLayout android:id="@+id/dialog" android:background="#44000000" android:layout_width="match_parent" android:layout_height="match_parent">
<FrameLayout android:id="@+id/dialogContent" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center">
<!-- Dialog contents -->
</FramyLayout>
</FrameLayout>
</FrameLayout>
Because the item is after the nav host in the layout, it will be rendered on top of it. Be aware that buttons or other elements having the elevation
property set may appear on top of it.
Finally, overwrite your onBackPressed()
in your activity to restore the navigation behaviour:
override fun onBackPressed() {
val dialog = findViewById<View>(R.id.dialog)
val navController = (supportFragmentManager.findFragmentById(R.id.navHost) as NavHostFragment).navController
if (dialog.visibility == View.VISIBLE) {
hideDialog()
} else if(navController.popBackStack()) {
Log.i("MainActivity", "NavController handled back press")
} else {
super.onBackPressed()
}
}
private fun showDialog() {
findViewById<View>(R.id.dialog).visibility = View.VISIBLE
// Intro animation
}
private fun hideDialog() {
// Outro animation. Call the next line after the animation is done.
findViewById<View>(R.id.dialog).visibility = View.GONE
}
This might be what you are looking for but I'd not recommend it and work with AlertDialog
or DialogFragment
instead.
You could also use a new Activity with a transparent background for the dialog and disable the animations on this activity so you can manually animate the views in the activity. Activities can be 100% transparent.