Android Jetpack Navigation, BottomNavigationView mit automatischem Fragment-Back-Stack beim Klicken auf die Schaltfläche "Zurück"?
Was ich wollte, nachdem mehrere Benutzer nacheinander mehrere Registerkarten ausgewählt haben, klicken Sie auf die Schaltfläche "Zurück". Die App muss auf die zuletzt geöffnete Seite umleiten.
Dasselbe habe ich mit Android ViewPager erreicht, indem ich das aktuell ausgewählte Element in einer ArrayList speichere. Gibt es nach dem Android Jetpack Navigation Release einen automatischen Stapelwechsel? Ich möchte es mit einer Navigationsgrafik erreichen
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<Android.support.constraint.ConstraintLayout 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/container"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
tools:context=".main.MainActivity">
<fragment
Android:id="@+id/my_nav_Host_fragment"
Android:name="androidx.navigation.fragment.NavHostFragment"
Android:layout_width="match_parent"
Android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@+id/navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
<Android.support.design.widget.BottomNavigationView
Android:id="@+id/navigation"
Android:layout_width="0dp"
Android:layout_height="wrap_content"
Android:layout_marginStart="0dp"
Android:layout_marginEnd="0dp"
Android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/navigation" />
</Android.support.constraint.ConstraintLayout>
navigation.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:Android="http://schemas.Android.com/apk/res/Android">
<item
Android:id="@+id/navigation_home"
Android:icon="@drawable/ic_home"
Android:title="@string/title_home" />
<item
Android:id="@+id/navigation_people"
Android:icon="@drawable/ic_group"
Android:title="@string/title_people" />
<item
Android:id="@+id/navigation_organization"
Android:icon="@drawable/ic_organization"
Android:title="@string/title_organization" />
<item
Android:id="@+id/navigation_business"
Android:icon="@drawable/ic_business"
Android:title="@string/title_business" />
<item
Android:id="@+id/navigation_tasks"
Android:icon="@drawable/ic_dashboard"
Android:title="@string/title_tasks" />
</menu>
auch hinzugefügt
bottomNavigation.setupWithNavController(Navigation.findNavController(this, R.id.my_nav_Host_fragment))
Ich habe eine Antwort von Levi Moreira
erhalten, wie folgt
navigation.setOnNavigationItemSelectedListener {item ->
onNavDestinationSelected(item, Navigation.findNavController(this, R.id.my_nav_Host_fragment))
}
Auf diese Weise geschieht jedoch nur, dass die zuletzt geöffnete Instanz des Fragments erneut erstellt wird.
Bereitstellung der richtigen Rückwärtsnavigation für BottomNavigationView
Sie benötigen nicht wirklich eine ViewPager
, um mit BottomNavigation
und der neuen Navigationsarchitekturkomponente zu arbeiten. Ich habe in einer Beispiel-App gearbeitet, die genau beide verwendet, siehe hier .
Das Grundkonzept lautet: Sie haben die Hauptaktivität, die die Variable BottomNavigationView
hostet, und das ist der Navigationshost für Ihr Navigationsdiagramm. So sieht die XML-Datei aus:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<Android.support.constraint.ConstraintLayout 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/container"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
tools:context=".main.MainActivity">
<fragment
Android:id="@+id/my_nav_Host_fragment"
Android:name="androidx.navigation.fragment.NavHostFragment"
Android:layout_width="match_parent"
Android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@+id/navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
<Android.support.design.widget.BottomNavigationView
Android:id="@+id/navigation"
Android:layout_width="0dp"
Android:layout_height="wrap_content"
Android:layout_marginStart="0dp"
Android:layout_marginEnd="0dp"
Android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/navigation" />
</Android.support.constraint.ConstraintLayout>
Das Navigationsmenü (Registerkartenmenü) für die BottomNavigationView
sieht folgendermaßen aus:
navigation.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:Android="http://schemas.Android.com/apk/res/Android">
<item
Android:id="@+id/navigation_home"
Android:icon="@drawable/ic_home"
Android:title="@string/title_home" />
<item
Android:id="@+id/navigation_people"
Android:icon="@drawable/ic_group"
Android:title="@string/title_people" />
<item
Android:id="@+id/navigation_organization"
Android:icon="@drawable/ic_organization"
Android:title="@string/title_organization" />
<item
Android:id="@+id/navigation_business"
Android:icon="@drawable/ic_business"
Android:title="@string/title_business" />
<item
Android:id="@+id/navigation_tasks"
Android:icon="@drawable/ic_dashboard"
Android:title="@string/title_tasks" />
</menu>
All dies ist nur die BottomNavigationView
-Einstellung. Damit es mit der Navigations-Arch-Komponente funktioniert, müssen Sie in den Navigationsdiagramm-Editor gehen, alle Ihre Fragment-Ziele hinzufügen (in meinem Fall habe ich 5 davon, eines für jede Registerkarte) und die ID des Ziels mit derselben festlegen Name wie der in der navigation.xml
-Datei:
Dadurch wird Android angewiesen, eine Verknüpfung zwischen der Registerkarte und dem Fragment herzustellen. Der Benutzer klickt jetzt auf die Registerkarte "Home". Android sorgt dafür, dass das richtige Fragment geladen wird. Es gibt auch ein Stück Kotlin-Code, der benötigt wird zu Ihrem NavHost (der Hauptaktivität) hinzugefügt werden, um die Dinge mit der BottomNavigationView
zu verbinden:
Sie müssen in Ihrem onCreate Folgendes hinzufügen:
bottomNavigation.setupWithNavController(Navigation.findNavController(this, R.id.my_nav_Host_fragment))
Dadurch wird Android angewiesen, die Verbindung zwischen der Navigationsarchitekturkomponente und der BottomNavigationView herzustellen. Sehen Sie mehr in der docs .
Um den gleichen Beahvior zu erhalten, den Sie auch bei YouTube haben, fügen Sie einfach folgendes hinzu:
navigation.setOnNavigationItemSelectedListener {item ->
onNavDestinationSelected(item, Navigation.findNavController(this, R.id.my_nav_Host_fragment))
}
Dadurch werden Ziele in den Backstack verschoben. Wenn Sie auf die Zurück-Schaltfläche klicken, wird das zuletzt besuchte Ziel angezeigt.
Sie müssen die Hostnavigation wie folgt einstellen:
<LinearLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
xmlns:app="http://schemas.Android.com/apk/res-auto"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:orientation="vertical">
<Android.support.v7.widget.Toolbar
Android:id="@+id/toolbar"
Android:layout_width="match_parent"
Android:layout_height="wrap_content"
Android:background="@color/colorPrimary" />
<fragment
Android:id="@+id/navigation_Host_fragment"
Android:name="androidx.navigation.fragment.NavHostFragment"
Android:layout_width="match_parent"
Android:layout_height="0dp"
Android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
<Android.support.design.widget.BottomNavigationView
Android:id="@+id/bottom_navigation_view"
Android:layout_width="match_parent"
Android:layout_height="wrap_content"
app:itemIconTint="@drawable/color_state_list"
app:itemTextColor="@drawable/color_state_list"
app:menu="@menu/menu_bottom_navigation" />
</LinearLayout>
Setup mit dem Navigationscontroller:
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.navigation_Host_fragment);
NavigationUI.setupWithNavController(bottomNavigationView, navHostFragment.getNavController());
menu_bottom_navigation.xml:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:Android="http://schemas.Android.com/apk/res/Android">
<item
Android:id="@id/tab1" // Id of navigation graph
Android:icon="@mipmap/ic_launcher"
Android:title="@string/tab1" />
<item
Android:id="@id/tab2" // Id of navigation graph
Android:icon="@mipmap/ic_launcher"
Android:title="@string/tab2" />
<item
Android:id="@id/tab3" // Id of navigation graph
Android:icon="@mipmap/ic_launcher"
Android:title="@string/tab3" />
</menu>
nav_graph.xml:
<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/tab1">
<fragment
Android:id="@+id/tab1"
Android:name="com.navigationsample.Tab1Fragment"
Android:label="@string/tab1"
tools:layout="@layout/fragment_tab_1" />
<fragment
Android:id="@+id/tab2"
Android:name="com.navigationsample.Tab2Fragment"
Android:label="@string/tab2"
tools:layout="@layout/fragment_tab_2"/>
<fragment
Android:id="@+id/tab3"
Android:name="com.simform.navigationsample.Tab3Fragment"
Android:label="@string/tab3"
tools:layout="@layout/fragment_tab_3"/>
</navigation>
Durch Einrichten der gleichen ID von "nav_graph" auf "menu_bottom_navigation" wird der Klick der unteren Navigation ausgeführt.
Sie können die Rückaktion mit der popUpTo
-Eigenschaft im action
-Tag ausführen
Sie können ein Viewpager-Setup mit der unteren Navigationsansicht einrichten. Jedes Fragment im Viewpager ist ein Containerfragment. Es enthält untergeordnete Fragmente mit eigenem Backstack. Sie können den Backstack für jede Registerkarte im Viewpager auf diese Weise verwalten
Ich habe eine App wie diese gemacht (die noch nicht im PlayStore veröffentlicht wurde), die die gleiche Navigation hat. Möglicherweise unterscheidet sich die Implementierung von der von Google in ihren Apps verwendeten Funktionen.
die Struktur beinhaltet, dass ich Hauptaktivität habe, dass ich den Inhalt davon umschalte, indem Fragmente angezeigt/verborgen werden:
public void switchTo(final Fragment fragment, final String tag /*Each fragment should have a different Tag*/) {
// We compare if the current stack is the current fragment we try to show
if (fragment == getSupportFragmentManager().getPrimaryNavigationFragment()) {
return;
}
// We need to hide the current showing fragment (primary fragment)
final Fragment currentShowingFragment = getSupportFragmentManager().getPrimaryNavigationFragment();
final FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
if (currentShowingFragment != null) {
fragmentTransaction.hide(currentShowingFragment);
}
// We try to find that fragment if it was already added before
final Fragment alreadyAddedFragment = getSupportFragmentManager().findFragmentByTag(tag);
if (alreadyAddedFragment != null) {
// Since its already added before we just set it as primary navigation and show it again
fragmentTransaction.setPrimaryNavigationFragment(alreadyAddedFragment);
fragmentTransaction.show(alreadyAddedFragment);
} else {
// We add the new fragment and then show it
fragmentTransaction.add(containerId, fragment, tag);
fragmentTransaction.show(fragment);
// We set it as the primary navigation to support back stack and back navigation
fragmentTransaction.setPrimaryNavigationFragment(fragment);
}
fragmentTransaction.commit();
}
Wenn Sie eine bottomNavigationView
mit 3 Elementen haben, die 3 Fragment
s entspricht: FragmentA
, FragmentB
und FragmentC
, wobei FragmentA
die startDestination
in Ihrer Navigationsgrafik ist, werden Sie zu FragmentB
oder FragmentC
umgeleitet, wenn Sie sich im Hintergrund befinden FragmentA
, dieses Verhalten wird von Google empfohlen und standardmäßig implementiert.
Wenn Sie jedoch dieses Verhalten ändern möchten, müssen Sie entweder eine ViewPager
verwenden, wie dies in einigen anderen Antworten vorgeschlagen wurde, oder die Fragmente backStack- und back-Transaktionen selbst handhaben, was die Verwendung der Navigation in gewisser Weise beeinträchtigen würde Komponente insgesamt-.
Kurzer und guter Code in Kotlin zum Verbinden von unteren Navigationselementen mit Fragmenten innerhalb des Navigationsdiagramms:
val navControl = findNavController( R.id.nav_Host_frag_main)
bottomNavigationView?.setupWithNavController(navControl)
* Denken Sie nur daran: Die unteren Navigations-IDs und Fragmente im Navigationsdiagramm müssen dieselbe ID haben. Auch dank guter Erklärung von @sanat Answer
Lassen Sie mich zunächst klären, wie Youtube und Instagram die Fragmentnavigation handhaben.
Keine der oben genannten Antworten löst alle diese Probleme mit der Jetpack-Navigation.
Für die JetPack-Navigation gibt es hierfür keine Standardmethode. Ich fand es einfacher, den XML-Navigationsdiagramm für jedes untere Navigationselement in einen zu unterteilen und den hinteren Stapel zwischen den Navigationselementen selbst zu verwalten, indem ich die Aktivität FragmentManager verwende und den JetPack NavController verwende um die interne Navigation zwischen Stamm- und Detailfragmenten zu handhaben (ihre Implementierung verwendet den childFragmentManager-Stack).
Angenommen, Sie haben in Ihrem navigation
-Ordner diese 3 xmls:
res/navigation/
navigation_feed.xml
navigation_explore.xml
navigation_profile.xml
Halten Sie Ihre Ziel-IDs in den Navigations-XMLs wie in den Menü-IDs bottomNavigationBar. Legen Sie für jede XML-Datei den app:startDestination
auf das Fragment fest, das Sie als Stamm des Navigationselements verwenden möchten.
Erstellen Sie eine Klasse BottomNavController.kt
:
class BottomNavController(
val context: Context,
@IdRes val containerId: Int,
@IdRes val appStartDestinationId: Int
) {
private val navigationBackStack = BackStack.of(appStartDestinationId)
lateinit var activity: Activity
lateinit var fragmentManager: FragmentManager
private var listener: OnNavigationItemChanged? = null
private var navGraphProvider: NavGraphProvider? = null
interface OnNavigationItemChanged {
fun onItemChanged(itemId: Int)
}
interface NavGraphProvider {
@NavigationRes
fun getNavGraphId(itemId: Int): Int
}
init {
var ctx = context
while (ctx is ContextWrapper) {
if (ctx is Activity) {
activity = ctx
fragmentManager = (activity as FragmentActivity).supportFragmentManager
break
}
ctx = ctx.baseContext
}
}
fun setOnItemNavigationChanged(listener: (itemId: Int) -> Unit) {
this.listener = object : OnNavigationItemChanged {
override fun onItemChanged(itemId: Int) {
listener.invoke(itemId)
}
}
}
fun setNavGraphProvider(provider: NavGraphProvider) {
navGraphProvider = provider
}
fun onNavigationItemReselected(item: MenuItem) {
// If the user press a second time the navigation button, we pop the back stack to the root
activity.findNavController(containerId).popBackStack(item.itemId, false)
}
fun onNavigationItemSelected(itemId: Int = navigationBackStack.last()): Boolean {
// Replace fragment representing a navigation item
val fragment = fragmentManager.findFragmentByTag(itemId.toString())
?: NavHostFragment.create(navGraphProvider?.getNavGraphId(itemId)
?: throw RuntimeException("You need to set up a NavGraphProvider with " +
"BottomNavController#setNavGraphProvider")
)
fragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.nav_default_enter_anim,
R.anim.nav_default_exit_anim,
R.anim.nav_default_pop_enter_anim,
R.anim.nav_default_pop_exit_anim
)
.replace(containerId, fragment, itemId.toString())
.addToBackStack(null)
.commit()
// Add to back stack
navigationBackStack.moveLast(itemId)
listener?.onItemChanged(itemId)
return true
}
fun onBackPressed() {
val childFragmentManager = fragmentManager.findFragmentById(containerId)!!
.childFragmentManager
when {
// We should always try to go back on the child fragment manager stack before going to
// the navigation stack. It's important to use the child fragment manager instead of the
// NavController because if the user change tabs super fast commit of the
// supportFragmentManager may mess up with the NavController child fragment manager back
// stack
childFragmentManager.popBackStackImmediate() -> {
}
// Fragment back stack is empty so try to go back on the navigation stack
navigationBackStack.size > 1 -> {
// Remove last item from back stack
navigationBackStack.removeLast()
// Update the container with new fragment
onNavigationItemSelected()
}
// If the stack has only one and it's not the navigation home we should
// ensure that the application always leave from startDestination
navigationBackStack.last() != appStartDestinationId -> {
navigationBackStack.removeLast()
navigationBackStack.add(0, appStartDestinationId)
onNavigationItemSelected()
}
// Navigation stack is empty, so finish the activity
else -> activity.finish()
}
}
private class BackStack : ArrayList<Int>() {
companion object {
fun of(vararg elements: Int): BackStack {
val b = BackStack()
b.addAll(elements.toTypedArray())
return b
}
}
fun removeLast() = removeAt(size - 1)
fun moveLast(item: Int) {
remove(item)
add(item)
}
}
}
// Convenience extension to set up the navigation
fun BottomNavigationView.setUpNavigation(bottomNavController: BottomNavController, onReselect: ((menuItem: MenuItem) -> Unit)? = null) {
setOnNavigationItemSelectedListener {
bottomNavController.onNavigationItemSelected(it.itemId)
}
setOnNavigationItemReselectedListener {
bottomNavController.onNavigationItemReselected(it)
onReselect?.invoke(it)
}
bottomNavController.setOnItemNavigationChanged { itemId ->
menu.findItem(itemId).isChecked = true
}
}
Machen Sie Ihr Layout main.xml
so:
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
Android:layout_height="match_parent">
<FrameLayout
Android:id="@+id/container"
Android:layout_width="match_parent"
Android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/bottomNavigationView"
app:layout_constraintTop_toTopOf="parent" />
<com.google.Android.material.bottomnavigation.BottomNavigationView
Android:id="@+id/bottomNavigationView"
Android:layout_width="match_parent"
Android:layout_height="wrap_content"
Android:layout_marginStart="0dp"
Android:layout_marginEnd="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
Verwenden Sie für Ihre Aktivität wie folgt:
class MainActivity : AppCompatActivity(),
BottomNavController.NavGraphProvider {
private val navController by lazy(LazyThreadSafetyMode.NONE) {
Navigation.findNavController(this, R.id.container)
}
private val bottomNavController by lazy(LazyThreadSafetyMode.NONE) {
BottomNavController(this, R.id.container, R.id.navigation_feed)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
bottomNavController.setNavGraphProvider(this)
bottomNavigationView.setUpNavigation(bottomNavController)
if (savedInstanceState == null) bottomNavController
.onNavigationItemSelected()
// do your things...
}
override fun getNavGraphId(itemId: Int) = when (itemId) {
R.id.navigation_feed -> R.navigation.navigation_feed
R.id.navigation_explore -> R.navigation.navigation_explore
R.id.navigation_profile -> R.navigation.navigation_profile
else -> R.navigation.navigation_feed
}
override fun onSupportNavigateUp(): Boolean = navController
.navigateUp()
override fun onBackPressed() = bottomNavController.onBackPressed()
}