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