1/* 2 * Copyright (C) 2013 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 17 18package android.support.v4.app; 19 20import android.app.ActionBar; 21import android.app.Activity; 22import android.content.Context; 23import android.content.res.Configuration; 24import android.content.res.TypedArray; 25import android.graphics.Canvas; 26import android.graphics.Rect; 27import android.graphics.drawable.Drawable; 28import android.graphics.drawable.InsetDrawable; 29import android.os.Build; 30import android.support.annotation.DrawableRes; 31import android.support.annotation.NonNull; 32import android.support.annotation.Nullable; 33import android.support.annotation.StringRes; 34import android.support.v4.content.ContextCompat; 35import android.support.v4.view.GravityCompat; 36import android.support.v4.view.ViewCompat; 37import android.support.v4.widget.DrawerLayout; 38import android.util.Log; 39import android.view.MenuItem; 40import android.view.View; 41import android.view.ViewGroup; 42import android.widget.ImageView; 43 44import java.lang.reflect.Method; 45 46/** 47 * This class provides a handy way to tie together the functionality of 48 * {@link DrawerLayout} and the framework <code>ActionBar</code> to implement the recommended 49 * design for navigation drawers. 50 * 51 * <p>To use <code>ActionBarDrawerToggle</code>, create one in your Activity and call through 52 * to the following methods corresponding to your Activity callbacks:</p> 53 * 54 * <ul> 55 * <li>{@link Activity#onConfigurationChanged(android.content.res.Configuration) onConfigurationChanged}</li> 56 * <li>{@link Activity#onOptionsItemSelected(android.view.MenuItem) onOptionsItemSelected}</li> 57 * </ul> 58 * 59 * <p>Call {@link #syncState()} from your <code>Activity</code>'s 60 * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} to synchronize the indicator 61 * with the state of the linked DrawerLayout after <code>onRestoreInstanceState</code> 62 * has occurred.</p> 63 * 64 * <p><code>ActionBarDrawerToggle</code> can be used directly as a 65 * {@link DrawerLayout.DrawerListener}, or if you are already providing your own listener, 66 * call through to each of the listener methods from your own.</p> 67 * 68 */ 69@Deprecated 70public class ActionBarDrawerToggle implements DrawerLayout.DrawerListener { 71 72 /** 73 * Allows an implementing Activity to return an {@link ActionBarDrawerToggle.Delegate} to use 74 * with ActionBarDrawerToggle. 75 * 76 * @deprecated Use ActionBarDrawerToggle.DelegateProvider in support-v7-appcompat. 77 */ 78 @Deprecated 79 public interface DelegateProvider { 80 81 /** 82 * @return Delegate to use for ActionBarDrawableToggles, or null if the Activity 83 * does not wish to override the default behavior. 84 */ 85 @Nullable 86 Delegate getDrawerToggleDelegate(); 87 } 88 89 /** 90 * @deprecated Use ActionBarDrawerToggle.DelegateProvider in support-v7-appcompat. 91 */ 92 @Deprecated 93 public interface Delegate { 94 /** 95 * @return Up indicator drawable as defined in the Activity's theme, or null if one is not 96 * defined. 97 */ 98 @Nullable 99 Drawable getThemeUpIndicator(); 100 101 /** 102 * Set the Action Bar's up indicator drawable and content description. 103 * 104 * @param upDrawable - Drawable to set as up indicator 105 * @param contentDescRes - Content description to set 106 */ 107 void setActionBarUpIndicator(Drawable upDrawable, @StringRes int contentDescRes); 108 109 /** 110 * Set the Action Bar's up indicator content description. 111 * 112 * @param contentDescRes - Content description to set 113 */ 114 void setActionBarDescription(@StringRes int contentDescRes); 115 } 116 117 private static final String TAG = "ActionBarDrawerToggle"; 118 119 private static final int[] THEME_ATTRS = new int[] { 120 android.R.attr.homeAsUpIndicator 121 }; 122 123 /** Fraction of its total width by which to offset the toggle drawable. */ 124 private static final float TOGGLE_DRAWABLE_OFFSET = 1 / 3f; 125 126 // android.R.id.home as defined by public API in v11 127 private static final int ID_HOME = 0x0102002c; 128 129 final Activity mActivity; 130 private final Delegate mActivityImpl; 131 private final DrawerLayout mDrawerLayout; 132 private boolean mDrawerIndicatorEnabled = true; 133 private boolean mHasCustomUpIndicator; 134 135 private Drawable mHomeAsUpIndicator; 136 private Drawable mDrawerImage; 137 private SlideDrawable mSlider; 138 private final int mDrawerImageResource; 139 private final int mOpenDrawerContentDescRes; 140 private final int mCloseDrawerContentDescRes; 141 142 private SetIndicatorInfo mSetIndicatorInfo; 143 144 /** 145 * Construct a new ActionBarDrawerToggle. 146 * 147 * <p>The given {@link Activity} will be linked to the specified {@link DrawerLayout}. 148 * The provided drawer indicator drawable will animate slightly off-screen as the drawer 149 * is opened, indicating that in the open state the drawer will move off-screen when pressed 150 * and in the closed state the drawer will move on-screen when pressed.</p> 151 * 152 * <p>String resources must be provided to describe the open/close drawer actions for 153 * accessibility services.</p> 154 * 155 * @param activity The Activity hosting the drawer 156 * @param drawerLayout The DrawerLayout to link to the given Activity's ActionBar 157 * @param drawerImageRes A Drawable resource to use as the drawer indicator 158 * @param openDrawerContentDescRes A String resource to describe the "open drawer" action 159 * for accessibility 160 * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action 161 * for accessibility 162 */ 163 public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, 164 @DrawableRes int drawerImageRes, @StringRes int openDrawerContentDescRes, 165 @StringRes int closeDrawerContentDescRes) { 166 this(activity, drawerLayout, !assumeMaterial(activity), drawerImageRes, 167 openDrawerContentDescRes, closeDrawerContentDescRes); 168 } 169 170 private static boolean assumeMaterial(Context context) { 171 return context.getApplicationInfo().targetSdkVersion >= 21 172 && (Build.VERSION.SDK_INT >= 21); 173 } 174 175 /** 176 * Construct a new ActionBarDrawerToggle. 177 * 178 * <p>The given {@link Activity} will be linked to the specified {@link DrawerLayout}. 179 * The provided drawer indicator drawable will animate slightly off-screen as the drawer 180 * is opened, indicating that in the open state the drawer will move off-screen when pressed 181 * and in the closed state the drawer will move on-screen when pressed.</p> 182 * 183 * <p>String resources must be provided to describe the open/close drawer actions for 184 * accessibility services.</p> 185 * 186 * @param activity The Activity hosting the drawer 187 * @param drawerLayout The DrawerLayout to link to the given Activity's ActionBar 188 * @param animate True to animate the drawer indicator along with the drawer's position. 189 * Material apps should set this to false. 190 * @param drawerImageRes A Drawable resource to use as the drawer indicator 191 * @param openDrawerContentDescRes A String resource to describe the "open drawer" action 192 * for accessibility 193 * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action 194 * for accessibility 195 */ 196 public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, boolean animate, 197 @DrawableRes int drawerImageRes, @StringRes int openDrawerContentDescRes, 198 @StringRes int closeDrawerContentDescRes) { 199 mActivity = activity; 200 201 // Allow the Activity to provide an impl 202 if (activity instanceof DelegateProvider) { 203 mActivityImpl = ((DelegateProvider) activity).getDrawerToggleDelegate(); 204 } else { 205 mActivityImpl = null; 206 } 207 208 mDrawerLayout = drawerLayout; 209 mDrawerImageResource = drawerImageRes; 210 mOpenDrawerContentDescRes = openDrawerContentDescRes; 211 mCloseDrawerContentDescRes = closeDrawerContentDescRes; 212 213 mHomeAsUpIndicator = getThemeUpIndicator(); 214 mDrawerImage = ContextCompat.getDrawable(activity, drawerImageRes); 215 mSlider = new SlideDrawable(mDrawerImage); 216 mSlider.setOffset(animate ? TOGGLE_DRAWABLE_OFFSET : 0); 217 } 218 219 /** 220 * Synchronize the state of the drawer indicator/affordance with the linked DrawerLayout. 221 * 222 * <p>This should be called from your <code>Activity</code>'s 223 * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} method to synchronize after 224 * the DrawerLayout's instance state has been restored, and any other time when the state 225 * may have diverged in such a way that the ActionBarDrawerToggle was not notified. 226 * (For example, if you stop forwarding appropriate drawer events for a period of time.)</p> 227 */ 228 public void syncState() { 229 if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) { 230 mSlider.setPosition(1); 231 } else { 232 mSlider.setPosition(0); 233 } 234 235 if (mDrawerIndicatorEnabled) { 236 setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) 237 ? mCloseDrawerContentDescRes : mOpenDrawerContentDescRes); 238 } 239 } 240 241 /** 242 * Set the up indicator to display when the drawer indicator is not 243 * enabled. 244 * <p> 245 * If you pass <code>null</code> to this method, the default drawable from 246 * the theme will be used. 247 * 248 * @param indicator A drawable to use for the up indicator, or null to use 249 * the theme's default 250 * @see #setDrawerIndicatorEnabled(boolean) 251 */ 252 public void setHomeAsUpIndicator(Drawable indicator) { 253 if (indicator == null) { 254 mHomeAsUpIndicator = getThemeUpIndicator(); 255 mHasCustomUpIndicator = false; 256 } else { 257 mHomeAsUpIndicator = indicator; 258 mHasCustomUpIndicator = true; 259 } 260 261 if (!mDrawerIndicatorEnabled) { 262 setActionBarUpIndicator(mHomeAsUpIndicator, 0); 263 } 264 } 265 266 /** 267 * Set the up indicator to display when the drawer indicator is not 268 * enabled. 269 * <p> 270 * If you pass 0 to this method, the default drawable from the theme will 271 * be used. 272 * 273 * @param resId Resource ID of a drawable to use for the up indicator, or 0 274 * to use the theme's default 275 * @see #setDrawerIndicatorEnabled(boolean) 276 */ 277 public void setHomeAsUpIndicator(int resId) { 278 Drawable indicator = null; 279 if (resId != 0) { 280 indicator = ContextCompat.getDrawable(mActivity, resId); 281 } 282 283 setHomeAsUpIndicator(indicator); 284 } 285 286 /** 287 * Enable or disable the drawer indicator. The indicator defaults to enabled. 288 * 289 * <p>When the indicator is disabled, the <code>ActionBar</code> will revert to displaying 290 * the home-as-up indicator provided by the <code>Activity</code>'s theme in the 291 * <code>android.R.attr.homeAsUpIndicator</code> attribute instead of the animated 292 * drawer glyph.</p> 293 * 294 * @param enable true to enable, false to disable 295 */ 296 public void setDrawerIndicatorEnabled(boolean enable) { 297 if (enable != mDrawerIndicatorEnabled) { 298 if (enable) { 299 setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) 300 ? mCloseDrawerContentDescRes : mOpenDrawerContentDescRes); 301 } else { 302 setActionBarUpIndicator(mHomeAsUpIndicator, 0); 303 } 304 mDrawerIndicatorEnabled = enable; 305 } 306 } 307 308 /** 309 * @return true if the enhanced drawer indicator is enabled, false otherwise 310 * @see #setDrawerIndicatorEnabled(boolean) 311 */ 312 public boolean isDrawerIndicatorEnabled() { 313 return mDrawerIndicatorEnabled; 314 } 315 316 /** 317 * This method should always be called by your <code>Activity</code>'s 318 * {@link Activity#onConfigurationChanged(android.content.res.Configuration) onConfigurationChanged} 319 * method. 320 * 321 * @param newConfig The new configuration 322 */ 323 public void onConfigurationChanged(Configuration newConfig) { 324 // Reload drawables that can change with configuration 325 if (!mHasCustomUpIndicator) { 326 mHomeAsUpIndicator = getThemeUpIndicator(); 327 } 328 mDrawerImage = ContextCompat.getDrawable(mActivity, mDrawerImageResource); 329 syncState(); 330 } 331 332 /** 333 * This method should be called by your <code>Activity</code>'s 334 * {@link Activity#onOptionsItemSelected(android.view.MenuItem) onOptionsItemSelected} method. 335 * If it returns true, your <code>onOptionsItemSelected</code> method should return true and 336 * skip further processing. 337 * 338 * @param item the MenuItem instance representing the selected menu item 339 * @return true if the event was handled and further processing should not occur 340 */ 341 public boolean onOptionsItemSelected(MenuItem item) { 342 if (item != null && item.getItemId() == ID_HOME && mDrawerIndicatorEnabled) { 343 if (mDrawerLayout.isDrawerVisible(GravityCompat.START)) { 344 mDrawerLayout.closeDrawer(GravityCompat.START); 345 } else { 346 mDrawerLayout.openDrawer(GravityCompat.START); 347 } 348 return true; 349 } 350 return false; 351 } 352 353 /** 354 * {@link DrawerLayout.DrawerListener} callback method. If you do not use your 355 * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call 356 * through to this method from your own listener object. 357 * 358 * @param drawerView The child view that was moved 359 * @param slideOffset The new offset of this drawer within its range, from 0-1 360 */ 361 @Override 362 public void onDrawerSlide(View drawerView, float slideOffset) { 363 float glyphOffset = mSlider.getPosition(); 364 if (slideOffset > 0.5f) { 365 glyphOffset = Math.max(glyphOffset, Math.max(0.f, slideOffset - 0.5f) * 2); 366 } else { 367 glyphOffset = Math.min(glyphOffset, slideOffset * 2); 368 } 369 mSlider.setPosition(glyphOffset); 370 } 371 372 /** 373 * {@link DrawerLayout.DrawerListener} callback method. If you do not use your 374 * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call 375 * through to this method from your own listener object. 376 * 377 * @param drawerView Drawer view that is now open 378 */ 379 @Override 380 public void onDrawerOpened(View drawerView) { 381 mSlider.setPosition(1); 382 if (mDrawerIndicatorEnabled) { 383 setActionBarDescription(mCloseDrawerContentDescRes); 384 } 385 } 386 387 /** 388 * {@link DrawerLayout.DrawerListener} callback method. If you do not use your 389 * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call 390 * through to this method from your own listener object. 391 * 392 * @param drawerView Drawer view that is now closed 393 */ 394 @Override 395 public void onDrawerClosed(View drawerView) { 396 mSlider.setPosition(0); 397 if (mDrawerIndicatorEnabled) { 398 setActionBarDescription(mOpenDrawerContentDescRes); 399 } 400 } 401 402 /** 403 * {@link DrawerLayout.DrawerListener} callback method. If you do not use your 404 * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call 405 * through to this method from your own listener object. 406 * 407 * @param newState The new drawer motion state 408 */ 409 @Override 410 public void onDrawerStateChanged(int newState) { 411 } 412 413 private Drawable getThemeUpIndicator() { 414 if (mActivityImpl != null) { 415 return mActivityImpl.getThemeUpIndicator(); 416 } 417 if (Build.VERSION.SDK_INT >= 18) { 418 final ActionBar actionBar = mActivity.getActionBar(); 419 final Context context; 420 if (actionBar != null) { 421 context = actionBar.getThemedContext(); 422 } else { 423 context = mActivity; 424 } 425 426 final TypedArray a = context.obtainStyledAttributes(null, THEME_ATTRS, 427 android.R.attr.actionBarStyle, 0); 428 final Drawable result = a.getDrawable(0); 429 a.recycle(); 430 return result; 431 } else { 432 final TypedArray a = mActivity.obtainStyledAttributes(THEME_ATTRS); 433 final Drawable result = a.getDrawable(0); 434 a.recycle(); 435 return result; 436 } 437 } 438 439 private void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) { 440 if (mActivityImpl != null) { 441 mActivityImpl.setActionBarUpIndicator(upDrawable, contentDescRes); 442 return; 443 } 444 if (Build.VERSION.SDK_INT >= 18) { 445 final ActionBar actionBar = mActivity.getActionBar(); 446 if (actionBar != null) { 447 actionBar.setHomeAsUpIndicator(upDrawable); 448 actionBar.setHomeActionContentDescription(contentDescRes); 449 } 450 } else { 451 if (mSetIndicatorInfo == null) { 452 mSetIndicatorInfo = new SetIndicatorInfo(mActivity); 453 } 454 if (mSetIndicatorInfo.mSetHomeAsUpIndicator != null) { 455 try { 456 final ActionBar actionBar = mActivity.getActionBar(); 457 mSetIndicatorInfo.mSetHomeAsUpIndicator.invoke(actionBar, upDrawable); 458 mSetIndicatorInfo.mSetHomeActionContentDescription.invoke( 459 actionBar, contentDescRes); 460 } catch (Exception e) { 461 Log.w(TAG, "Couldn't set home-as-up indicator via JB-MR2 API", e); 462 } 463 } else if (mSetIndicatorInfo.mUpIndicatorView != null) { 464 mSetIndicatorInfo.mUpIndicatorView.setImageDrawable(upDrawable); 465 } else { 466 Log.w(TAG, "Couldn't set home-as-up indicator"); 467 } 468 } 469 } 470 471 private void setActionBarDescription(int contentDescRes) { 472 if (mActivityImpl != null) { 473 mActivityImpl.setActionBarDescription(contentDescRes); 474 return; 475 } 476 if (Build.VERSION.SDK_INT >= 18) { 477 final ActionBar actionBar = mActivity.getActionBar(); 478 if (actionBar != null) { 479 actionBar.setHomeActionContentDescription(contentDescRes); 480 } 481 } else { 482 if (mSetIndicatorInfo == null) { 483 mSetIndicatorInfo = new SetIndicatorInfo(mActivity); 484 } 485 if (mSetIndicatorInfo.mSetHomeAsUpIndicator != null) { 486 try { 487 final ActionBar actionBar = mActivity.getActionBar(); 488 mSetIndicatorInfo.mSetHomeActionContentDescription.invoke( 489 actionBar, contentDescRes); 490 // For API 19 and earlier, we need to manually force the 491 // action bar to generate a new content description. 492 actionBar.setSubtitle(actionBar.getSubtitle()); 493 } catch (Exception e) { 494 Log.w(TAG, "Couldn't set content description via JB-MR2 API", e); 495 } 496 } 497 } 498 } 499 500 private static class SetIndicatorInfo { 501 Method mSetHomeAsUpIndicator; 502 Method mSetHomeActionContentDescription; 503 ImageView mUpIndicatorView; 504 505 SetIndicatorInfo(Activity activity) { 506 try { 507 mSetHomeAsUpIndicator = ActionBar.class.getDeclaredMethod("setHomeAsUpIndicator", 508 Drawable.class); 509 mSetHomeActionContentDescription = ActionBar.class.getDeclaredMethod( 510 "setHomeActionContentDescription", Integer.TYPE); 511 512 // If we got the method we won't need the stuff below. 513 return; 514 } catch (NoSuchMethodException e) { 515 // Oh well. We'll use the other mechanism below instead. 516 } 517 518 final View home = activity.findViewById(android.R.id.home); 519 if (home == null) { 520 // Action bar doesn't have a known configuration, an OEM messed with things. 521 return; 522 } 523 524 final ViewGroup parent = (ViewGroup) home.getParent(); 525 final int childCount = parent.getChildCount(); 526 if (childCount != 2) { 527 // No idea which one will be the right one, an OEM messed with things. 528 return; 529 } 530 531 final View first = parent.getChildAt(0); 532 final View second = parent.getChildAt(1); 533 final View up = first.getId() == android.R.id.home ? second : first; 534 535 if (up instanceof ImageView) { 536 // Jackpot! (Probably...) 537 mUpIndicatorView = (ImageView) up; 538 } 539 } 540 } 541 542 private class SlideDrawable extends InsetDrawable implements Drawable.Callback { 543 private final boolean mHasMirroring = Build.VERSION.SDK_INT > 18; 544 private final Rect mTmpRect = new Rect(); 545 546 private float mPosition; 547 private float mOffset; 548 549 SlideDrawable(Drawable wrapped) { 550 super(wrapped, 0); 551 } 552 553 /** 554 * Sets the current position along the offset. 555 * 556 * @param position a value between 0 and 1 557 */ 558 public void setPosition(float position) { 559 mPosition = position; 560 invalidateSelf(); 561 } 562 563 public float getPosition() { 564 return mPosition; 565 } 566 567 /** 568 * Specifies the maximum offset when the position is at 1. 569 * 570 * @param offset maximum offset as a fraction of the drawable width, 571 * positive to shift left or negative to shift right. 572 * @see #setPosition(float) 573 */ 574 public void setOffset(float offset) { 575 mOffset = offset; 576 invalidateSelf(); 577 } 578 579 @Override 580 public void draw(@NonNull Canvas canvas) { 581 copyBounds(mTmpRect); 582 canvas.save(); 583 584 // Layout direction must be obtained from the activity. 585 final boolean isLayoutRTL = ViewCompat.getLayoutDirection( 586 mActivity.getWindow().getDecorView()) == ViewCompat.LAYOUT_DIRECTION_RTL; 587 final int flipRtl = isLayoutRTL ? -1 : 1; 588 final int width = mTmpRect.width(); 589 canvas.translate(-mOffset * width * mPosition * flipRtl, 0); 590 591 // Force auto-mirroring if it's not supported by the platform. 592 if (isLayoutRTL && !mHasMirroring) { 593 canvas.translate(width, 0); 594 canvas.scale(-1, 1); 595 } 596 597 super.draw(canvas); 598 canvas.restore(); 599 } 600 } 601} 602