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