ActionBarDrawerToggle.java revision eb1dc82afa7464222ceeea95f16407ff873e59ff
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 final ActionBarDrawerToggleImpl IMPL; 142 143 static { 144 final int version = Build.VERSION.SDK_INT; 145 if (version >= 11) { 146 IMPL = new ActionBarDrawerToggleImplHC(); 147 } else { 148 IMPL = new ActionBarDrawerToggleImplBase(); 149 } 150 } 151 152 /** Fraction of its total width by which to offset the toggle drawable. */ 153 private static final float TOGGLE_DRAWABLE_OFFSET = 1 / 3f; 154 155 // android.R.id.home as defined by public API in v11 156 private static final int ID_HOME = 0x0102002c; 157 158 private final Activity mActivity; 159 private final Delegate mActivityImpl; 160 private final DrawerLayout mDrawerLayout; 161 private boolean mDrawerIndicatorEnabled = true; 162 163 private Drawable mThemeImage; 164 private Drawable mDrawerImage; 165 private SlideDrawable mSlider; 166 private final int mDrawerImageResource; 167 private final int mOpenDrawerContentDescRes; 168 private final int mCloseDrawerContentDescRes; 169 170 private Object mSetIndicatorInfo; 171 172 /** 173 * Construct a new ActionBarDrawerToggle. 174 * 175 * <p>The given {@link Activity} will be linked to the specified {@link DrawerLayout}. 176 * The provided drawer indicator drawable will animate slightly off-screen as the drawer 177 * is opened, indicating that in the open state the drawer will move off-screen when pressed 178 * and in the closed state the drawer will move on-screen when pressed.</p> 179 * 180 * <p>String resources must be provided to describe the open/close drawer actions for 181 * accessibility services.</p> 182 * 183 * @param activity The Activity hosting the drawer 184 * @param drawerLayout The DrawerLayout to link to the given Activity's ActionBar 185 * @param drawerImageRes A Drawable resource to use as the drawer indicator 186 * @param openDrawerContentDescRes A String resource to describe the "open drawer" action 187 * for accessibility 188 * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action 189 * for accessibility 190 */ 191 public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, 192 int drawerImageRes, int openDrawerContentDescRes, int closeDrawerContentDescRes) { 193 mActivity = activity; 194 195 // Allow the Activity to provide an impl 196 if (activity instanceof DelegateProvider) { 197 mActivityImpl = ((DelegateProvider) activity).getDrawerToggleDelegate(); 198 } else { 199 mActivityImpl = null; 200 } 201 202 mDrawerLayout = drawerLayout; 203 mDrawerImageResource = drawerImageRes; 204 mOpenDrawerContentDescRes = openDrawerContentDescRes; 205 mCloseDrawerContentDescRes = closeDrawerContentDescRes; 206 207 mThemeImage = getThemeUpIndicator(); 208 mDrawerImage = activity.getResources().getDrawable(drawerImageRes); 209 mSlider = new SlideDrawable(mDrawerImage); 210 mSlider.setOffset(TOGGLE_DRAWABLE_OFFSET); 211 } 212 213 /** 214 * Synchronize the state of the drawer indicator/affordance with the linked DrawerLayout. 215 * 216 * <p>This should be called from your <code>Activity</code>'s 217 * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} method to synchronize after 218 * the DrawerLayout's instance state has been restored, and any other time when the state 219 * may have diverged in such a way that the ActionBarDrawerToggle was not notified. 220 * (For example, if you stop forwarding appropriate drawer events for a period of time.)</p> 221 */ 222 public void syncState() { 223 if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) { 224 mSlider.setPosition(1); 225 } else { 226 mSlider.setPosition(0); 227 } 228 229 if (mDrawerIndicatorEnabled) { 230 setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ? 231 mOpenDrawerContentDescRes : mCloseDrawerContentDescRes); 232 } 233 } 234 235 /** 236 * Enable or disable the drawer indicator. The indicator defaults to enabled. 237 * 238 * <p>When the indicator is disabled, the <code>ActionBar</code> will revert to displaying 239 * the home-as-up indicator provided by the <code>Activity</code>'s theme in the 240 * <code>android.R.attr.homeAsUpIndicator</code> attribute instead of the animated 241 * drawer glyph.</p> 242 * 243 * @param enable true to enable, false to disable 244 */ 245 public void setDrawerIndicatorEnabled(boolean enable) { 246 if (enable != mDrawerIndicatorEnabled) { 247 if (enable) { 248 setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ? 249 mOpenDrawerContentDescRes : mCloseDrawerContentDescRes); 250 } else { 251 setActionBarUpIndicator(mThemeImage, 0); 252 } 253 mDrawerIndicatorEnabled = enable; 254 } 255 } 256 257 /** 258 * @return true if the enhanced drawer indicator is enabled, false otherwise 259 * @see #setDrawerIndicatorEnabled(boolean) 260 */ 261 public boolean isDrawerIndicatorEnabled() { 262 return mDrawerIndicatorEnabled; 263 } 264 265 /** 266 * This method should always be called by your <code>Activity</code>'s 267 * {@link Activity#onConfigurationChanged(android.content.res.Configuration) onConfigurationChanged} 268 * method. 269 * 270 * @param newConfig The new configuration 271 */ 272 public void onConfigurationChanged(Configuration newConfig) { 273 // Reload drawables that can change with configuration 274 mThemeImage = getThemeUpIndicator(); 275 mDrawerImage = mActivity.getResources().getDrawable(mDrawerImageResource); 276 syncState(); 277 } 278 279 /** 280 * This method should be called by your <code>Activity</code>'s 281 * {@link Activity#onOptionsItemSelected(android.view.MenuItem) onOptionsItemSelected} method. 282 * If it returns true, your <code>onOptionsItemSelected</code> method should return true and 283 * skip further processing. 284 * 285 * @param item the MenuItem instance representing the selected menu item 286 * @return true if the event was handled and further processing should not occur 287 */ 288 public boolean onOptionsItemSelected(MenuItem item) { 289 if (item != null && item.getItemId() == ID_HOME && mDrawerIndicatorEnabled) { 290 if (mDrawerLayout.isDrawerVisible(GravityCompat.START)) { 291 mDrawerLayout.closeDrawer(GravityCompat.START); 292 } else { 293 mDrawerLayout.openDrawer(GravityCompat.START); 294 } 295 return true; 296 } 297 return false; 298 } 299 300 /** 301 * {@link DrawerLayout.DrawerListener} callback method. If you do not use your 302 * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call 303 * through to this method from your own listener object. 304 * 305 * @param drawerView The child view that was moved 306 * @param slideOffset The new offset of this drawer within its range, from 0-1 307 */ 308 @Override 309 public void onDrawerSlide(View drawerView, float slideOffset) { 310 float glyphOffset = mSlider.getPosition(); 311 if (slideOffset > 0.5f) { 312 glyphOffset = Math.max(glyphOffset, Math.max(0.f, slideOffset - 0.5f) * 2); 313 } else { 314 glyphOffset = Math.min(glyphOffset, slideOffset * 2); 315 } 316 mSlider.setPosition(glyphOffset); 317 } 318 319 /** 320 * {@link DrawerLayout.DrawerListener} callback method. If you do not use your 321 * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call 322 * through to this method from your own listener object. 323 * 324 * @param drawerView Drawer view that is now open 325 */ 326 @Override 327 public void onDrawerOpened(View drawerView) { 328 mSlider.setPosition(1); 329 if (mDrawerIndicatorEnabled) { 330 setActionBarDescription(mOpenDrawerContentDescRes); 331 } 332 } 333 334 /** 335 * {@link DrawerLayout.DrawerListener} callback method. If you do not use your 336 * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call 337 * through to this method from your own listener object. 338 * 339 * @param drawerView Drawer view that is now closed 340 */ 341 @Override 342 public void onDrawerClosed(View drawerView) { 343 mSlider.setPosition(0); 344 if (mDrawerIndicatorEnabled) { 345 setActionBarDescription(mCloseDrawerContentDescRes); 346 } 347 } 348 349 /** 350 * {@link DrawerLayout.DrawerListener} callback method. If you do not use your 351 * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call 352 * through to this method from your own listener object. 353 * 354 * @param newState The new drawer motion state 355 */ 356 @Override 357 public void onDrawerStateChanged(int newState) { 358 } 359 360 Drawable getThemeUpIndicator() { 361 if (mActivityImpl != null) { 362 return mActivityImpl.getThemeUpIndicator(); 363 } 364 return IMPL.getThemeUpIndicator(mActivity); 365 } 366 367 void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) { 368 if (mActivityImpl != null) { 369 mActivityImpl.setActionBarUpIndicator(upDrawable, contentDescRes); 370 return; 371 } 372 mSetIndicatorInfo = IMPL 373 .setActionBarUpIndicator(mSetIndicatorInfo, mActivity, upDrawable, contentDescRes); 374 } 375 376 void setActionBarDescription(int contentDescRes) { 377 if (mActivityImpl != null) { 378 mActivityImpl.setActionBarDescription(contentDescRes); 379 return; 380 } 381 mSetIndicatorInfo = IMPL 382 .setActionBarDescription(mSetIndicatorInfo, mActivity, contentDescRes); 383 } 384 385 private class SlideDrawable extends LevelListDrawable implements Drawable.Callback { 386 private final boolean mHasMirroring = Build.VERSION.SDK_INT > 18; 387 private final Rect mTmpRect = new Rect(); 388 389 private float mPosition; 390 private float mOffset; 391 392 private SlideDrawable(Drawable wrapped) { 393 super(); 394 395 if (DrawableCompat.isAutoMirrored(wrapped)) { 396 DrawableCompat.setAutoMirrored(this, true); 397 } 398 399 addLevel(0, 0, wrapped); 400 } 401 402 /** 403 * Sets the current position along the offset. 404 * 405 * @param position a value between 0 and 1 406 */ 407 public void setPosition(float position) { 408 mPosition = position; 409 invalidateSelf(); 410 } 411 412 public float getPosition() { 413 return mPosition; 414 } 415 416 /** 417 * Specifies the maximum offset when the position is at 1. 418 * 419 * @param offset maximum offset as a fraction of the drawable width, 420 * positive to shift left or negative to shift right. 421 * @see #setPosition(float) 422 */ 423 public void setOffset(float offset) { 424 mOffset = offset; 425 invalidateSelf(); 426 } 427 428 @Override 429 public void draw(Canvas canvas) { 430 copyBounds(mTmpRect); 431 canvas.save(); 432 433 // Layout direction must be obtained from the activity. 434 final boolean isLayoutRTL = ViewCompat.getLayoutDirection( 435 mActivity.getWindow().getDecorView()) == ViewCompat.LAYOUT_DIRECTION_RTL; 436 final int flipRtl = isLayoutRTL ? -1 : 1; 437 final int width = mTmpRect.width(); 438 canvas.translate(-mOffset * width * mPosition * flipRtl, 0); 439 440 // Force auto-mirroring if it's not supported by the platform. 441 if (isLayoutRTL && !mHasMirroring) { 442 canvas.translate(width, 0); 443 canvas.scale(-1, 1); 444 } 445 446 super.draw(canvas); 447 canvas.restore(); 448 } 449 } 450} 451