1/* 2 * Copyright (C) 2014 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 */ 16package androidx.appcompat.app; 17 18import android.app.ActionBar; 19import android.app.Activity; 20import android.content.Context; 21import android.content.res.Configuration; 22import android.content.res.TypedArray; 23import android.graphics.drawable.Drawable; 24import android.os.Build; 25import android.util.Log; 26import android.view.MenuItem; 27import android.view.View; 28 29import androidx.annotation.NonNull; 30import androidx.annotation.Nullable; 31import androidx.annotation.StringRes; 32import androidx.appcompat.graphics.drawable.DrawerArrowDrawable; 33import androidx.appcompat.widget.Toolbar; 34import androidx.core.view.GravityCompat; 35import androidx.drawerlayout.widget.DrawerLayout; 36 37/** 38 * This class provides a handy way to tie together the functionality of 39 * {@link DrawerLayout} and the framework <code>ActionBar</code> to 40 * implement the recommended design for navigation drawers. 41 * 42 * <p>To use <code>ActionBarDrawerToggle</code>, create one in your Activity and call through 43 * to the following methods corresponding to your Activity callbacks:</p> 44 * 45 * <ul> 46 * <li>{@link android.app.Activity#onConfigurationChanged(android.content.res.Configuration) 47 * onConfigurationChanged} 48 * <li>{@link android.app.Activity#onOptionsItemSelected(android.view.MenuItem) 49 * onOptionsItemSelected}</li> 50 * </ul> 51 * 52 * <p>Call {@link #syncState()} from your <code>Activity</code>'s 53 * {@link android.app.Activity#onPostCreate(android.os.Bundle) onPostCreate} to synchronize the 54 * indicator with the state of the linked DrawerLayout after <code>onRestoreInstanceState</code> 55 * has occurred.</p> 56 * 57 * <p><code>ActionBarDrawerToggle</code> can be used directly as a 58 * {@link DrawerLayout.DrawerListener}, or if you are already providing 59 * your own listener, call through to each of the listener methods from your own.</p> 60 * 61 * <p> 62 * You can customize the the animated toggle by defining the 63 * {@link androidx.appcompat.R.styleable#DrawerArrowToggle drawerArrowStyle} in your 64 * ActionBar theme. 65 */ 66public class ActionBarDrawerToggle implements DrawerLayout.DrawerListener { 67 68 /** 69 * Allows an implementing Activity to return an {@link ActionBarDrawerToggle.Delegate} to use 70 * with ActionBarDrawerToggle. 71 */ 72 public interface DelegateProvider { 73 74 /** 75 * @return Delegate to use for ActionBarDrawableToggles, or null if the Activity 76 * does not wish to override the default behavior. 77 */ 78 @Nullable 79 Delegate getDrawerToggleDelegate(); 80 } 81 82 public interface Delegate { 83 84 /** 85 * Set the Action Bar's up indicator drawable and content description. 86 * 87 * @param upDrawable - Drawable to set as up indicator 88 * @param contentDescRes - Content description to set 89 */ 90 void setActionBarUpIndicator(Drawable upDrawable, @StringRes int contentDescRes); 91 92 /** 93 * Set the Action Bar's up indicator content description. 94 * 95 * @param contentDescRes - Content description to set 96 */ 97 void setActionBarDescription(@StringRes int contentDescRes); 98 99 /** 100 * Returns the drawable to be set as up button when DrawerToggle is disabled 101 */ 102 Drawable getThemeUpIndicator(); 103 104 /** 105 * Returns the context of ActionBar 106 */ 107 Context getActionBarThemedContext(); 108 109 /** 110 * Returns whether navigation icon is visible or not. 111 * Used to print warning messages in case developer forgets to set displayHomeAsUp to true 112 */ 113 boolean isNavigationVisible(); 114 } 115 116 private final Delegate mActivityImpl; 117 private final DrawerLayout mDrawerLayout; 118 119 private DrawerArrowDrawable mSlider; 120 private boolean mDrawerSlideAnimationEnabled = true; 121 private Drawable mHomeAsUpIndicator; 122 boolean mDrawerIndicatorEnabled = true; 123 private boolean mHasCustomUpIndicator; 124 private final int mOpenDrawerContentDescRes; 125 private final int mCloseDrawerContentDescRes; 126 // used in toolbar mode when DrawerToggle is disabled 127 View.OnClickListener mToolbarNavigationClickListener; 128 // If developer does not set displayHomeAsUp, DrawerToggle won't show up. 129 // DrawerToggle logs a warning if this case is detected 130 private boolean mWarnedForDisplayHomeAsUp = false; 131 132 /** 133 * Construct a new ActionBarDrawerToggle. 134 * 135 * <p>The given {@link Activity} will be linked to the specified {@link DrawerLayout} and 136 * its Actionbar's Up button will be set to a custom drawable. 137 * <p>This drawable shows a Hamburger icon when drawer is closed and an arrow when drawer 138 * is open. It animates between these two states as the drawer opens.</p> 139 * 140 * <p>String resources must be provided to describe the open/close drawer actions for 141 * accessibility services.</p> 142 * 143 * @param activity The Activity hosting the drawer. Should have an ActionBar. 144 * @param drawerLayout The DrawerLayout to link to the given Activity's ActionBar 145 * @param openDrawerContentDescRes A String resource to describe the "open drawer" action 146 * for accessibility 147 * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action 148 * for accessibility 149 */ 150 public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, 151 @StringRes int openDrawerContentDescRes, 152 @StringRes int closeDrawerContentDescRes) { 153 this(activity, null, drawerLayout, null, openDrawerContentDescRes, 154 closeDrawerContentDescRes); 155 } 156 157 /** 158 * Construct a new ActionBarDrawerToggle with a Toolbar. 159 * <p> 160 * The given {@link Activity} will be linked to the specified {@link DrawerLayout} and 161 * the Toolbar's navigation icon will be set to a custom drawable. Using this constructor 162 * will set Toolbar's navigation click listener to toggle the drawer when it is clicked. 163 * <p> 164 * This drawable shows a Hamburger icon when drawer is closed and an arrow when drawer 165 * is open. It animates between these two states as the drawer opens. 166 * <p> 167 * String resources must be provided to describe the open/close drawer actions for 168 * accessibility services. 169 * <p> 170 * Please use {@link #ActionBarDrawerToggle(Activity, DrawerLayout, int, int)} if you are 171 * setting the Toolbar as the ActionBar of your activity. 172 * 173 * @param activity The Activity hosting the drawer. 174 * @param toolbar The toolbar to use if you have an independent Toolbar. 175 * @param drawerLayout The DrawerLayout to link to the given Activity's ActionBar 176 * @param openDrawerContentDescRes A String resource to describe the "open drawer" action 177 * for accessibility 178 * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action 179 * for accessibility 180 */ 181 public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, 182 Toolbar toolbar, @StringRes int openDrawerContentDescRes, 183 @StringRes int closeDrawerContentDescRes) { 184 this(activity, toolbar, drawerLayout, null, openDrawerContentDescRes, 185 closeDrawerContentDescRes); 186 } 187 188 /** 189 * In the future, we can make this constructor public if we want to let developers customize 190 * the 191 * animation. 192 */ 193 ActionBarDrawerToggle(Activity activity, Toolbar toolbar, DrawerLayout drawerLayout, 194 DrawerArrowDrawable slider, @StringRes int openDrawerContentDescRes, 195 @StringRes int closeDrawerContentDescRes) { 196 if (toolbar != null) { 197 mActivityImpl = new ToolbarCompatDelegate(toolbar); 198 toolbar.setNavigationOnClickListener(new View.OnClickListener() { 199 @Override 200 public void onClick(View v) { 201 if (mDrawerIndicatorEnabled) { 202 toggle(); 203 } else if (mToolbarNavigationClickListener != null) { 204 mToolbarNavigationClickListener.onClick(v); 205 } 206 } 207 }); 208 } else if (activity instanceof DelegateProvider) { // Allow the Activity to provide an impl 209 mActivityImpl = ((DelegateProvider) activity).getDrawerToggleDelegate(); 210 } else { 211 mActivityImpl = new FrameworkActionBarDelegate(activity); 212 } 213 214 mDrawerLayout = drawerLayout; 215 mOpenDrawerContentDescRes = openDrawerContentDescRes; 216 mCloseDrawerContentDescRes = closeDrawerContentDescRes; 217 if (slider == null) { 218 mSlider = new DrawerArrowDrawable(mActivityImpl.getActionBarThemedContext()); 219 } else { 220 mSlider = slider; 221 } 222 223 mHomeAsUpIndicator = getThemeUpIndicator(); 224 } 225 226 /** 227 * Synchronize the state of the drawer indicator/affordance with the linked DrawerLayout. 228 * 229 * <p>This should be called from your <code>Activity</code>'s 230 * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} method to synchronize after 231 * the DrawerLayout's instance state has been restored, and any other time when the state 232 * may have diverged in such a way that the ActionBarDrawerToggle was not notified. 233 * (For example, if you stop forwarding appropriate drawer events for a period of time.)</p> 234 */ 235 public void syncState() { 236 if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) { 237 setPosition(1); 238 } else { 239 setPosition(0); 240 } 241 if (mDrawerIndicatorEnabled) { 242 setActionBarUpIndicator(mSlider, 243 mDrawerLayout.isDrawerOpen(GravityCompat.START) ? 244 mCloseDrawerContentDescRes : mOpenDrawerContentDescRes); 245 } 246 } 247 248 /** 249 * This method should always be called by your <code>Activity</code>'s 250 * {@link Activity#onConfigurationChanged(android.content.res.Configuration) 251 * onConfigurationChanged} 252 * method. 253 * 254 * @param newConfig The new configuration 255 */ 256 public void onConfigurationChanged(Configuration newConfig) { 257 // Reload drawables that can change with configuration 258 if (!mHasCustomUpIndicator) { 259 mHomeAsUpIndicator = getThemeUpIndicator(); 260 } 261 syncState(); 262 } 263 264 /** 265 * This method should be called by your <code>Activity</code>'s 266 * {@link Activity#onOptionsItemSelected(android.view.MenuItem) onOptionsItemSelected} method. 267 * If it returns true, your <code>onOptionsItemSelected</code> method should return true and 268 * skip further processing. 269 * 270 * @param item the MenuItem instance representing the selected menu item 271 * @return true if the event was handled and further processing should not occur 272 */ 273 public boolean onOptionsItemSelected(MenuItem item) { 274 if (item != null && item.getItemId() == android.R.id.home && mDrawerIndicatorEnabled) { 275 toggle(); 276 return true; 277 } 278 return false; 279 } 280 281 void toggle() { 282 int drawerLockMode = mDrawerLayout.getDrawerLockMode(GravityCompat.START); 283 if (mDrawerLayout.isDrawerVisible(GravityCompat.START) 284 && (drawerLockMode != DrawerLayout.LOCK_MODE_LOCKED_OPEN)) { 285 mDrawerLayout.closeDrawer(GravityCompat.START); 286 } else if (drawerLockMode != DrawerLayout.LOCK_MODE_LOCKED_CLOSED) { 287 mDrawerLayout.openDrawer(GravityCompat.START); 288 } 289 } 290 291 /** 292 * Set the up indicator to display when the drawer indicator is not 293 * enabled. 294 * <p> 295 * If you pass <code>null</code> to this method, the default drawable from 296 * the theme will be used. 297 * 298 * @param indicator A drawable to use for the up indicator, or null to use 299 * the theme's default 300 * @see #setDrawerIndicatorEnabled(boolean) 301 */ 302 public void setHomeAsUpIndicator(Drawable indicator) { 303 if (indicator == null) { 304 mHomeAsUpIndicator = getThemeUpIndicator(); 305 mHasCustomUpIndicator = false; 306 } else { 307 mHomeAsUpIndicator = indicator; 308 mHasCustomUpIndicator = true; 309 } 310 311 if (!mDrawerIndicatorEnabled) { 312 setActionBarUpIndicator(mHomeAsUpIndicator, 0); 313 } 314 } 315 316 /** 317 * Set the up indicator to display when the drawer indicator is not 318 * enabled. 319 * <p> 320 * If you pass 0 to this method, the default drawable from the theme will 321 * be used. 322 * 323 * @param resId Resource ID of a drawable to use for the up indicator, or 0 324 * to use the theme's default 325 * @see #setDrawerIndicatorEnabled(boolean) 326 */ 327 public void setHomeAsUpIndicator(int resId) { 328 Drawable indicator = null; 329 if (resId != 0) { 330 indicator = mDrawerLayout.getResources().getDrawable(resId); 331 } 332 setHomeAsUpIndicator(indicator); 333 } 334 335 /** 336 * @return true if the enhanced drawer indicator is enabled, false otherwise 337 * @see #setDrawerIndicatorEnabled(boolean) 338 */ 339 public boolean isDrawerIndicatorEnabled() { 340 return mDrawerIndicatorEnabled; 341 } 342 343 /** 344 * Enable or disable the drawer indicator. The indicator defaults to enabled. 345 * 346 * <p>When the indicator is disabled, the <code>ActionBar</code> will revert to displaying 347 * the home-as-up indicator provided by the <code>Activity</code>'s theme in the 348 * <code>android.R.attr.homeAsUpIndicator</code> attribute instead of the animated 349 * drawer glyph.</p> 350 * 351 * @param enable true to enable, false to disable 352 */ 353 public void setDrawerIndicatorEnabled(boolean enable) { 354 if (enable != mDrawerIndicatorEnabled) { 355 if (enable) { 356 setActionBarUpIndicator(mSlider, 357 mDrawerLayout.isDrawerOpen(GravityCompat.START) ? 358 mCloseDrawerContentDescRes : mOpenDrawerContentDescRes); 359 } else { 360 setActionBarUpIndicator(mHomeAsUpIndicator, 0); 361 } 362 mDrawerIndicatorEnabled = enable; 363 } 364 } 365 366 /** 367 * @return DrawerArrowDrawable that is currently shown by the ActionBarDrawerToggle. 368 */ 369 @NonNull 370 public DrawerArrowDrawable getDrawerArrowDrawable() { 371 return mSlider; 372 } 373 374 /** 375 * Sets the DrawerArrowDrawable that should be shown by this ActionBarDrawerToggle. 376 * 377 * @param drawable DrawerArrowDrawable that should be shown by this ActionBarDrawerToggle 378 */ 379 public void setDrawerArrowDrawable(@NonNull DrawerArrowDrawable drawable) { 380 mSlider = drawable; 381 syncState(); 382 } 383 384 /** 385 * Specifies whether the drawer arrow should animate when the drawer position changes. 386 * 387 * @param enabled if this is {@code true} then the animation will run, else it will be skipped 388 */ 389 public void setDrawerSlideAnimationEnabled(boolean enabled) { 390 mDrawerSlideAnimationEnabled = enabled; 391 if (!enabled) { 392 setPosition(0); 393 } 394 } 395 396 /** 397 * @return whether the drawer slide animation is enabled 398 */ 399 public boolean isDrawerSlideAnimationEnabled() { 400 return mDrawerSlideAnimationEnabled; 401 } 402 403 /** 404 * {@link DrawerLayout.DrawerListener} callback method. If you do not use your 405 * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call 406 * through to this method from your own listener object. 407 * 408 * @param drawerView The child view that was moved 409 * @param slideOffset The new offset of this drawer within its range, from 0-1 410 */ 411 @Override 412 public void onDrawerSlide(View drawerView, float slideOffset) { 413 if (mDrawerSlideAnimationEnabled) { 414 setPosition(Math.min(1f, Math.max(0, slideOffset))); 415 } else { 416 setPosition(0); // disable animation. 417 } 418 } 419 420 /** 421 * {@link DrawerLayout.DrawerListener} callback method. If you do not use your 422 * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call 423 * through to this method from your own listener object. 424 * 425 * @param drawerView Drawer view that is now open 426 */ 427 @Override 428 public void onDrawerOpened(View drawerView) { 429 setPosition(1); 430 if (mDrawerIndicatorEnabled) { 431 setActionBarDescription(mCloseDrawerContentDescRes); 432 } 433 } 434 435 /** 436 * {@link DrawerLayout.DrawerListener} callback method. If you do not use your 437 * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call 438 * through to this method from your own listener object. 439 * 440 * @param drawerView Drawer view that is now closed 441 */ 442 @Override 443 public void onDrawerClosed(View drawerView) { 444 setPosition(0); 445 if (mDrawerIndicatorEnabled) { 446 setActionBarDescription(mOpenDrawerContentDescRes); 447 } 448 } 449 450 /** 451 * {@link DrawerLayout.DrawerListener} callback method. If you do not use your 452 * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call 453 * through to this method from your own listener object. 454 * 455 * @param newState The new drawer motion state 456 */ 457 @Override 458 public void onDrawerStateChanged(int newState) { 459 } 460 461 /** 462 * Returns the fallback listener for Navigation icon click events. 463 * 464 * @return The click listener which receives Navigation click events from Toolbar when 465 * drawer indicator is disabled. 466 * @see #setToolbarNavigationClickListener(android.view.View.OnClickListener) 467 * @see #setDrawerIndicatorEnabled(boolean) 468 * @see #isDrawerIndicatorEnabled() 469 */ 470 public View.OnClickListener getToolbarNavigationClickListener() { 471 return mToolbarNavigationClickListener; 472 } 473 474 /** 475 * When DrawerToggle is constructed with a Toolbar, it sets the click listener on 476 * the Navigation icon. If you want to listen for clicks on the Navigation icon when 477 * DrawerToggle is disabled ({@link #setDrawerIndicatorEnabled(boolean)}, you should call this 478 * method with your listener and DrawerToggle will forward click events to that listener 479 * when drawer indicator is disabled. 480 * 481 * @see #setDrawerIndicatorEnabled(boolean) 482 */ 483 public void setToolbarNavigationClickListener( 484 View.OnClickListener onToolbarNavigationClickListener) { 485 mToolbarNavigationClickListener = onToolbarNavigationClickListener; 486 } 487 488 void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) { 489 if (!mWarnedForDisplayHomeAsUp && !mActivityImpl.isNavigationVisible()) { 490 Log.w("ActionBarDrawerToggle", "DrawerToggle may not show up because NavigationIcon" 491 + " is not visible. You may need to call " 492 + "actionbar.setDisplayHomeAsUpEnabled(true);"); 493 mWarnedForDisplayHomeAsUp = true; 494 } 495 mActivityImpl.setActionBarUpIndicator(upDrawable, contentDescRes); 496 } 497 498 void setActionBarDescription(int contentDescRes) { 499 mActivityImpl.setActionBarDescription(contentDescRes); 500 } 501 502 Drawable getThemeUpIndicator() { 503 return mActivityImpl.getThemeUpIndicator(); 504 } 505 506 private void setPosition(float position) { 507 if (position == 1f) { 508 mSlider.setVerticalMirror(true); 509 } else if (position == 0f) { 510 mSlider.setVerticalMirror(false); 511 } 512 mSlider.setProgress(position); 513 } 514 515 private static class FrameworkActionBarDelegate implements Delegate { 516 private final Activity mActivity; 517 private ActionBarDrawerToggleHoneycomb.SetIndicatorInfo mSetIndicatorInfo; 518 519 FrameworkActionBarDelegate(Activity activity) { 520 mActivity = activity; 521 } 522 523 @Override 524 public Drawable getThemeUpIndicator() { 525 if (Build.VERSION.SDK_INT >= 18) { 526 final TypedArray a = getActionBarThemedContext().obtainStyledAttributes(null, 527 new int[] {android.R.attr.homeAsUpIndicator}, 528 android.R.attr.actionBarStyle, 0); 529 final Drawable result = a.getDrawable(0); 530 a.recycle(); 531 return result; 532 } 533 return ActionBarDrawerToggleHoneycomb.getThemeUpIndicator(mActivity); 534 } 535 536 @Override 537 public Context getActionBarThemedContext() { 538 final ActionBar actionBar = mActivity.getActionBar(); 539 if (actionBar != null) { 540 return actionBar.getThemedContext(); 541 } 542 return mActivity; 543 } 544 545 @Override 546 public boolean isNavigationVisible() { 547 final ActionBar actionBar = mActivity.getActionBar(); 548 return actionBar != null 549 && (actionBar.getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) != 0; 550 } 551 552 @Override 553 public void setActionBarUpIndicator(Drawable themeImage, int contentDescRes) { 554 final ActionBar actionBar = mActivity.getActionBar(); 555 if (actionBar != null) { 556 if (Build.VERSION.SDK_INT >= 18) { 557 actionBar.setHomeAsUpIndicator(themeImage); 558 actionBar.setHomeActionContentDescription(contentDescRes); 559 } else { 560 actionBar.setDisplayShowHomeEnabled(true); 561 mSetIndicatorInfo = ActionBarDrawerToggleHoneycomb.setActionBarUpIndicator( 562 mSetIndicatorInfo, mActivity, themeImage, contentDescRes); 563 actionBar.setDisplayShowHomeEnabled(false); 564 } 565 } 566 } 567 568 @Override 569 public void setActionBarDescription(int contentDescRes) { 570 if (Build.VERSION.SDK_INT >= 18) { 571 final ActionBar actionBar = mActivity.getActionBar(); 572 if (actionBar != null) { 573 actionBar.setHomeActionContentDescription(contentDescRes); 574 } 575 } else { 576 mSetIndicatorInfo = ActionBarDrawerToggleHoneycomb.setActionBarDescription( 577 mSetIndicatorInfo, mActivity, contentDescRes); 578 } 579 } 580 } 581 582 /** 583 * Used when DrawerToggle is initialized with a Toolbar 584 */ 585 static class ToolbarCompatDelegate implements Delegate { 586 587 final Toolbar mToolbar; 588 final Drawable mDefaultUpIndicator; 589 final CharSequence mDefaultContentDescription; 590 591 ToolbarCompatDelegate(Toolbar toolbar) { 592 mToolbar = toolbar; 593 mDefaultUpIndicator = toolbar.getNavigationIcon(); 594 mDefaultContentDescription = toolbar.getNavigationContentDescription(); 595 } 596 597 @Override 598 public void setActionBarUpIndicator(Drawable upDrawable, @StringRes int contentDescRes) { 599 mToolbar.setNavigationIcon(upDrawable); 600 setActionBarDescription(contentDescRes); 601 } 602 603 @Override 604 public void setActionBarDescription(@StringRes int contentDescRes) { 605 if (contentDescRes == 0) { 606 mToolbar.setNavigationContentDescription(mDefaultContentDescription); 607 } else { 608 mToolbar.setNavigationContentDescription(contentDescRes); 609 } 610 } 611 612 @Override 613 public Drawable getThemeUpIndicator() { 614 return mDefaultUpIndicator; 615 } 616 617 @Override 618 public Context getActionBarThemedContext() { 619 return mToolbar.getContext(); 620 } 621 622 @Override 623 public boolean isNavigationVisible() { 624 return true; 625 } 626 } 627} 628