ActionBarDrawerToggle.java revision 9932c13e77bd1c9766d294904180c31729f3bf87
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.Activity; 21import android.content.res.Configuration; 22import android.graphics.Canvas; 23import android.graphics.Rect; 24import android.graphics.drawable.Drawable; 25import android.graphics.drawable.LevelListDrawable; 26import android.os.Build; 27import android.support.v4.graphics.drawable.DrawableCompat; 28import android.support.v4.view.GravityCompat; 29import android.support.v4.view.ViewCompat; 30import android.support.v4.widget.DrawerLayout; 31import android.view.MenuItem; 32import android.view.View; 33 34/** 35 * This class provides a handy way to tie together the functionality of 36 * {@link DrawerLayout} and the framework <code>ActionBar</code> to implement the recommended 37 * design for navigation drawers. 38 * 39 * <p>To use <code>ActionBarDrawerToggle</code>, create one in your Activity and call through 40 * to the following methods corresponding to your Activity callbacks:</p> 41 * 42 * <ul> 43 * <li>{@link Activity#onConfigurationChanged(android.content.res.Configuration) onConfigurationChanged}</li> 44 * <li>{@link Activity#onOptionsItemSelected(android.view.MenuItem) onOptionsItemSelected}</li> 45 * </ul> 46 * 47 * <p>Call {@link #syncState()} from your <code>Activity</code>'s 48 * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} to synchronize the indicator 49 * with the state of the linked DrawerLayout after <code>onRestoreInstanceState</code> 50 * has occurred.</p> 51 * 52 * <p><code>ActionBarDrawerToggle</code> can be used directly as a 53 * {@link DrawerLayout.DrawerListener}, or if you are already providing your own listener, 54 * call through to each of the listener methods from your own.</p> 55 */ 56public class ActionBarDrawerToggle implements DrawerLayout.DrawerListener { 57 58 /** 59 * Allows an implementing Activity to return an {@link ActionBarDrawerToggle.Delegate} to use 60 * with ActionBarDrawerToggle. 61 */ 62 public interface DelegateProvider { 63 64 /** 65 * @return Delegate to use for ActionBarDrawableToggles, or null if the Activity 66 * does not wish to override the default behavior. 67 */ 68 Delegate getDrawerToggleDelegate(); 69 } 70 71 public interface Delegate { 72 /** 73 * @return Up indicator drawable as defined in the Activity's theme, or null if one is not 74 * defined. 75 */ 76 Drawable getThemeUpIndicator(); 77 78 /** 79 * Set the Action Bar's up indicator drawable and content description. 80 * 81 * @param upDrawable - Drawable to set as up indicator 82 * @param contentDescRes - Content description to set 83 */ 84 void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes); 85 86 /** 87 * Set the Action Bar's up indicator content description. 88 * 89 * @param contentDescRes - Content description to set 90 */ 91 void setActionBarDescription(int contentDescRes); 92 } 93 94 private interface ActionBarDrawerToggleImpl { 95 Drawable getThemeUpIndicator(Activity activity); 96 Object setActionBarUpIndicator(Object info, Activity activity, 97 Drawable themeImage, int contentDescRes); 98 Object setActionBarDescription(Object info, Activity activity, int contentDescRes); 99 } 100 101 private static class ActionBarDrawerToggleImplBase implements ActionBarDrawerToggleImpl { 102 @Override 103 public Drawable getThemeUpIndicator(Activity activity) { 104 return null; 105 } 106 107 @Override 108 public Object setActionBarUpIndicator(Object info, Activity activity, 109 Drawable themeImage, int contentDescRes) { 110 // No action bar to set. 111 return info; 112 } 113 114 @Override 115 public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) { 116 // No action bar to set 117 return info; 118 } 119 } 120 121 private static class ActionBarDrawerToggleImplHC implements ActionBarDrawerToggleImpl { 122 @Override 123 public Drawable getThemeUpIndicator(Activity activity) { 124 return ActionBarDrawerToggleHoneycomb.getThemeUpIndicator(activity); 125 } 126 127 @Override 128 public Object setActionBarUpIndicator(Object info, Activity activity, 129 Drawable themeImage, int contentDescRes) { 130 return ActionBarDrawerToggleHoneycomb.setActionBarUpIndicator(info, activity, 131 themeImage, contentDescRes); 132 } 133 134 @Override 135 public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) { 136 return ActionBarDrawerToggleHoneycomb.setActionBarDescription(info, activity, 137 contentDescRes); 138 } 139 } 140 141 private static class ActionBarDrawerToggleImplJellybeanMR2 142 implements ActionBarDrawerToggleImpl { 143 @Override 144 public Drawable getThemeUpIndicator(Activity activity) { 145 return ActionBarDrawerToggleJellybeanMR2.getThemeUpIndicator(activity); 146 } 147 148 @Override 149 public Object setActionBarUpIndicator(Object info, Activity activity, 150 Drawable themeImage, int contentDescRes) { 151 return ActionBarDrawerToggleJellybeanMR2.setActionBarUpIndicator(info, activity, 152 themeImage, contentDescRes); 153 } 154 155 @Override 156 public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) { 157 return ActionBarDrawerToggleJellybeanMR2.setActionBarDescription(info, activity, 158 contentDescRes); 159 } 160 } 161 162 private static final ActionBarDrawerToggleImpl IMPL; 163 164 static { 165 final int version = Build.VERSION.SDK_INT; 166 if (version >= 18) { 167 IMPL = new ActionBarDrawerToggleImplJellybeanMR2(); 168 } else if (version >= 11) { 169 IMPL = new ActionBarDrawerToggleImplHC(); 170 } else { 171 IMPL = new ActionBarDrawerToggleImplBase(); 172 } 173 } 174 175 /** Fraction of its total width by which to offset the toggle drawable. */ 176 private static final float TOGGLE_DRAWABLE_OFFSET = 1 / 3f; 177 178 // android.R.id.home as defined by public API in v11 179 private static final int ID_HOME = 0x0102002c; 180 181 private final Activity mActivity; 182 private final Delegate mActivityImpl; 183 private final DrawerLayout mDrawerLayout; 184 private boolean mDrawerIndicatorEnabled = true; 185 186 private Drawable mThemeImage; 187 private Drawable mDrawerImage; 188 private SlideDrawable mSlider; 189 private final int mDrawerImageResource; 190 private final int mOpenDrawerContentDescRes; 191 private final int mCloseDrawerContentDescRes; 192 193 private Object mSetIndicatorInfo; 194 195 /** 196 * Construct a new ActionBarDrawerToggle. 197 * 198 * <p>The given {@link Activity} will be linked to the specified {@link DrawerLayout}. 199 * The provided drawer indicator drawable will animate slightly off-screen as the drawer 200 * is opened, indicating that in the open state the drawer will move off-screen when pressed 201 * and in the closed state the drawer will move on-screen when pressed.</p> 202 * 203 * <p>String resources must be provided to describe the open/close drawer actions for 204 * accessibility services.</p> 205 * 206 * @param activity The Activity hosting the drawer 207 * @param drawerLayout The DrawerLayout to link to the given Activity's ActionBar 208 * @param drawerImageRes A Drawable resource to use as the drawer indicator 209 * @param openDrawerContentDescRes A String resource to describe the "open drawer" action 210 * for accessibility 211 * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action 212 * for accessibility 213 */ 214 public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, 215 int drawerImageRes, int openDrawerContentDescRes, int closeDrawerContentDescRes) { 216 mActivity = activity; 217 218 // Allow the Activity to provide an impl 219 if (activity instanceof DelegateProvider) { 220 mActivityImpl = ((DelegateProvider) activity).getDrawerToggleDelegate(); 221 } else { 222 mActivityImpl = null; 223 } 224 225 mDrawerLayout = drawerLayout; 226 mDrawerImageResource = drawerImageRes; 227 mOpenDrawerContentDescRes = openDrawerContentDescRes; 228 mCloseDrawerContentDescRes = closeDrawerContentDescRes; 229 230 mThemeImage = getThemeUpIndicator(); 231 mDrawerImage = activity.getResources().getDrawable(drawerImageRes); 232 mSlider = new SlideDrawable(mDrawerImage); 233 mSlider.setOffset(TOGGLE_DRAWABLE_OFFSET); 234 } 235 236 /** 237 * Synchronize the state of the drawer indicator/affordance with the linked DrawerLayout. 238 * 239 * <p>This should be called from your <code>Activity</code>'s 240 * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} method to synchronize after 241 * the DrawerLayout's instance state has been restored, and any other time when the state 242 * may have diverged in such a way that the ActionBarDrawerToggle was not notified. 243 * (For example, if you stop forwarding appropriate drawer events for a period of time.)</p> 244 */ 245 public void syncState() { 246 if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) { 247 mSlider.setPosition(1); 248 } else { 249 mSlider.setPosition(0); 250 } 251 252 if (mDrawerIndicatorEnabled) { 253 setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ? 254 mCloseDrawerContentDescRes : mOpenDrawerContentDescRes); 255 } 256 } 257 258 /** 259 * Enable or disable the drawer indicator. The indicator defaults to enabled. 260 * 261 * <p>When the indicator is disabled, the <code>ActionBar</code> will revert to displaying 262 * the home-as-up indicator provided by the <code>Activity</code>'s theme in the 263 * <code>android.R.attr.homeAsUpIndicator</code> attribute instead of the animated 264 * drawer glyph.</p> 265 * 266 * @param enable true to enable, false to disable 267 */ 268 public void setDrawerIndicatorEnabled(boolean enable) { 269 if (enable != mDrawerIndicatorEnabled) { 270 if (enable) { 271 setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ? 272 mCloseDrawerContentDescRes : mOpenDrawerContentDescRes); 273 } else { 274 setActionBarUpIndicator(mThemeImage, 0); 275 } 276 mDrawerIndicatorEnabled = enable; 277 } 278 } 279 280 /** 281 * @return true if the enhanced drawer indicator is enabled, false otherwise 282 * @see #setDrawerIndicatorEnabled(boolean) 283 */ 284 public boolean isDrawerIndicatorEnabled() { 285 return mDrawerIndicatorEnabled; 286 } 287 288 /** 289 * This method should always be called by your <code>Activity</code>'s 290 * {@link Activity#onConfigurationChanged(android.content.res.Configuration) onConfigurationChanged} 291 * method. 292 * 293 * @param newConfig The new configuration 294 */ 295 public void onConfigurationChanged(Configuration newConfig) { 296 // Reload drawables that can change with configuration 297 mThemeImage = getThemeUpIndicator(); 298 mDrawerImage = mActivity.getResources().getDrawable(mDrawerImageResource); 299 syncState(); 300 } 301 302 /** 303 * This method should be called by your <code>Activity</code>'s 304 * {@link Activity#onOptionsItemSelected(android.view.MenuItem) onOptionsItemSelected} method. 305 * If it returns true, your <code>onOptionsItemSelected</code> method should return true and 306 * skip further processing. 307 * 308 * @param item the MenuItem instance representing the selected menu item 309 * @return true if the event was handled and further processing should not occur 310 */ 311 public boolean onOptionsItemSelected(MenuItem item) { 312 if (item != null && item.getItemId() == ID_HOME && mDrawerIndicatorEnabled) { 313 if (mDrawerLayout.isDrawerVisible(GravityCompat.START)) { 314 mDrawerLayout.closeDrawer(GravityCompat.START); 315 } else { 316 mDrawerLayout.openDrawer(GravityCompat.START); 317 } 318 return true; 319 } 320 return false; 321 } 322 323 /** 324 * {@link DrawerLayout.DrawerListener} callback method. If you do not use your 325 * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call 326 * through to this method from your own listener object. 327 * 328 * @param drawerView The child view that was moved 329 * @param slideOffset The new offset of this drawer within its range, from 0-1 330 */ 331 @Override 332 public void onDrawerSlide(View drawerView, float slideOffset) { 333 float glyphOffset = mSlider.getPosition(); 334 if (slideOffset > 0.5f) { 335 glyphOffset = Math.max(glyphOffset, Math.max(0.f, slideOffset - 0.5f) * 2); 336 } else { 337 glyphOffset = Math.min(glyphOffset, slideOffset * 2); 338 } 339 mSlider.setPosition(glyphOffset); 340 } 341 342 /** 343 * {@link DrawerLayout.DrawerListener} callback method. If you do not use your 344 * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call 345 * through to this method from your own listener object. 346 * 347 * @param drawerView Drawer view that is now open 348 */ 349 @Override 350 public void onDrawerOpened(View drawerView) { 351 mSlider.setPosition(1); 352 if (mDrawerIndicatorEnabled) { 353 setActionBarDescription(mCloseDrawerContentDescRes); 354 } 355 } 356 357 /** 358 * {@link DrawerLayout.DrawerListener} callback method. If you do not use your 359 * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call 360 * through to this method from your own listener object. 361 * 362 * @param drawerView Drawer view that is now closed 363 */ 364 @Override 365 public void onDrawerClosed(View drawerView) { 366 mSlider.setPosition(0); 367 if (mDrawerIndicatorEnabled) { 368 setActionBarDescription(mOpenDrawerContentDescRes); 369 } 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 newState The new drawer motion state 378 */ 379 @Override 380 public void onDrawerStateChanged(int newState) { 381 } 382 383 Drawable getThemeUpIndicator() { 384 if (mActivityImpl != null) { 385 return mActivityImpl.getThemeUpIndicator(); 386 } 387 return IMPL.getThemeUpIndicator(mActivity); 388 } 389 390 void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) { 391 if (mActivityImpl != null) { 392 mActivityImpl.setActionBarUpIndicator(upDrawable, contentDescRes); 393 return; 394 } 395 mSetIndicatorInfo = IMPL 396 .setActionBarUpIndicator(mSetIndicatorInfo, mActivity, upDrawable, contentDescRes); 397 } 398 399 void setActionBarDescription(int contentDescRes) { 400 if (mActivityImpl != null) { 401 mActivityImpl.setActionBarDescription(contentDescRes); 402 return; 403 } 404 mSetIndicatorInfo = IMPL 405 .setActionBarDescription(mSetIndicatorInfo, mActivity, contentDescRes); 406 } 407 408 private class SlideDrawable extends LevelListDrawable implements Drawable.Callback { 409 private final boolean mHasMirroring = Build.VERSION.SDK_INT > 18; 410 private final Rect mTmpRect = new Rect(); 411 412 private float mPosition; 413 private float mOffset; 414 415 private SlideDrawable(Drawable wrapped) { 416 super(); 417 418 if (DrawableCompat.isAutoMirrored(wrapped)) { 419 DrawableCompat.setAutoMirrored(this, true); 420 } 421 422 addLevel(0, 0, wrapped); 423 } 424 425 /** 426 * Sets the current position along the offset. 427 * 428 * @param position a value between 0 and 1 429 */ 430 public void setPosition(float position) { 431 mPosition = position; 432 invalidateSelf(); 433 } 434 435 public float getPosition() { 436 return mPosition; 437 } 438 439 /** 440 * Specifies the maximum offset when the position is at 1. 441 * 442 * @param offset maximum offset as a fraction of the drawable width, 443 * positive to shift left or negative to shift right. 444 * @see #setPosition(float) 445 */ 446 public void setOffset(float offset) { 447 mOffset = offset; 448 invalidateSelf(); 449 } 450 451 @Override 452 public void draw(Canvas canvas) { 453 copyBounds(mTmpRect); 454 canvas.save(); 455 456 // Layout direction must be obtained from the activity. 457 final boolean isLayoutRTL = ViewCompat.getLayoutDirection( 458 mActivity.getWindow().getDecorView()) == ViewCompat.LAYOUT_DIRECTION_RTL; 459 final int flipRtl = isLayoutRTL ? -1 : 1; 460 final int width = mTmpRect.width(); 461 canvas.translate(-mOffset * width * mPosition * flipRtl, 0); 462 463 // Force auto-mirroring if it's not supported by the platform. 464 if (isLayoutRTL && !mHasMirroring) { 465 canvas.translate(width, 0); 466 canvas.scale(-1, 1); 467 } 468 469 super.draw(canvas); 470 canvas.restore(); 471 } 472 } 473} 474