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