1/* 2 * 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 17package androidx.navigation.ui; 18 19import android.animation.ObjectAnimator; 20import android.animation.ValueAnimator; 21import android.support.annotation.IdRes; 22import android.support.annotation.NonNull; 23import android.support.annotation.Nullable; 24import android.support.design.widget.BottomNavigationView; 25import android.support.design.widget.NavigationView; 26import android.support.v4.view.GravityCompat; 27import android.support.v4.widget.DrawerLayout; 28import android.support.v7.app.ActionBar; 29import android.support.v7.app.ActionBarDrawerToggle; 30import android.support.v7.app.AppCompatActivity; 31import android.support.v7.graphics.drawable.DrawerArrowDrawable; 32import android.text.TextUtils; 33import android.view.Menu; 34import android.view.MenuItem; 35import android.view.ViewParent; 36 37import androidx.navigation.NavController; 38import androidx.navigation.NavDestination; 39import androidx.navigation.NavGraph; 40import androidx.navigation.NavOptions; 41 42/** 43 * Class which hooks up elements typically in the 'chrome' of your application such as global 44 * navigation patterns like a navigation drawer or bottom nav bar with your {@link NavController}. 45 */ 46public class NavigationUI { 47 48 // No instances. Static utilities only. 49 private NavigationUI() { 50 } 51 52 /** 53 * Attempt to navigate to the {@link NavDestination} associated with the given MenuItem. This 54 * MenuItem should have been added via one of the helper methods in this class. 55 * 56 * <p>Importantly, it assumes the {@link MenuItem#getItemId() menu item id} matches a valid 57 * {@link NavDestination#getAction(int) action id} or 58 * {@link NavDestination#getId() destination id} to be navigated to.</p> 59 * 60 * @param item The selected MenuItem. 61 * @param navController The NavController that hosts the destination. 62 * @return True if the {@link NavController} was able to navigate to the destination 63 * associated with the given MenuItem. 64 */ 65 public static boolean onNavDestinationSelected(@NonNull MenuItem item, 66 @NonNull NavController navController) { 67 return onNavDestinationSelected(item, navController, false); 68 } 69 70 private static boolean onNavDestinationSelected(@NonNull MenuItem item, 71 @NonNull NavController navController, boolean popUp) { 72 NavOptions.Builder builder = new NavOptions.Builder() 73 .setLaunchSingleTop(true) 74 .setEnterAnim(R.anim.nav_default_enter_anim) 75 .setExitAnim(R.anim.nav_default_exit_anim) 76 .setPopEnterAnim(R.anim.nav_default_pop_enter_anim) 77 .setPopExitAnim(R.anim.nav_default_pop_exit_anim); 78 if (popUp) { 79 builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false); 80 } 81 NavOptions options = builder.build(); 82 try { 83 //TODO provide proper API instead of using Exceptions as Control-Flow. 84 navController.navigate(item.getItemId(), null, options); 85 return true; 86 } catch (IllegalArgumentException e) { 87 return false; 88 } 89 } 90 91 /** 92 * Handles the Up button by delegating its behavior to the given NavController. This should 93 * generally be called from {@link AppCompatActivity#onSupportNavigateUp()}. 94 * <p>If you do not have a {@link DrawerLayout}, you should call 95 * {@link NavController#navigateUp()} directly. 96 * 97 * @param drawerLayout The DrawerLayout that should be opened if you are on the topmost level 98 * of the app. 99 * @param navController The NavController that hosts your content. 100 * @return True if the {@link NavController} was able to navigate up. 101 */ 102 public static boolean navigateUp(@Nullable DrawerLayout drawerLayout, 103 @NonNull NavController navController) { 104 if (drawerLayout != null && navController.getCurrentDestination() 105 == findStartDestination(navController.getGraph())) { 106 drawerLayout.openDrawer(GravityCompat.START); 107 return true; 108 } else { 109 return navController.navigateUp(); 110 } 111 } 112 113 /** 114 * Sets up the ActionBar returned by {@link AppCompatActivity#getSupportActionBar()} for use 115 * with a {@link NavController}. 116 * 117 * <p>By calling this method, the title in the action bar will automatically be updated when 118 * the destination changes (assuming there is a valid {@link NavDestination#getLabel label}). 119 * 120 * <p>The action bar will also display the Up button when you are on a non-root destination. 121 * Call {@link #navigateUp(DrawerLayout, NavController)} to handle the Up button. 122 * 123 * @param activity The activity hosting the action bar that should be kept in sync with changes 124 * to the NavController. 125 * @param navController The NavController that supplies the secondary menu. Navigation actions 126 * on this NavController will be reflected in the title of the action bar. 127 */ 128 public static void setupActionBarWithNavController(@NonNull AppCompatActivity activity, 129 @NonNull NavController navController) { 130 setupActionBarWithNavController(activity, navController, null); 131 } 132 133 /** 134 * Sets up the ActionBar returned by {@link AppCompatActivity#getSupportActionBar()} for use 135 * with a {@link NavController}. 136 * 137 * <p>By calling this method, the title in the action bar will automatically be updated when 138 * the destination changes (assuming there is a valid {@link NavDestination#getLabel label}). 139 * 140 * <p>The action bar will also display the Up button when you are on a non-root destination and 141 * the drawer icon when on the root destination, automatically animating between them. 142 * Call {@link #navigateUp(DrawerLayout, NavController)} to handle the Up button. 143 * @param activity The activity hosting the action bar that should be kept in sync with changes 144 * to the NavController. 145 * @param navController The NavController whose navigation actions will be reflected 146 * in the title of the action bar. 147 * @param drawerLayout The DrawerLayout that should be toggled from the home button 148 */ 149 public static void setupActionBarWithNavController(@NonNull AppCompatActivity activity, 150 @NonNull NavController navController, 151 @Nullable DrawerLayout drawerLayout) { 152 navController.addOnNavigatedListener( 153 new ActionBarOnNavigatedListener(activity, drawerLayout)); 154 } 155 156 /** 157 * Sets up a {@link NavigationView} for use with a {@link NavController}. This will call 158 * {@link #onNavDestinationSelected(MenuItem, NavController)} when a menu item is selected. 159 * The selected item in the NavigationView will automatically be updated when the destination 160 * changes. 161 * 162 * @param navigationView The NavigationView that should be kept in sync with changes to the 163 * NavController. 164 * @param navController The NavController that supplies the primary and secondary menu. 165 * Navigation actions on this NavController will be reflected in the 166 * selected item in the NavigationView. 167 */ 168 public static void setupWithNavController(@NonNull final NavigationView navigationView, 169 @NonNull final NavController navController) { 170 navigationView.setNavigationItemSelectedListener( 171 new NavigationView.OnNavigationItemSelectedListener() { 172 @Override 173 public boolean onNavigationItemSelected(@NonNull MenuItem item) { 174 boolean handled = onNavDestinationSelected(item, navController, true); 175 if (handled) { 176 ViewParent parent = navigationView.getParent(); 177 if (parent instanceof DrawerLayout) { 178 ((DrawerLayout) parent).closeDrawer(navigationView); 179 } 180 } 181 return handled; 182 } 183 }); 184 navController.addOnNavigatedListener(new NavController.OnNavigatedListener() { 185 @Override 186 public void onNavigated(@NonNull NavController controller, 187 @NonNull NavDestination destination) { 188 Menu menu = navigationView.getMenu(); 189 for (int h = 0, size = menu.size(); h < size; h++) { 190 MenuItem item = menu.getItem(h); 191 item.setChecked(matchDestination(destination, item.getItemId())); 192 } 193 } 194 }); 195 } 196 197 /** 198 * Sets up a {@link BottomNavigationView} for use with a {@link NavController}. This will call 199 * {@link #onNavDestinationSelected(MenuItem, NavController)} when a menu item is selected. The 200 * selected item in the BottomNavigationView will automatically be updated when the destination 201 * changes. 202 * 203 * @param bottomNavigationView The BottomNavigationView that should be kept in sync with 204 * changes to the NavController. 205 * @param navController The NavController that supplies the primary menu. 206 * Navigation actions on this NavController will be reflected in the 207 * selected item in the BottomNavigationView. 208 */ 209 public static void setupWithNavController( 210 @NonNull final BottomNavigationView bottomNavigationView, 211 @NonNull final NavController navController) { 212 bottomNavigationView.setOnNavigationItemSelectedListener( 213 new BottomNavigationView.OnNavigationItemSelectedListener() { 214 @Override 215 public boolean onNavigationItemSelected(@NonNull MenuItem item) { 216 return onNavDestinationSelected(item, navController, true); 217 } 218 }); 219 navController.addOnNavigatedListener(new NavController.OnNavigatedListener() { 220 @Override 221 public void onNavigated(@NonNull NavController controller, 222 @NonNull NavDestination destination) { 223 Menu menu = bottomNavigationView.getMenu(); 224 for (int h = 0, size = menu.size(); h < size; h++) { 225 MenuItem item = menu.getItem(h); 226 if (matchDestination(destination, item.getItemId())) { 227 item.setChecked(true); 228 } 229 } 230 } 231 }); 232 } 233 234 /** 235 * Determines whether the given <code>destId</code> matches the NavDestination. This handles 236 * both the default case (the destination's id matches the given id) and the nested case where 237 * the given id is a parent/grandparent/etc of the destination. 238 */ 239 private static boolean matchDestination(@NonNull NavDestination destination, 240 @IdRes int destId) { 241 NavDestination currentDestination = destination; 242 while (currentDestination.getId() != destId && currentDestination.getParent() != null) { 243 currentDestination = currentDestination.getParent(); 244 } 245 return currentDestination.getId() == destId; 246 } 247 248 /** 249 * Finds the actual start destination of the graph, handling cases where the graph's starting 250 * destination is itself a NavGraph. 251 */ 252 private static NavDestination findStartDestination(@NonNull NavGraph graph) { 253 NavDestination startDestination = graph; 254 while (startDestination instanceof NavGraph) { 255 NavGraph parent = (NavGraph) startDestination; 256 startDestination = parent.findNode(parent.getStartDestination()); 257 } 258 return startDestination; 259 } 260 261 /** 262 * The OnNavigatedListener specifically for keeping the ActionBar updated. This handles both 263 * updating the title and updating the Up Indicator transitioning between the 264 */ 265 private static class ActionBarOnNavigatedListener implements NavController.OnNavigatedListener { 266 private final AppCompatActivity mActivity; 267 @Nullable 268 private final DrawerLayout mDrawerLayout; 269 private DrawerArrowDrawable mArrowDrawable; 270 private ValueAnimator mAnimator; 271 272 ActionBarOnNavigatedListener( 273 @NonNull AppCompatActivity activity, @Nullable DrawerLayout drawerLayout) { 274 mActivity = activity; 275 mDrawerLayout = drawerLayout; 276 } 277 278 @Override 279 public void onNavigated(@NonNull NavController controller, 280 @NonNull NavDestination destination) { 281 ActionBar actionBar = mActivity.getSupportActionBar(); 282 CharSequence title = destination.getLabel(); 283 if (!TextUtils.isEmpty(title)) { 284 actionBar.setTitle(title); 285 } 286 boolean isStartDestination = findStartDestination(controller.getGraph()) == destination; 287 actionBar.setDisplayHomeAsUpEnabled(mDrawerLayout != null || !isStartDestination); 288 setActionBarUpIndicator(mDrawerLayout != null && isStartDestination); 289 } 290 291 void setActionBarUpIndicator(boolean showAsDrawerIndicator) { 292 ActionBarDrawerToggle.Delegate delegate = mActivity.getDrawerToggleDelegate(); 293 boolean animate = true; 294 if (mArrowDrawable == null) { 295 mArrowDrawable = new DrawerArrowDrawable( 296 delegate.getActionBarThemedContext()); 297 delegate.setActionBarUpIndicator(mArrowDrawable, 0); 298 // We're setting the initial state, so skip the animation 299 animate = false; 300 } 301 float endValue = showAsDrawerIndicator ? 0f : 1f; 302 if (animate) { 303 float startValue = mArrowDrawable.getProgress(); 304 if (mAnimator != null) { 305 mAnimator.cancel(); 306 } 307 mAnimator = ObjectAnimator.ofFloat(mArrowDrawable, "progress", 308 startValue, endValue); 309 mAnimator.start(); 310 } else { 311 mArrowDrawable.setProgress(endValue); 312 } 313 } 314 } 315} 316