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