/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.navigation.ui; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.support.annotation.IdRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.BottomNavigationView; import android.support.design.widget.NavigationView; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; import android.support.v7.graphics.drawable.DrawerArrowDrawable; import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; import android.view.ViewParent; import androidx.navigation.NavController; import androidx.navigation.NavDestination; import androidx.navigation.NavGraph; import androidx.navigation.NavOptions; /** * Class which hooks up elements typically in the 'chrome' of your application such as global * navigation patterns like a navigation drawer or bottom nav bar with your {@link NavController}. */ public class NavigationUI { // No instances. Static utilities only. private NavigationUI() { } /** * Attempt to navigate to the {@link NavDestination} associated with the given MenuItem. This * MenuItem should have been added via one of the helper methods in this class. * *
Importantly, it assumes the {@link MenuItem#getItemId() menu item id} matches a valid * {@link NavDestination#getAction(int) action id} or * {@link NavDestination#getId() destination id} to be navigated to.
* * @param item The selected MenuItem. * @param navController The NavController that hosts the destination. * @return True if the {@link NavController} was able to navigate to the destination * associated with the given MenuItem. */ public static boolean onNavDestinationSelected(@NonNull MenuItem item, @NonNull NavController navController) { return onNavDestinationSelected(item, navController, false); } private static boolean onNavDestinationSelected(@NonNull MenuItem item, @NonNull NavController navController, boolean popUp) { NavOptions.Builder builder = new NavOptions.Builder() .setLaunchSingleTop(true) .setEnterAnim(R.anim.nav_default_enter_anim) .setExitAnim(R.anim.nav_default_exit_anim) .setPopEnterAnim(R.anim.nav_default_pop_enter_anim) .setPopExitAnim(R.anim.nav_default_pop_exit_anim); if (popUp) { builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false); } NavOptions options = builder.build(); try { //TODO provide proper API instead of using Exceptions as Control-Flow. navController.navigate(item.getItemId(), null, options); return true; } catch (IllegalArgumentException e) { return false; } } /** * Handles the Up button by delegating its behavior to the given NavController. This should * generally be called from {@link AppCompatActivity#onSupportNavigateUp()}. *If you do not have a {@link DrawerLayout}, you should call * {@link NavController#navigateUp()} directly. * * @param drawerLayout The DrawerLayout that should be opened if you are on the topmost level * of the app. * @param navController The NavController that hosts your content. * @return True if the {@link NavController} was able to navigate up. */ public static boolean navigateUp(@Nullable DrawerLayout drawerLayout, @NonNull NavController navController) { if (drawerLayout != null && navController.getCurrentDestination() == findStartDestination(navController.getGraph())) { drawerLayout.openDrawer(GravityCompat.START); return true; } else { return navController.navigateUp(); } } /** * Sets up the ActionBar returned by {@link AppCompatActivity#getSupportActionBar()} for use * with a {@link NavController}. * *
By calling this method, the title in the action bar will automatically be updated when * the destination changes (assuming there is a valid {@link NavDestination#getLabel label}). * *
The action bar will also display the Up button when you are on a non-root destination. * Call {@link #navigateUp(DrawerLayout, NavController)} to handle the Up button. * * @param activity The activity hosting the action bar that should be kept in sync with changes * to the NavController. * @param navController The NavController that supplies the secondary menu. Navigation actions * on this NavController will be reflected in the title of the action bar. */ public static void setupActionBarWithNavController(@NonNull AppCompatActivity activity, @NonNull NavController navController) { setupActionBarWithNavController(activity, navController, null); } /** * Sets up the ActionBar returned by {@link AppCompatActivity#getSupportActionBar()} for use * with a {@link NavController}. * *
By calling this method, the title in the action bar will automatically be updated when * the destination changes (assuming there is a valid {@link NavDestination#getLabel label}). * *
The action bar will also display the Up button when you are on a non-root destination and
* the drawer icon when on the root destination, automatically animating between them.
* Call {@link #navigateUp(DrawerLayout, NavController)} to handle the Up button.
* @param activity The activity hosting the action bar that should be kept in sync with changes
* to the NavController.
* @param navController The NavController whose navigation actions will be reflected
* in the title of the action bar.
* @param drawerLayout The DrawerLayout that should be toggled from the home button
*/
public static void setupActionBarWithNavController(@NonNull AppCompatActivity activity,
@NonNull NavController navController,
@Nullable DrawerLayout drawerLayout) {
navController.addOnNavigatedListener(
new ActionBarOnNavigatedListener(activity, drawerLayout));
}
/**
* Sets up a {@link NavigationView} for use with a {@link NavController}. This will call
* {@link #onNavDestinationSelected(MenuItem, NavController)} when a menu item is selected.
* The selected item in the NavigationView will automatically be updated when the destination
* changes.
*
* @param navigationView The NavigationView that should be kept in sync with changes to the
* NavController.
* @param navController The NavController that supplies the primary and secondary menu.
* Navigation actions on this NavController will be reflected in the
* selected item in the NavigationView.
*/
public static void setupWithNavController(@NonNull final NavigationView navigationView,
@NonNull final NavController navController) {
navigationView.setNavigationItemSelectedListener(
new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
boolean handled = onNavDestinationSelected(item, navController, true);
if (handled) {
ViewParent parent = navigationView.getParent();
if (parent instanceof DrawerLayout) {
((DrawerLayout) parent).closeDrawer(navigationView);
}
}
return handled;
}
});
navController.addOnNavigatedListener(new NavController.OnNavigatedListener() {
@Override
public void onNavigated(@NonNull NavController controller,
@NonNull NavDestination destination) {
Menu menu = navigationView.getMenu();
for (int h = 0, size = menu.size(); h < size; h++) {
MenuItem item = menu.getItem(h);
item.setChecked(matchDestination(destination, item.getItemId()));
}
}
});
}
/**
* Sets up a {@link BottomNavigationView} for use with a {@link NavController}. This will call
* {@link #onNavDestinationSelected(MenuItem, NavController)} when a menu item is selected. The
* selected item in the BottomNavigationView will automatically be updated when the destination
* changes.
*
* @param bottomNavigationView The BottomNavigationView that should be kept in sync with
* changes to the NavController.
* @param navController The NavController that supplies the primary menu.
* Navigation actions on this NavController will be reflected in the
* selected item in the BottomNavigationView.
*/
public static void setupWithNavController(
@NonNull final BottomNavigationView bottomNavigationView,
@NonNull final NavController navController) {
bottomNavigationView.setOnNavigationItemSelectedListener(
new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
return onNavDestinationSelected(item, navController, true);
}
});
navController.addOnNavigatedListener(new NavController.OnNavigatedListener() {
@Override
public void onNavigated(@NonNull NavController controller,
@NonNull NavDestination destination) {
Menu menu = bottomNavigationView.getMenu();
for (int h = 0, size = menu.size(); h < size; h++) {
MenuItem item = menu.getItem(h);
if (matchDestination(destination, item.getItemId())) {
item.setChecked(true);
}
}
}
});
}
/**
* Determines whether the given destId
matches the NavDestination. This handles
* both the default case (the destination's id matches the given id) and the nested case where
* the given id is a parent/grandparent/etc of the destination.
*/
private static boolean matchDestination(@NonNull NavDestination destination,
@IdRes int destId) {
NavDestination currentDestination = destination;
while (currentDestination.getId() != destId && currentDestination.getParent() != null) {
currentDestination = currentDestination.getParent();
}
return currentDestination.getId() == destId;
}
/**
* Finds the actual start destination of the graph, handling cases where the graph's starting
* destination is itself a NavGraph.
*/
private static NavDestination findStartDestination(@NonNull NavGraph graph) {
NavDestination startDestination = graph;
while (startDestination instanceof NavGraph) {
NavGraph parent = (NavGraph) startDestination;
startDestination = parent.findNode(parent.getStartDestination());
}
return startDestination;
}
/**
* The OnNavigatedListener specifically for keeping the ActionBar updated. This handles both
* updating the title and updating the Up Indicator transitioning between the
*/
private static class ActionBarOnNavigatedListener implements NavController.OnNavigatedListener {
private final AppCompatActivity mActivity;
@Nullable
private final DrawerLayout mDrawerLayout;
private DrawerArrowDrawable mArrowDrawable;
private ValueAnimator mAnimator;
ActionBarOnNavigatedListener(
@NonNull AppCompatActivity activity, @Nullable DrawerLayout drawerLayout) {
mActivity = activity;
mDrawerLayout = drawerLayout;
}
@Override
public void onNavigated(@NonNull NavController controller,
@NonNull NavDestination destination) {
ActionBar actionBar = mActivity.getSupportActionBar();
CharSequence title = destination.getLabel();
if (!TextUtils.isEmpty(title)) {
actionBar.setTitle(title);
}
boolean isStartDestination = findStartDestination(controller.getGraph()) == destination;
actionBar.setDisplayHomeAsUpEnabled(mDrawerLayout != null || !isStartDestination);
setActionBarUpIndicator(mDrawerLayout != null && isStartDestination);
}
void setActionBarUpIndicator(boolean showAsDrawerIndicator) {
ActionBarDrawerToggle.Delegate delegate = mActivity.getDrawerToggleDelegate();
boolean animate = true;
if (mArrowDrawable == null) {
mArrowDrawable = new DrawerArrowDrawable(
delegate.getActionBarThemedContext());
delegate.setActionBarUpIndicator(mArrowDrawable, 0);
// We're setting the initial state, so skip the animation
animate = false;
}
float endValue = showAsDrawerIndicator ? 0f : 1f;
if (animate) {
float startValue = mArrowDrawable.getProgress();
if (mAnimator != null) {
mAnimator.cancel();
}
mAnimator = ObjectAnimator.ofFloat(mArrowDrawable, "progress",
startValue, endValue);
mAnimator.start();
} else {
mArrowDrawable.setProgress(endValue);
}
}
}
}