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