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