1 /*
<lambda>null2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package androidx.navigation.ui
17 
18 import android.os.Bundle
19 import android.util.Log
20 import android.view.Menu
21 import android.view.MenuItem
22 import android.view.View
23 import androidx.annotation.IdRes
24 import androidx.annotation.RestrictTo
25 import androidx.appcompat.app.AppCompatActivity
26 import androidx.appcompat.widget.Toolbar
27 import androidx.coordinatorlayout.widget.CoordinatorLayout
28 import androidx.core.view.forEach
29 import androidx.customview.widget.Openable
30 import androidx.navigation.ActivityNavigator
31 import androidx.navigation.FloatingWindow
32 import androidx.navigation.NavController
33 import androidx.navigation.NavDestination
34 import androidx.navigation.NavDestination.Companion.hierarchy
35 import androidx.navigation.NavGraph.Companion.findStartDestination
36 import androidx.navigation.NavOptions
37 import androidx.navigation.internal.NavContext
38 import com.google.android.material.appbar.CollapsingToolbarLayout
39 import com.google.android.material.bottomnavigation.BottomNavigationView
40 import com.google.android.material.bottomsheet.BottomSheetBehavior
41 import com.google.android.material.navigation.NavigationBarView
42 import com.google.android.material.navigation.NavigationView
43 import com.google.android.material.navigationrail.NavigationRailView
44 import java.lang.ref.WeakReference
45 
46 /**
47  * Class which hooks up elements typically in the 'chrome' of your application such as global
48  * navigation patterns like a navigation drawer or bottom nav bar with your [NavController].
49  */
50 public object NavigationUI {
51     private const val TAG = "NavigationUI"
52 
53     /**
54      * Attempt to navigate to the [NavDestination] associated with the given MenuItem. This MenuItem
55      * should have been added via one of the helper methods in this class.
56      *
57      * Importantly, it assumes the [menu item id][MenuItem.getItemId] matches a valid
58      * [action id][NavDestination.getAction] or [destination id][NavDestination.id] to be navigated
59      * to.
60      *
61      * By default, the back stack will be popped back to the navigation graph's start destination.
62      * Menu items that have `android:menuCategory="secondary"` will not pop the back stack.
63      *
64      * @param item The selected MenuItem.
65      * @param navController The NavController that hosts the destination.
66      * @return True if the [NavController] was able to navigate to the destination associated with
67      *   the given MenuItem.
68      */
69     @JvmStatic
70     public fun onNavDestinationSelected(item: MenuItem, navController: NavController): Boolean {
71         val builder = NavOptions.Builder().setLaunchSingleTop(true).setRestoreState(true)
72         if (
73             navController.currentDestination!!.parent!!.findNode(item.itemId)
74                 is ActivityNavigator.Destination
75         ) {
76             builder
77                 .setEnterAnim(R.anim.nav_default_enter_anim)
78                 .setExitAnim(R.anim.nav_default_exit_anim)
79                 .setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
80                 .setPopExitAnim(R.anim.nav_default_pop_exit_anim)
81         } else {
82             builder
83                 .setEnterAnim(R.animator.nav_default_enter_anim)
84                 .setExitAnim(R.animator.nav_default_exit_anim)
85                 .setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
86                 .setPopExitAnim(R.animator.nav_default_pop_exit_anim)
87         }
88         if (item.order and Menu.CATEGORY_SECONDARY == 0) {
89             builder.setPopUpTo(
90                 navController.graph.findStartDestination().id,
91                 inclusive = false,
92                 saveState = true
93             )
94         }
95         val options = builder.build()
96         return try {
97             // TODO provide proper API instead of using Exceptions as Control-Flow.
98             navController.navigate(item.itemId, null, options)
99             // Return true only if the destination we've navigated to matches the MenuItem
100             navController.currentDestination?.matchDestination(item.itemId) == true
101         } catch (e: IllegalArgumentException) {
102             val name = NavDestination.getDisplayName(NavContext(navController.context), item.itemId)
103             Log.i(
104                 TAG,
105                 "Ignoring onNavDestinationSelected for MenuItem $name as it cannot be found " +
106                     "from the current destination ${navController.currentDestination}",
107                 e
108             )
109             false
110         }
111     }
112 
113     /**
114      * Attempt to navigate to the [NavDestination] associated with the given MenuItem. This MenuItem
115      * should have been added via one of the helper methods in this class.
116      *
117      * Importantly, it assumes the [menu item id][MenuItem.getItemId] matches a valid
118      * [action id][NavDestination.getAction] or [destination id][NavDestination.id] to be navigated
119      * to.
120      *
121      * By default, the back stack will be popped back to the navigation graph's start destination.
122      * Menu items that have `android:menuCategory="secondary"` will not pop the back stack.
123      *
124      * @param item The selected MenuItem.
125      * @param navController The NavController that hosts the destination.
126      * @param saveState Whether the NavController should save the back stack state. This must always
127      *   be `false`: leave this parameter off entirely to use the non-experimental version of this
128      *   API, which saves the state by default.
129      * @return True if the [NavController] was able to navigate to the destination associated with
130      *   the given MenuItem.
131      */
132     @NavigationUiSaveStateControl
133     @JvmStatic
134     public fun onNavDestinationSelected(
135         item: MenuItem,
136         navController: NavController,
137         saveState: Boolean
138     ): Boolean {
139         check(!saveState) {
140             "Leave the saveState parameter out entirely to use the non-experimental version of " +
141                 "this API, which saves the state by default"
142         }
143         val builder = NavOptions.Builder().setLaunchSingleTop(true)
144         if (
145             navController.currentDestination!!.parent!!.findNode(item.itemId)
146                 is ActivityNavigator.Destination
147         ) {
148             builder
149                 .setEnterAnim(R.anim.nav_default_enter_anim)
150                 .setExitAnim(R.anim.nav_default_exit_anim)
151                 .setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
152                 .setPopExitAnim(R.anim.nav_default_pop_exit_anim)
153         } else {
154             builder
155                 .setEnterAnim(R.animator.nav_default_enter_anim)
156                 .setExitAnim(R.animator.nav_default_exit_anim)
157                 .setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
158                 .setPopExitAnim(R.animator.nav_default_pop_exit_anim)
159         }
160         if (item.order and Menu.CATEGORY_SECONDARY == 0) {
161             builder.setPopUpTo(navController.graph.findStartDestination().id, inclusive = false)
162         }
163         val options = builder.build()
164         return try {
165             // TODO provide proper API instead of using Exceptions as Control-Flow.
166             navController.navigate(item.itemId, null, options)
167             // Return true only if the destination we've navigated to matches the MenuItem
168             navController.currentDestination?.matchDestination(item.itemId) == true
169         } catch (e: IllegalArgumentException) {
170             val name = NavDestination.getDisplayName(NavContext(navController.context), item.itemId)
171             Log.i(
172                 TAG,
173                 "Ignoring onNavDestinationSelected for MenuItem $name as it cannot be found " +
174                     "from the current destination ${navController.currentDestination}",
175                 e
176             )
177             false
178         }
179     }
180 
181     /**
182      * Handles the Up button by delegating its behavior to the given NavController. This should
183      * generally be called from [AppCompatActivity.onSupportNavigateUp].
184      *
185      * If you do not have a [Openable] layout, you should call [NavController.navigateUp] directly.
186      *
187      * @param navController The NavController that hosts your content.
188      * @param openableLayout The Openable layout that should be opened if you are on the topmost
189      *   level of the app.
190      * @return True if the [NavController] was able to navigate up.
191      */
192     @JvmStatic
193     public fun navigateUp(navController: NavController, openableLayout: Openable?): Boolean =
194         navigateUp(
195             navController,
196             AppBarConfiguration.Builder(navController.graph)
197                 .setOpenableLayout(openableLayout)
198                 .build()
199         )
200 
201     /**
202      * Handles the Up button by delegating its behavior to the given NavController. This is an
203      * alternative to using [NavController.navigateUp] directly when the given [AppBarConfiguration]
204      * needs to be considered when determining what should happen when the Up button is pressed.
205      *
206      * In cases where no Up action is available, the
207      * [AppBarConfiguration.fallbackOnNavigateUpListener] will be called to provide additional
208      * control.
209      *
210      * @param navController The NavController that hosts your content.
211      * @param configuration Additional configuration options for determining what should happen when
212      *   the Up button is pressed.
213      * @return True if the [NavController] was able to navigate up.
214      */
215     @JvmStatic
216     public fun navigateUp(
217         navController: NavController,
218         configuration: AppBarConfiguration
219     ): Boolean {
220         val openableLayout = configuration.openableLayout
221         val currentDestination = navController.currentDestination
222         return if (
223             openableLayout != null &&
224                 currentDestination != null &&
225                 configuration.isTopLevelDestination(currentDestination)
226         ) {
227             openableLayout.open()
228             true
229         } else {
230             return if (navController.navigateUp()) {
231                 true
232             } else configuration.fallbackOnNavigateUpListener?.onNavigateUp() ?: false
233         }
234     }
235 
236     /**
237      * Sets up the ActionBar returned by [AppCompatActivity.getSupportActionBar] for use with a
238      * [NavController].
239      *
240      * By calling this method, the title in the action bar will automatically be updated when the
241      * destination changes (assuming there is a valid [label][NavDestination.label]).
242      *
243      * The start destination of your navigation graph is considered the only top level destination.
244      * On the start destination of your navigation graph, the ActionBar will show the drawer icon if
245      * the given Openable layout is non null. On all other destinations, the ActionBar will show the
246      * Up button. Call [navigateUp] to handle the Up button.
247      *
248      * Destinations that implement [androidx.navigation.FloatingWindow] will be ignored.
249      *
250      * @param activity The activity hosting the action bar that should be kept in sync with changes
251      *   to the NavController.
252      * @param navController The NavController whose navigation actions will be reflected in the
253      *   title of the action bar.
254      * @param openableLayout The Openable layout that should be toggled from the home button
255      * @see NavigationUI.setupActionBarWithNavController
256      */
257     @JvmStatic
258     public fun setupActionBarWithNavController(
259         activity: AppCompatActivity,
260         navController: NavController,
261         openableLayout: Openable?
262     ): Unit =
263         setupActionBarWithNavController(
264             activity,
265             navController,
266             AppBarConfiguration.Builder(navController.graph)
267                 .setOpenableLayout(openableLayout)
268                 .build()
269         )
270 
271     /**
272      * Sets up the ActionBar returned by [AppCompatActivity.getSupportActionBar] for use with a
273      * [NavController].
274      *
275      * By calling this method, the title in the action bar will automatically be updated when the
276      * destination changes (assuming there is a valid [label][NavDestination.label]).
277      *
278      * The [AppBarConfiguration] you provide controls how the Navigation button is displayed. Call
279      * [navigateUp] to handle the Up button.
280      *
281      * Destinations that implement [androidx.navigation.FloatingWindow] will be ignored.
282      *
283      * @param activity The activity hosting the action bar that should be kept in sync with changes
284      *   to the NavController.
285      * @param navController The NavController whose navigation actions will be reflected in the
286      *   title of the action bar.
287      * @param configuration Additional configuration options for customizing the behavior of the
288      *   ActionBar
289      */
290     @JvmStatic
291     @JvmOverloads
292     public fun setupActionBarWithNavController(
293         activity: AppCompatActivity,
294         navController: NavController,
295         configuration: AppBarConfiguration =
296             AppBarConfiguration.Builder(navController.graph).build()
297     ): Unit =
298         navController.addOnDestinationChangedListener(
299             ActionBarOnDestinationChangedListener(activity, configuration)
300         )
301 
302     /**
303      * Sets up a [Toolbar] for use with a [NavController].
304      *
305      * By calling this method, the title in the Toolbar will automatically be updated when the
306      * destination changes (assuming there is a valid [label][NavDestination.label]).
307      *
308      * The start destination of your navigation graph is considered the only top level destination.
309      * On the start destination of your navigation graph, the Toolbar will show the drawer icon if
310      * the given Openable layout is non null. On all other destinations, the Toolbar will show the
311      * Up button. This method will call [navigateUp] when the Navigation button is clicked.
312      *
313      * Destinations that implement [androidx.navigation.FloatingWindow] will be ignored.
314      *
315      * @param toolbar The Toolbar that should be kept in sync with changes to the NavController.
316      * @param navController The NavController whose navigation actions will be reflected in the
317      *   title of the Toolbar.
318      * @param openableLayout The Openable layout that should be toggled from the Navigation button
319      * @see setupWithNavController
320      */
321     @JvmStatic
322     public fun setupWithNavController(
323         toolbar: Toolbar,
324         navController: NavController,
325         openableLayout: Openable?
326     ): Unit =
327         setupWithNavController(
328             toolbar,
329             navController,
330             AppBarConfiguration.Builder(navController.graph)
331                 .setOpenableLayout(openableLayout)
332                 .build()
333         )
334 
335     /**
336      * Sets up a [Toolbar] for use with a [NavController].
337      *
338      * By calling this method, the title in the Toolbar will automatically be updated when the
339      * destination changes (assuming there is a valid [label][NavDestination.label]).
340      *
341      * The [AppBarConfiguration] you provide controls how the Navigation button is displayed and
342      * what action is triggered when the Navigation button is tapped. This method will call
343      * [navigateUp] when the Navigation button is clicked.
344      *
345      * Destinations that implement [androidx.navigation.FloatingWindow] will be ignored.
346      *
347      * @param toolbar The Toolbar that should be kept in sync with changes to the NavController.
348      * @param navController The NavController whose navigation actions will be reflected in the
349      *   title of the Toolbar.
350      * @param configuration Additional configuration options for customizing the behavior of the
351      *   Toolbar
352      */
353     @JvmStatic
354     @JvmOverloads
355     public fun setupWithNavController(
356         toolbar: Toolbar,
357         navController: NavController,
358         configuration: AppBarConfiguration =
359             AppBarConfiguration.Builder(navController.graph).build()
360     ) {
361         navController.addOnDestinationChangedListener(
362             ToolbarOnDestinationChangedListener(toolbar, configuration)
363         )
364         toolbar.setNavigationOnClickListener { navigateUp(navController, configuration) }
365     }
366 
367     /**
368      * Sets up a [CollapsingToolbarLayout] and [Toolbar] for use with a [NavController].
369      *
370      * By calling this method, the title in the CollapsingToolbarLayout will automatically be
371      * updated when the destination changes (assuming there is a valid
372      * [label][NavDestination.label]).
373      *
374      * The start destination of your navigation graph is considered the only top level destination.
375      * On the start destination of your navigation graph, the Toolbar will show the drawer icon if
376      * the given Openable layout is non null. On all other destinations, the Toolbar will show the
377      * Up button. This method will call [navigateUp] when the Navigation button is clicked.
378      *
379      * Destinations that implement [androidx.navigation.FloatingWindow] will be ignored.
380      *
381      * @param collapsingToolbarLayout The CollapsingToolbarLayout that should be kept in sync with
382      *   changes to the NavController.
383      * @param toolbar The Toolbar that should be kept in sync with changes to the NavController.
384      * @param navController The NavController whose navigation actions will be reflected in the
385      *   title of the Toolbar.
386      * @param openableLayout The Openable layout that should be toggled from the Navigation button
387      */
388     @JvmStatic
389     public fun setupWithNavController(
390         collapsingToolbarLayout: CollapsingToolbarLayout,
391         toolbar: Toolbar,
392         navController: NavController,
393         openableLayout: Openable?
394     ): Unit =
395         setupWithNavController(
396             collapsingToolbarLayout,
397             toolbar,
398             navController,
399             AppBarConfiguration.Builder(navController.graph)
400                 .setOpenableLayout(openableLayout)
401                 .build()
402         )
403 
404     /**
405      * Sets up a [CollapsingToolbarLayout] and [Toolbar] for use with a [NavController].
406      *
407      * By calling this method, the title in the CollapsingToolbarLayout will automatically be
408      * updated when the destination changes (assuming there is a valid
409      * [label][NavDestination.label]).
410      *
411      * The [AppBarConfiguration] you provide controls how the Navigation button is displayed and
412      * what action is triggered when the Navigation button is tapped. This method will call
413      * [navigateUp] when the Navigation button is clicked.
414      *
415      * Destinations that implement [androidx.navigation.FloatingWindow] will be ignored.
416      *
417      * @param collapsingToolbarLayout The CollapsingToolbarLayout that should be kept in sync with
418      *   changes to the NavController.
419      * @param toolbar The Toolbar that should be kept in sync with changes to the NavController.
420      * @param navController The NavController whose navigation actions will be reflected in the
421      *   title of the Toolbar.
422      * @param configuration Additional configuration options for customizing the behavior of the
423      *   Toolbar
424      */
425     @JvmStatic
426     @JvmOverloads
427     public fun setupWithNavController(
428         collapsingToolbarLayout: CollapsingToolbarLayout,
429         toolbar: Toolbar,
430         navController: NavController,
431         configuration: AppBarConfiguration =
432             AppBarConfiguration.Builder(navController.graph).build()
433     ) {
434         navController.addOnDestinationChangedListener(
435             CollapsingToolbarOnDestinationChangedListener(
436                 collapsingToolbarLayout,
437                 toolbar,
438                 configuration
439             )
440         )
441         toolbar.setNavigationOnClickListener { navigateUp(navController, configuration) }
442     }
443 
444     /**
445      * Sets up a [NavigationView] for use with a [NavController]. This will call
446      * [onNavDestinationSelected] when a menu item is selected. The selected item in the
447      * NavigationView will automatically be updated when the destination changes.
448      *
449      * If the [NavigationView] is directly contained with an [Openable] layout, it will be closed
450      * when a menu item is selected.
451      *
452      * Similarly, if the [NavigationView] has a [BottomSheetBehavior] associated with it (as is the
453      * case when using a [com.google.android.material.bottomsheet.BottomSheetDialog]), the bottom
454      * sheet will be hidden when a menu item is selected.
455      *
456      * Destinations that implement [androidx.navigation.FloatingWindow] will be ignored.
457      *
458      * @param navigationView The NavigationView that should be kept in sync with changes to the
459      *   NavController.
460      * @param navController The NavController that supplies the primary and secondary menu.
461      *   Navigation actions on this NavController will be reflected in the selected item in the
462      *   NavigationView.
463      */
464     @JvmStatic
465     public fun setupWithNavController(
466         navigationView: NavigationView,
467         navController: NavController
468     ) {
469         navigationView.setNavigationItemSelectedListener { item ->
470             val handled = onNavDestinationSelected(item, navController)
471             if (handled) {
472                 val parent = navigationView.parent
473                 if (parent is Openable) {
474                     parent.close()
475                 } else {
476                     val bottomSheetBehavior = findBottomSheetBehavior(navigationView)
477                     if (bottomSheetBehavior != null) {
478                         bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
479                     }
480                 }
481             }
482             handled
483         }
484         val weakReference = WeakReference(navigationView)
485         navController.addOnDestinationChangedListener(
486             object : NavController.OnDestinationChangedListener {
487                 override fun onDestinationChanged(
488                     controller: NavController,
489                     destination: NavDestination,
490                     arguments: Bundle?
491                 ) {
492                     val view = weakReference.get()
493                     if (view == null) {
494                         navController.removeOnDestinationChangedListener(this)
495                         return
496                     }
497                     if (destination is FloatingWindow) {
498                         return
499                     }
500                     view.menu.forEach { item ->
501                         item.isChecked = destination.matchDestination(item.itemId)
502                     }
503                 }
504             }
505         )
506     }
507 
508     /**
509      * Sets up a [NavigationView] for use with a [NavController]. This will call
510      * [onNavDestinationSelected] when a menu item is selected. The selected item in the
511      * NavigationView will automatically be updated when the destination changes.
512      *
513      * If the [NavigationView] is directly contained with an [Openable] layout, it will be closed
514      * when a menu item is selected.
515      *
516      * Similarly, if the [NavigationView] has a [BottomSheetBehavior] associated with it (as is the
517      * case when using a [com.google.android.material.bottomsheet.BottomSheetDialog]), the bottom
518      * sheet will be hidden when a menu item is selected.
519      *
520      * Destinations that implement [androidx.navigation.FloatingWindow] will be ignored.
521      *
522      * @param navigationView The NavigationView that should be kept in sync with changes to the
523      *   NavController.
524      * @param navController The NavController that supplies the primary and secondary menu.
525      * @param saveState Whether the NavController should save the back stack state. This must always
526      *   be `false`: leave this parameter off entirely to use the non-experimental version of this
527      *   API, which saves the state by default.
528      *
529      * Navigation actions on this NavController will be reflected in the selected item in the
530      * NavigationView.
531      */
532     @NavigationUiSaveStateControl
533     @JvmStatic
534     public fun setupWithNavController(
535         navigationView: NavigationView,
536         navController: NavController,
537         saveState: Boolean
538     ) {
539         check(!saveState) {
540             "Leave the saveState parameter out entirely to use the non-experimental version of " +
541                 "this API, which saves the state by default"
542         }
543         navigationView.setNavigationItemSelectedListener { item ->
544             val handled = onNavDestinationSelected(item, navController, saveState)
545             if (handled) {
546                 val parent = navigationView.parent
547                 if (parent is Openable) {
548                     parent.close()
549                 } else {
550                     val bottomSheetBehavior = findBottomSheetBehavior(navigationView)
551                     if (bottomSheetBehavior != null) {
552                         bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
553                     }
554                 }
555             }
556             handled
557         }
558         val weakReference = WeakReference(navigationView)
559         navController.addOnDestinationChangedListener(
560             object : NavController.OnDestinationChangedListener {
561                 override fun onDestinationChanged(
562                     controller: NavController,
563                     destination: NavDestination,
564                     arguments: Bundle?
565                 ) {
566                     val view = weakReference.get()
567                     if (view == null) {
568                         navController.removeOnDestinationChangedListener(this)
569                         return
570                     }
571                     if (destination is FloatingWindow) {
572                         return
573                     }
574                     view.menu.forEach { item ->
575                         item.isChecked = destination.matchDestination(item.itemId)
576                     }
577                 }
578             }
579         )
580     }
581 
582     /**
583      * Walks up the view hierarchy, trying to determine if the given View is contained within a
584      * bottom sheet.
585      */
586     @JvmStatic
587     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
588     public fun findBottomSheetBehavior(view: View): BottomSheetBehavior<*>? {
589         val params = view.layoutParams
590         if (params !is CoordinatorLayout.LayoutParams) {
591             val parent = view.parent
592             return if (parent is View) {
593                 findBottomSheetBehavior(parent as View)
594             } else null
595         }
596         val behavior = params.behavior
597         return if (behavior !is BottomSheetBehavior<*>) {
598             // We hit a CoordinatorLayout, but the View doesn't have the BottomSheetBehavior
599             null
600         } else behavior
601     }
602 
603     /**
604      * Sets up a [NavigationBarView] for use with a [NavController]. This will call
605      * [onNavDestinationSelected] when a menu item is selected. The selected item in the
606      * NavigationBarView will automatically be updated when the destination changes.
607      *
608      * Destinations that implement [androidx.navigation.FloatingWindow] will be ignored.
609      *
610      * @param navigationBarView The NavigationBarView ([BottomNavigationView] or
611      *   [NavigationRailView]) that should be kept in sync with changes to the NavController.
612      * @param navController The NavController that supplies the primary menu. Navigation actions on
613      *   this NavController will be reflected in the selected item in the NavigationBarView.
614      */
615     @JvmStatic
616     public fun setupWithNavController(
617         navigationBarView: NavigationBarView,
618         navController: NavController
619     ) {
620         navigationBarView.setOnItemSelectedListener { item ->
621             onNavDestinationSelected(item, navController)
622         }
623         val weakReference = WeakReference(navigationBarView)
624         navController.addOnDestinationChangedListener(
625             object : NavController.OnDestinationChangedListener {
626                 override fun onDestinationChanged(
627                     controller: NavController,
628                     destination: NavDestination,
629                     arguments: Bundle?
630                 ) {
631                     val view = weakReference.get()
632                     if (view == null) {
633                         navController.removeOnDestinationChangedListener(this)
634                         return
635                     }
636                     if (destination is FloatingWindow) {
637                         return
638                     }
639                     view.menu.forEach { item ->
640                         if (destination.matchDestination(item.itemId)) {
641                             item.isChecked = true
642                         }
643                     }
644                 }
645             }
646         )
647     }
648 
649     /**
650      * Sets up a [NavigationBarView] for use with a [NavController]. This will call
651      * [onNavDestinationSelected] when a menu item is selected. The selected item in the
652      * NavigationBarView will automatically be updated when the destination changes.
653      *
654      * Destinations that implement [androidx.navigation.FloatingWindow] will be ignored.
655      *
656      * @param navigationBarView The NavigationBarView ([BottomNavigationView] or
657      *   [NavigationRailView]) that should be kept in sync with changes to the NavController.
658      * @param navController The NavController that supplies the primary menu.
659      * @param saveState Whether the NavController should save the back stack state. This must always
660      *   be `false`: leave this parameter off entirely to use the non-experimental version of this
661      *   API, which saves the state by default.
662      *
663      * Navigation actions on this NavController will be reflected in the selected item in the
664      * NavigationBarView.
665      */
666     @NavigationUiSaveStateControl
667     @JvmStatic
668     public fun setupWithNavController(
669         navigationBarView: NavigationBarView,
670         navController: NavController,
671         saveState: Boolean
672     ) {
673         check(!saveState) {
674             "Leave the saveState parameter out entirely to use the non-experimental version of " +
675                 "this API, which saves the state by default"
676         }
677         navigationBarView.setOnItemSelectedListener { item ->
678             onNavDestinationSelected(item, navController, saveState)
679         }
680         val weakReference = WeakReference(navigationBarView)
681         navController.addOnDestinationChangedListener(
682             object : NavController.OnDestinationChangedListener {
683                 override fun onDestinationChanged(
684                     controller: NavController,
685                     destination: NavDestination,
686                     arguments: Bundle?
687                 ) {
688                     val view = weakReference.get()
689                     if (view == null) {
690                         navController.removeOnDestinationChangedListener(this)
691                         return
692                     }
693                     if (destination is FloatingWindow) {
694                         return
695                     }
696                     view.menu.forEach { item ->
697                         if (destination.matchDestination(item.itemId)) {
698                             item.isChecked = true
699                         }
700                     }
701                 }
702             }
703         )
704     }
705 
706     /**
707      * Determines whether the given `destId` matches the NavDestination. This handles both the
708      * default case (the destination's id matches the given id) and the nested case where the given
709      * id is a parent/grandparent/etc of the destination.
710      */
711     @JvmStatic
712     internal fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
713         hierarchy.any { it.id == destId }
714 }
715