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