1/*
2 * Copyright (C) 2014 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 */
16package android.support.v7.app;
17
18import android.app.Activity;
19import android.app.ActionBar;
20import android.content.Context;
21import android.content.res.Configuration;
22import android.content.res.TypedArray;
23import android.graphics.drawable.Drawable;
24import android.os.Build;
25import android.support.annotation.Nullable;
26import android.support.annotation.StringRes;
27import android.support.v4.view.GravityCompat;
28import android.support.v4.view.ViewCompat;
29import android.support.v4.widget.DrawerLayout;
30import android.support.v7.widget.Toolbar;
31import android.view.MenuItem;
32import android.view.View;
33import android.support.v7.appcompat.R;
34
35/**
36 * This class provides a handy way to tie together the functionality of
37 * {@link android.support.v4.widget.DrawerLayout} and the framework <code>ActionBar</code> to
38 * implement the recommended design for navigation drawers.
39 *
40 * <p>To use <code>ActionBarDrawerToggle</code>, create one in your Activity and call through
41 * to the following methods corresponding to your Activity callbacks:</p>
42 *
43 * <ul>
44 * <li>{@link android.app.Activity#onConfigurationChanged(android.content.res.Configuration)
45 * onConfigurationChanged}
46 * <li>{@link android.app.Activity#onOptionsItemSelected(android.view.MenuItem)
47 * onOptionsItemSelected}</li>
48 * </ul>
49 *
50 * <p>Call {@link #syncState()} from your <code>Activity</code>'s
51 * {@link android.app.Activity#onPostCreate(android.os.Bundle) onPostCreate} to synchronize the
52 * indicator with the state of the linked DrawerLayout after <code>onRestoreInstanceState</code>
53 * has occurred.</p>
54 *
55 * <p><code>ActionBarDrawerToggle</code> can be used directly as a
56 * {@link android.support.v4.widget.DrawerLayout.DrawerListener}, or if you are already providing
57 * your own listener, call through to each of the listener methods from your own.</p>
58 *
59 * <p>
60 * You can customize the the animated toggle by defining the
61 * {@link android.support.v7.appcompat.R.styleable#DrawerArrowToggle drawerArrowStyle} in your
62 * ActionBar theme.
63 */
64public class ActionBarDrawerToggle implements DrawerLayout.DrawerListener {
65
66    /**
67     * Allows an implementing Activity to return an {@link ActionBarDrawerToggle.Delegate} to use
68     * with ActionBarDrawerToggle.
69     */
70    public interface DelegateProvider {
71
72        /**
73         * @return Delegate to use for ActionBarDrawableToggles, or null if the Activity
74         * does not wish to override the default behavior.
75         */
76        @Nullable
77        Delegate getDrawerToggleDelegate();
78    }
79
80    /**
81     * @deprecated Temporary class for the transition from old drawer toggle to new drawer toggle.
82     */
83    interface TmpDelegateProvider {
84
85        @Nullable
86        Delegate getV7DrawerToggleDelegate();
87    }
88
89    public interface Delegate {
90
91        /**
92         * Set the Action Bar's up indicator drawable and content description.
93         *
94         * @param upDrawable     - Drawable to set as up indicator
95         * @param contentDescRes - Content description to set
96         */
97        void setActionBarUpIndicator(Drawable upDrawable, @StringRes int contentDescRes);
98
99        /**
100         * Set the Action Bar's up indicator content description.
101         *
102         * @param contentDescRes - Content description to set
103         */
104        void setActionBarDescription(@StringRes int contentDescRes);
105
106        /**
107         * Returns the drawable to be set as up button when DrawerToggle is disabled
108         */
109        Drawable getThemeUpIndicator();
110
111        /**
112         * Returns the context of ActionBar
113         */
114        Context getActionBarThemedContext();
115    }
116
117    private final Delegate mActivityImpl;
118    private final DrawerLayout mDrawerLayout;
119
120    private DrawerToggle mSlider;
121    private Drawable mHomeAsUpIndicator;
122    private boolean mDrawerIndicatorEnabled = true;
123    private boolean mHasCustomUpIndicator;
124    private final int mOpenDrawerContentDescRes;
125    private final int mCloseDrawerContentDescRes;
126    // used in toolbar mode when DrawerToggle is disabled
127    private View.OnClickListener mToolbarNavigationClickListener;
128
129    /**
130     * Construct a new ActionBarDrawerToggle.
131     *
132     * <p>The given {@link Activity} will be linked to the specified {@link DrawerLayout} and
133     * its Actionbar's Up button will be set to a custom drawable.
134     * <p>This drawable shows a Hamburger icon when drawer is closed and an arrow when drawer
135     * is open. It animates between these two states as the drawer opens.</p>
136     *
137     * <p>String resources must be provided to describe the open/close drawer actions for
138     * accessibility services.</p>
139     *
140     * @param activity                  The Activity hosting the drawer. Should have an ActionBar.
141     * @param drawerLayout              The DrawerLayout to link to the given Activity's ActionBar
142     * @param openDrawerContentDescRes  A String resource to describe the "open drawer" action
143     *                                  for accessibility
144     * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action
145     *                                  for accessibility
146     */
147    public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout,
148            @StringRes int openDrawerContentDescRes,
149            @StringRes int closeDrawerContentDescRes) {
150        this(activity, null, drawerLayout, null, openDrawerContentDescRes,
151                closeDrawerContentDescRes);
152    }
153
154    /**
155     * Construct a new ActionBarDrawerToggle with a Toolbar.
156     * <p>
157     * The given {@link Activity} will be linked to the specified {@link DrawerLayout} and
158     * the Toolbar's navigation icon will be set to a custom drawable. Using this constructor
159     * will set Toolbar's navigation click listener to toggle the drawer when it is clicked.
160     * <p>
161     * This drawable shows a Hamburger icon when drawer is closed and an arrow when drawer
162     * is open. It animates between these two states as the drawer opens.
163     * <p>
164     * String resources must be provided to describe the open/close drawer actions for
165     * accessibility services.
166     * <p>
167     * Please use {@link #ActionBarDrawerToggle(Activity, DrawerLayout, int, int)} if you are
168     * setting the Toolbar as the ActionBar of your activity.
169     *
170     * @param activity                  The Activity hosting the drawer.
171     * @param toolbar                   The toolbar to use if you have an independent Toolbar.
172     * @param drawerLayout              The DrawerLayout to link to the given Activity's ActionBar
173     * @param openDrawerContentDescRes  A String resource to describe the "open drawer" action
174     *                                  for accessibility
175     * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action
176     *                                  for accessibility
177     */
178    public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout,
179            Toolbar toolbar, @StringRes int openDrawerContentDescRes,
180            @StringRes int closeDrawerContentDescRes) {
181        this(activity, toolbar, drawerLayout, null, openDrawerContentDescRes,
182                closeDrawerContentDescRes);
183    }
184
185    /**
186     * In the future, we can make this constructor public if we want to let developers customize
187     * the
188     * animation.
189     */
190    <T extends Drawable & DrawerToggle> ActionBarDrawerToggle(Activity activity, Toolbar toolbar,
191            DrawerLayout drawerLayout, T slider,
192            @StringRes int openDrawerContentDescRes,
193            @StringRes int closeDrawerContentDescRes) {
194        if (toolbar != null) {
195            mActivityImpl = new ToolbarCompatDelegate(toolbar);
196            toolbar.setNavigationOnClickListener(new View.OnClickListener() {
197                @Override
198                public void onClick(View v) {
199                    if (mDrawerIndicatorEnabled) {
200                        toggle();
201                    } else if (mToolbarNavigationClickListener != null) {
202                        mToolbarNavigationClickListener.onClick(v);
203                    }
204                }
205            });
206        } else if (activity instanceof DelegateProvider) { // Allow the Activity to provide an impl
207            mActivityImpl = ((DelegateProvider) activity).getDrawerToggleDelegate();
208        } else if (activity instanceof TmpDelegateProvider) {// tmp interface for transition
209            mActivityImpl = ((TmpDelegateProvider) activity).getV7DrawerToggleDelegate();
210        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
211            mActivityImpl = new JellybeanMr2Delegate(activity);
212        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
213            mActivityImpl = new HoneycombDelegate(activity);
214        } else {
215            mActivityImpl = new DummyDelegate(activity);
216        }
217
218        mDrawerLayout = drawerLayout;
219        mOpenDrawerContentDescRes = openDrawerContentDescRes;
220        mCloseDrawerContentDescRes = closeDrawerContentDescRes;
221        if (slider == null) {
222            mSlider = new DrawerArrowDrawableToggle(activity,
223                    mActivityImpl.getActionBarThemedContext());
224        } else {
225            mSlider = slider;
226        }
227
228        mHomeAsUpIndicator = getThemeUpIndicator();
229    }
230
231    /**
232     * Synchronize the state of the drawer indicator/affordance with the linked DrawerLayout.
233     *
234     * <p>This should be called from your <code>Activity</code>'s
235     * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} method to synchronize after
236     * the DrawerLayout's instance state has been restored, and any other time when the state
237     * may have diverged in such a way that the ActionBarDrawerToggle was not notified.
238     * (For example, if you stop forwarding appropriate drawer events for a period of time.)</p>
239     */
240    public void syncState() {
241        if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
242            mSlider.setPosition(1);
243        } else {
244            mSlider.setPosition(0);
245        }
246        if (mDrawerIndicatorEnabled) {
247            setActionBarUpIndicator((Drawable) mSlider,
248                    mDrawerLayout.isDrawerOpen(GravityCompat.START) ?
249                            mCloseDrawerContentDescRes : mOpenDrawerContentDescRes);
250        }
251    }
252
253    /**
254     * This method should always be called by your <code>Activity</code>'s
255     * {@link Activity#onConfigurationChanged(android.content.res.Configuration)
256     * onConfigurationChanged}
257     * method.
258     *
259     * @param newConfig The new configuration
260     */
261    public void onConfigurationChanged(Configuration newConfig) {
262        // Reload drawables that can change with configuration
263        if (!mHasCustomUpIndicator) {
264            mHomeAsUpIndicator = getThemeUpIndicator();
265        }
266        syncState();
267    }
268
269    /**
270     * This method should be called by your <code>Activity</code>'s
271     * {@link Activity#onOptionsItemSelected(android.view.MenuItem) onOptionsItemSelected} method.
272     * If it returns true, your <code>onOptionsItemSelected</code> method should return true and
273     * skip further processing.
274     *
275     * @param item the MenuItem instance representing the selected menu item
276     * @return true if the event was handled and further processing should not occur
277     */
278    public boolean onOptionsItemSelected(MenuItem item) {
279        if (item != null && item.getItemId() == android.R.id.home && mDrawerIndicatorEnabled) {
280            toggle();
281            return true;
282        }
283        return false;
284    }
285
286    private void toggle() {
287        if (mDrawerLayout.isDrawerVisible(GravityCompat.START)) {
288            mDrawerLayout.closeDrawer(GravityCompat.START);
289        } else {
290            mDrawerLayout.openDrawer(GravityCompat.START);
291        }
292    }
293
294    /**
295     * Set the up indicator to display when the drawer indicator is not
296     * enabled.
297     * <p>
298     * If you pass <code>null</code> to this method, the default drawable from
299     * the theme will be used.
300     *
301     * @param indicator A drawable to use for the up indicator, or null to use
302     *                  the theme's default
303     * @see #setDrawerIndicatorEnabled(boolean)
304     */
305    public void setHomeAsUpIndicator(Drawable indicator) {
306        if (indicator == null) {
307            mHomeAsUpIndicator = getThemeUpIndicator();
308            mHasCustomUpIndicator = false;
309        } else {
310            mHomeAsUpIndicator = indicator;
311            mHasCustomUpIndicator = true;
312        }
313
314        if (!mDrawerIndicatorEnabled) {
315            setActionBarUpIndicator(mHomeAsUpIndicator, 0);
316        }
317    }
318
319    /**
320     * Set the up indicator to display when the drawer indicator is not
321     * enabled.
322     * <p>
323     * If you pass 0 to this method, the default drawable from the theme will
324     * be used.
325     *
326     * @param resId Resource ID of a drawable to use for the up indicator, or 0
327     *              to use the theme's default
328     * @see #setDrawerIndicatorEnabled(boolean)
329     */
330    public void setHomeAsUpIndicator(int resId) {
331        Drawable indicator = null;
332        if (resId != 0) {
333            indicator = mDrawerLayout.getResources().getDrawable(resId);
334        }
335        setHomeAsUpIndicator(indicator);
336    }
337
338    /**
339     * @return true if the enhanced drawer indicator is enabled, false otherwise
340     * @see #setDrawerIndicatorEnabled(boolean)
341     */
342    public boolean isDrawerIndicatorEnabled() {
343        return mDrawerIndicatorEnabled;
344    }
345
346    /**
347     * Enable or disable the drawer indicator. The indicator defaults to enabled.
348     *
349     * <p>When the indicator is disabled, the <code>ActionBar</code> will revert to displaying
350     * the home-as-up indicator provided by the <code>Activity</code>'s theme in the
351     * <code>android.R.attr.homeAsUpIndicator</code> attribute instead of the animated
352     * drawer glyph.</p>
353     *
354     * @param enable true to enable, false to disable
355     */
356    public void setDrawerIndicatorEnabled(boolean enable) {
357        if (enable != mDrawerIndicatorEnabled) {
358            if (enable) {
359                setActionBarUpIndicator((Drawable) mSlider,
360                        mDrawerLayout.isDrawerOpen(GravityCompat.START) ?
361                                mCloseDrawerContentDescRes : mOpenDrawerContentDescRes);
362            } else {
363                setActionBarUpIndicator(mHomeAsUpIndicator, 0);
364            }
365            mDrawerIndicatorEnabled = enable;
366        }
367    }
368
369
370    /**
371     * {@link DrawerLayout.DrawerListener} callback method. If you do not use your
372     * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call
373     * through to this method from your own listener object.
374     *
375     * @param drawerView  The child view that was moved
376     * @param slideOffset The new offset of this drawer within its range, from 0-1
377     */
378    @Override
379    public void onDrawerSlide(View drawerView, float slideOffset) {
380        mSlider.setPosition(Math.min(1f, Math.max(0, slideOffset)));
381    }
382
383    /**
384     * {@link DrawerLayout.DrawerListener} callback method. If you do not use your
385     * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call
386     * through to this method from your own listener object.
387     *
388     * @param drawerView Drawer view that is now open
389     */
390    @Override
391    public void onDrawerOpened(View drawerView) {
392        mSlider.setPosition(1);
393        if (mDrawerIndicatorEnabled) {
394            setActionBarDescription(mCloseDrawerContentDescRes);
395        }
396    }
397
398    /**
399     * {@link DrawerLayout.DrawerListener} callback method. If you do not use your
400     * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call
401     * through to this method from your own listener object.
402     *
403     * @param drawerView Drawer view that is now closed
404     */
405    @Override
406    public void onDrawerClosed(View drawerView) {
407        mSlider.setPosition(0);
408        if (mDrawerIndicatorEnabled) {
409            setActionBarDescription(mOpenDrawerContentDescRes);
410        }
411    }
412
413    /**
414     * {@link DrawerLayout.DrawerListener} callback method. If you do not use your
415     * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call
416     * through to this method from your own listener object.
417     *
418     * @param newState The new drawer motion state
419     */
420    @Override
421    public void onDrawerStateChanged(int newState) {
422    }
423
424    /**
425     * Returns the fallback listener for Navigation icon click events.
426     *
427     * @return The click listener which receives Navigation click events from Toolbar when
428     * drawer indicator is disabled.
429     * @see #setToolbarNavigationClickListener(android.view.View.OnClickListener)
430     * @see #setDrawerIndicatorEnabled(boolean)
431     * @see #isDrawerIndicatorEnabled()
432     */
433    public View.OnClickListener getToolbarNavigationClickListener() {
434        return mToolbarNavigationClickListener;
435    }
436
437    /**
438     * When DrawerToggle is constructed with a Toolbar, it sets the click listener on
439     * the Navigation icon. If you want to listen for clicks on the Navigation icon when
440     * DrawerToggle is disabled ({@link #setDrawerIndicatorEnabled(boolean)}, you should call this
441     * method with your listener and DrawerToggle will forward click events to that listener
442     * when drawer indicator is disabled.
443     *
444     * @see #setDrawerIndicatorEnabled(boolean)
445     */
446    public void setToolbarNavigationClickListener(
447            View.OnClickListener onToolbarNavigationClickListener) {
448        mToolbarNavigationClickListener = onToolbarNavigationClickListener;
449    }
450
451    void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) {
452        mActivityImpl.setActionBarUpIndicator(upDrawable, contentDescRes);
453    }
454
455    void setActionBarDescription(int contentDescRes) {
456        mActivityImpl.setActionBarDescription(contentDescRes);
457    }
458
459    Drawable getThemeUpIndicator() {
460        return mActivityImpl.getThemeUpIndicator();
461    }
462
463    static class DrawerArrowDrawableToggle extends DrawerArrowDrawable
464            implements DrawerToggle {
465
466        private final Activity mActivity;
467
468        public DrawerArrowDrawableToggle(Activity activity, Context themedContext) {
469            super(themedContext);
470            mActivity = activity;
471        }
472
473        public void setPosition(float position) {
474            if (position == 1f) {
475                setVerticalMirror(true);
476            } else if (position == 0f) {
477                setVerticalMirror(false);
478            }
479            super.setProgress(position);
480        }
481
482        @Override
483        boolean isLayoutRtl() {
484            return ViewCompat.getLayoutDirection(mActivity.getWindow().getDecorView())
485                    == ViewCompat.LAYOUT_DIRECTION_RTL;
486        }
487
488        public float getPosition() {
489            return super.getProgress();
490        }
491    }
492
493    /**
494     * Interface for toggle drawables. Can be public in the future
495     */
496    static interface DrawerToggle {
497
498        public void setPosition(float position);
499
500        public float getPosition();
501    }
502
503    /**
504     * Delegate if SDK version is between honeycomb and JBMR2
505     */
506    private static class HoneycombDelegate implements Delegate {
507
508        final Activity mActivity;
509        ActionBarDrawerToggleHoneycomb.SetIndicatorInfo mSetIndicatorInfo;
510
511        private HoneycombDelegate(Activity activity) {
512            mActivity = activity;
513        }
514
515        @Override
516        public Drawable getThemeUpIndicator() {
517            return ActionBarDrawerToggleHoneycomb.getThemeUpIndicator(mActivity);
518        }
519
520        @Override
521        public Context getActionBarThemedContext() {
522            final ActionBar actionBar = mActivity.getActionBar();
523            final Context context;
524            if (actionBar != null) {
525                context = actionBar.getThemedContext();
526            } else {
527                context = mActivity;
528            }
529            return context;
530        }
531
532        @Override
533        public void setActionBarUpIndicator(Drawable themeImage, int contentDescRes) {
534            mActivity.getActionBar().setDisplayShowHomeEnabled(true);
535            mSetIndicatorInfo = ActionBarDrawerToggleHoneycomb.setActionBarUpIndicator(
536                    mSetIndicatorInfo, mActivity, themeImage, contentDescRes);
537            mActivity.getActionBar().setDisplayShowHomeEnabled(false);
538        }
539
540        @Override
541        public void setActionBarDescription(int contentDescRes) {
542            mSetIndicatorInfo = ActionBarDrawerToggleHoneycomb.setActionBarDescription(
543                    mSetIndicatorInfo, mActivity, contentDescRes);
544        }
545    }
546
547    /**
548     * Delegate if SDK version is JB MR2 or newer
549     */
550    private static class JellybeanMr2Delegate implements Delegate {
551
552        final Activity mActivity;
553
554        private JellybeanMr2Delegate(Activity activity) {
555            mActivity = activity;
556        }
557
558        @Override
559        public Drawable getThemeUpIndicator() {
560            final TypedArray a = getActionBarThemedContext().obtainStyledAttributes(null,
561                    new int[]{android.R.attr.homeAsUpIndicator}, android.R.attr.actionBarStyle, 0);
562            final Drawable result = a.getDrawable(0);
563            a.recycle();
564            return result;
565        }
566
567        @Override
568        public Context getActionBarThemedContext() {
569            final ActionBar actionBar = mActivity.getActionBar();
570            final Context context;
571            if (actionBar != null) {
572                context = actionBar.getThemedContext();
573            } else {
574                context = mActivity;
575            }
576            return context;
577        }
578
579        @Override
580        public void setActionBarUpIndicator(Drawable drawable, int contentDescRes) {
581            final ActionBar actionBar = mActivity.getActionBar();
582            if (actionBar != null) {
583                actionBar.setHomeAsUpIndicator(drawable);
584                actionBar.setHomeActionContentDescription(contentDescRes);
585            }
586        }
587
588        @Override
589        public void setActionBarDescription(int contentDescRes) {
590            final ActionBar actionBar = mActivity.getActionBar();
591            if (actionBar != null) {
592                actionBar.setHomeActionContentDescription(contentDescRes);
593            }
594        }
595    }
596
597    /**
598     * Used when DrawerToggle is initialized with a Toolbar
599     */
600    static class ToolbarCompatDelegate implements Delegate {
601
602        final Toolbar mToolbar;
603
604        ToolbarCompatDelegate(Toolbar toolbar) {
605            mToolbar = toolbar;
606        }
607
608        @Override
609        public void setActionBarUpIndicator(Drawable upDrawable, @StringRes int contentDescRes) {
610            mToolbar.setNavigationIcon(upDrawable);
611            mToolbar.setNavigationContentDescription(contentDescRes);
612        }
613
614        @Override
615        public void setActionBarDescription(@StringRes int contentDescRes) {
616            mToolbar.setNavigationContentDescription(contentDescRes);
617        }
618
619        @Override
620        public Drawable getThemeUpIndicator() {
621            final TypedArray a = mToolbar.getContext()
622                    .obtainStyledAttributes(new int[]{android.R.id.home});
623            final Drawable result = a.getDrawable(0);
624            a.recycle();
625            return result;
626        }
627
628        @Override
629        public Context getActionBarThemedContext() {
630            return mToolbar.getContext();
631        }
632    }
633
634    /**
635     * Fallback delegate
636     */
637    static class DummyDelegate implements Delegate {
638        final Activity mActivity;
639
640        DummyDelegate(Activity activity) {
641            mActivity = activity;
642        }
643
644        @Override
645        public void setActionBarUpIndicator(Drawable upDrawable, @StringRes int contentDescRes) {
646
647        }
648
649        @Override
650        public void setActionBarDescription(@StringRes int contentDescRes) {
651
652        }
653
654        @Override
655        public Drawable getThemeUpIndicator() {
656            return null;
657        }
658
659        @Override
660        public Context getActionBarThemedContext() {
661            return mActivity;
662        }
663    }
664}
665