• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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