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.Context;
22import android.content.res.Configuration;
23import android.graphics.Canvas;
24import android.graphics.Rect;
25import android.graphics.drawable.Drawable;
26import android.graphics.drawable.InsetDrawable;
27import android.os.Build;
28import android.support.annotation.DrawableRes;
29import android.support.annotation.Nullable;
30import android.support.annotation.RequiresApi;
31import android.support.annotation.StringRes;
32import android.support.v4.content.ContextCompat;
33import android.support.v4.view.GravityCompat;
34import android.support.v4.view.ViewCompat;
35import android.support.v4.widget.DrawerLayout;
36import android.view.MenuItem;
37import android.view.View;
38
39/**
40 * This class provides a handy way to tie together the functionality of
41 * {@link DrawerLayout} and the framework <code>ActionBar</code> to implement the recommended
42 * design for navigation drawers.
43 *
44 * <p>To use <code>ActionBarDrawerToggle</code>, create one in your Activity and call through
45 * to the following methods corresponding to your Activity callbacks:</p>
46 *
47 * <ul>
48 * <li>{@link Activity#onConfigurationChanged(android.content.res.Configuration) onConfigurationChanged}</li>
49 * <li>{@link Activity#onOptionsItemSelected(android.view.MenuItem) onOptionsItemSelected}</li>
50 * </ul>
51 *
52 * <p>Call {@link #syncState()} from your <code>Activity</code>'s
53 * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} to synchronize the indicator
54 * 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 your own listener,
59 * call through to each of the listener methods from your own.</p>
60 *
61 */
62@Deprecated
63public class ActionBarDrawerToggle implements DrawerLayout.DrawerListener {
64
65    /**
66     * Allows an implementing Activity to return an {@link ActionBarDrawerToggle.Delegate} to use
67     * with ActionBarDrawerToggle.
68     *
69     * @deprecated Use ActionBarDrawerToggle.DelegateProvider in support-v7-appcompat.
70     */
71    @Deprecated
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    /**
83     * @deprecated Use ActionBarDrawerToggle.DelegateProvider in support-v7-appcompat.
84     */
85    @Deprecated
86    public interface Delegate {
87        /**
88         * @return Up indicator drawable as defined in the Activity's theme, or null if one is not
89         *         defined.
90         */
91        @Nullable
92        Drawable getThemeUpIndicator();
93
94        /**
95         * Set the Action Bar's up indicator drawable and content description.
96         *
97         * @param upDrawable     - Drawable to set as up indicator
98         * @param contentDescRes - Content description to set
99         */
100        void setActionBarUpIndicator(Drawable upDrawable, @StringRes int contentDescRes);
101
102        /**
103         * Set the Action Bar's up indicator content description.
104         *
105         * @param contentDescRes - Content description to set
106         */
107        void setActionBarDescription(@StringRes int contentDescRes);
108    }
109
110    private interface ActionBarDrawerToggleImpl {
111        Drawable getThemeUpIndicator(Activity activity);
112        Object setActionBarUpIndicator(Object info, Activity activity,
113                Drawable themeImage, int contentDescRes);
114        Object setActionBarDescription(Object info, Activity activity, int contentDescRes);
115    }
116
117    @RequiresApi(11)
118    private static class ActionBarDrawerToggleImplIcs implements ActionBarDrawerToggleImpl {
119        ActionBarDrawerToggleImplIcs() {
120        }
121
122        @Override
123        public Drawable getThemeUpIndicator(Activity activity) {
124            return ActionBarDrawerToggleIcs.getThemeUpIndicator(activity);
125        }
126
127        @Override
128        public Object setActionBarUpIndicator(Object info, Activity activity,
129                Drawable themeImage, int contentDescRes) {
130            return ActionBarDrawerToggleIcs.setActionBarUpIndicator(info, activity,
131                    themeImage, contentDescRes);
132        }
133
134        @Override
135        public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) {
136            return ActionBarDrawerToggleIcs.setActionBarDescription(info, activity,
137                    contentDescRes);
138        }
139    }
140
141    @RequiresApi(18)
142    private static class ActionBarDrawerToggleImplJellybeanMR2
143            implements ActionBarDrawerToggleImpl {
144        ActionBarDrawerToggleImplJellybeanMR2() {
145        }
146
147        @Override
148        public Drawable getThemeUpIndicator(Activity activity) {
149            return ActionBarDrawerToggleJellybeanMR2.getThemeUpIndicator(activity);
150        }
151
152        @Override
153        public Object setActionBarUpIndicator(Object info, Activity activity,
154                Drawable themeImage, int contentDescRes) {
155            return ActionBarDrawerToggleJellybeanMR2.setActionBarUpIndicator(info, activity,
156                    themeImage, contentDescRes);
157        }
158
159        @Override
160        public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) {
161            return ActionBarDrawerToggleJellybeanMR2.setActionBarDescription(info, activity,
162                    contentDescRes);
163        }
164    }
165
166    private static final ActionBarDrawerToggleImpl IMPL;
167
168    static {
169        if (Build.VERSION.SDK_INT >= 18) {
170            IMPL = new ActionBarDrawerToggleImplJellybeanMR2();
171        } else {
172            IMPL = new ActionBarDrawerToggleImplIcs();
173        }
174    }
175
176    /** Fraction of its total width by which to offset the toggle drawable. */
177    private static final float TOGGLE_DRAWABLE_OFFSET = 1 / 3f;
178
179    // android.R.id.home as defined by public API in v11
180    private static final int ID_HOME = 0x0102002c;
181
182    final Activity mActivity;
183    private final Delegate mActivityImpl;
184    private final DrawerLayout mDrawerLayout;
185    private boolean mDrawerIndicatorEnabled = true;
186    private boolean mHasCustomUpIndicator;
187
188    private Drawable mHomeAsUpIndicator;
189    private Drawable mDrawerImage;
190    private SlideDrawable mSlider;
191    private final int mDrawerImageResource;
192    private final int mOpenDrawerContentDescRes;
193    private final int mCloseDrawerContentDescRes;
194
195    private Object mSetIndicatorInfo;
196
197    /**
198     * Construct a new ActionBarDrawerToggle.
199     *
200     * <p>The given {@link Activity} will be linked to the specified {@link DrawerLayout}.
201     * The provided drawer indicator drawable will animate slightly off-screen as the drawer
202     * is opened, indicating that in the open state the drawer will move off-screen when pressed
203     * and in the closed state the drawer will move on-screen when pressed.</p>
204     *
205     * <p>String resources must be provided to describe the open/close drawer actions for
206     * accessibility services.</p>
207     *
208     * @param activity The Activity hosting the drawer
209     * @param drawerLayout The DrawerLayout to link to the given Activity's ActionBar
210     * @param drawerImageRes A Drawable resource to use as the drawer indicator
211     * @param openDrawerContentDescRes A String resource to describe the "open drawer" action
212     *                                 for accessibility
213     * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action
214     *                                  for accessibility
215     */
216    public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout,
217            @DrawableRes int drawerImageRes, @StringRes int openDrawerContentDescRes,
218            @StringRes int closeDrawerContentDescRes) {
219        this(activity, drawerLayout, !assumeMaterial(activity), drawerImageRes,
220                openDrawerContentDescRes, closeDrawerContentDescRes);
221    }
222
223    private static boolean assumeMaterial(Context context) {
224        return context.getApplicationInfo().targetSdkVersion >= 21
225                && (Build.VERSION.SDK_INT >= 21);
226    }
227
228    /**
229     * Construct a new ActionBarDrawerToggle.
230     *
231     * <p>The given {@link Activity} will be linked to the specified {@link DrawerLayout}.
232     * The provided drawer indicator drawable will animate slightly off-screen as the drawer
233     * is opened, indicating that in the open state the drawer will move off-screen when pressed
234     * and in the closed state the drawer will move on-screen when pressed.</p>
235     *
236     * <p>String resources must be provided to describe the open/close drawer actions for
237     * accessibility services.</p>
238     *
239     * @param activity The Activity hosting the drawer
240     * @param drawerLayout The DrawerLayout to link to the given Activity's ActionBar
241     * @param animate True to animate the drawer indicator along with the drawer's position.
242     *                Material apps should set this to false.
243     * @param drawerImageRes A Drawable resource to use as the drawer indicator
244     * @param openDrawerContentDescRes A String resource to describe the "open drawer" action
245     *                                 for accessibility
246     * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action
247     *                                  for accessibility
248     */
249    public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, boolean animate,
250            @DrawableRes int drawerImageRes, @StringRes int openDrawerContentDescRes,
251            @StringRes int closeDrawerContentDescRes) {
252        mActivity = activity;
253
254        // Allow the Activity to provide an impl
255        if (activity instanceof DelegateProvider) {
256            mActivityImpl = ((DelegateProvider) activity).getDrawerToggleDelegate();
257        } else {
258            mActivityImpl = null;
259        }
260
261        mDrawerLayout = drawerLayout;
262        mDrawerImageResource = drawerImageRes;
263        mOpenDrawerContentDescRes = openDrawerContentDescRes;
264        mCloseDrawerContentDescRes = closeDrawerContentDescRes;
265
266        mHomeAsUpIndicator = getThemeUpIndicator();
267        mDrawerImage = ContextCompat.getDrawable(activity, drawerImageRes);
268        mSlider = new SlideDrawable(mDrawerImage);
269        mSlider.setOffset(animate ? TOGGLE_DRAWABLE_OFFSET : 0);
270    }
271
272    /**
273     * Synchronize the state of the drawer indicator/affordance with the linked DrawerLayout.
274     *
275     * <p>This should be called from your <code>Activity</code>'s
276     * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} method to synchronize after
277     * the DrawerLayout's instance state has been restored, and any other time when the state
278     * may have diverged in such a way that the ActionBarDrawerToggle was not notified.
279     * (For example, if you stop forwarding appropriate drawer events for a period of time.)</p>
280     */
281    public void syncState() {
282        if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
283            mSlider.setPosition(1);
284        } else {
285            mSlider.setPosition(0);
286        }
287
288        if (mDrawerIndicatorEnabled) {
289            setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START)
290                    ? mCloseDrawerContentDescRes : mOpenDrawerContentDescRes);
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 = ContextCompat.getDrawable(mActivity, resId);
334        }
335
336        setHomeAsUpIndicator(indicator);
337    }
338
339    /**
340     * Enable or disable the drawer indicator. The indicator defaults to enabled.
341     *
342     * <p>When the indicator is disabled, the <code>ActionBar</code> will revert to displaying
343     * the home-as-up indicator provided by the <code>Activity</code>'s theme in the
344     * <code>android.R.attr.homeAsUpIndicator</code> attribute instead of the animated
345     * drawer glyph.</p>
346     *
347     * @param enable true to enable, false to disable
348     */
349    public void setDrawerIndicatorEnabled(boolean enable) {
350        if (enable != mDrawerIndicatorEnabled) {
351            if (enable) {
352                setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START)
353                        ? mCloseDrawerContentDescRes : mOpenDrawerContentDescRes);
354            } else {
355                setActionBarUpIndicator(mHomeAsUpIndicator, 0);
356            }
357            mDrawerIndicatorEnabled = enable;
358        }
359    }
360
361    /**
362     * @return true if the enhanced drawer indicator is enabled, false otherwise
363     * @see #setDrawerIndicatorEnabled(boolean)
364     */
365    public boolean isDrawerIndicatorEnabled() {
366        return mDrawerIndicatorEnabled;
367    }
368
369    /**
370     * This method should always be called by your <code>Activity</code>'s
371     * {@link Activity#onConfigurationChanged(android.content.res.Configuration) onConfigurationChanged}
372     * method.
373     *
374     * @param newConfig The new configuration
375     */
376    public void onConfigurationChanged(Configuration newConfig) {
377        // Reload drawables that can change with configuration
378        if (!mHasCustomUpIndicator) {
379            mHomeAsUpIndicator = getThemeUpIndicator();
380        }
381        mDrawerImage = ContextCompat.getDrawable(mActivity, mDrawerImageResource);
382        syncState();
383    }
384
385    /**
386     * This method should be called by your <code>Activity</code>'s
387     * {@link Activity#onOptionsItemSelected(android.view.MenuItem) onOptionsItemSelected} method.
388     * If it returns true, your <code>onOptionsItemSelected</code> method should return true and
389     * skip further processing.
390     *
391     * @param item the MenuItem instance representing the selected menu item
392     * @return true if the event was handled and further processing should not occur
393     */
394    public boolean onOptionsItemSelected(MenuItem item) {
395        if (item != null && item.getItemId() == ID_HOME && mDrawerIndicatorEnabled) {
396            if (mDrawerLayout.isDrawerVisible(GravityCompat.START)) {
397                mDrawerLayout.closeDrawer(GravityCompat.START);
398            } else {
399                mDrawerLayout.openDrawer(GravityCompat.START);
400            }
401            return true;
402        }
403        return false;
404    }
405
406    /**
407     * {@link DrawerLayout.DrawerListener} callback method. If you do not use your
408     * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call
409     * through to this method from your own listener object.
410     *
411     * @param drawerView The child view that was moved
412     * @param slideOffset The new offset of this drawer within its range, from 0-1
413     */
414    @Override
415    public void onDrawerSlide(View drawerView, float slideOffset) {
416        float glyphOffset = mSlider.getPosition();
417        if (slideOffset > 0.5f) {
418            glyphOffset = Math.max(glyphOffset, Math.max(0.f, slideOffset - 0.5f) * 2);
419        } else {
420            glyphOffset = Math.min(glyphOffset, slideOffset * 2);
421        }
422        mSlider.setPosition(glyphOffset);
423    }
424
425    /**
426     * {@link DrawerLayout.DrawerListener} callback method. If you do not use your
427     * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call
428     * through to this method from your own listener object.
429     *
430     * @param drawerView Drawer view that is now open
431     */
432    @Override
433    public void onDrawerOpened(View drawerView) {
434        mSlider.setPosition(1);
435        if (mDrawerIndicatorEnabled) {
436            setActionBarDescription(mCloseDrawerContentDescRes);
437        }
438    }
439
440    /**
441     * {@link DrawerLayout.DrawerListener} callback method. If you do not use your
442     * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call
443     * through to this method from your own listener object.
444     *
445     * @param drawerView Drawer view that is now closed
446     */
447    @Override
448    public void onDrawerClosed(View drawerView) {
449        mSlider.setPosition(0);
450        if (mDrawerIndicatorEnabled) {
451            setActionBarDescription(mOpenDrawerContentDescRes);
452        }
453    }
454
455    /**
456     * {@link DrawerLayout.DrawerListener} callback method. If you do not use your
457     * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call
458     * through to this method from your own listener object.
459     *
460     * @param newState The new drawer motion state
461     */
462    @Override
463    public void onDrawerStateChanged(int newState) {
464    }
465
466    Drawable getThemeUpIndicator() {
467        if (mActivityImpl != null) {
468            return mActivityImpl.getThemeUpIndicator();
469        }
470        return IMPL.getThemeUpIndicator(mActivity);
471    }
472
473    void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) {
474        if (mActivityImpl != null) {
475            mActivityImpl.setActionBarUpIndicator(upDrawable, contentDescRes);
476            return;
477        }
478        mSetIndicatorInfo = IMPL
479                .setActionBarUpIndicator(mSetIndicatorInfo, mActivity, upDrawable, contentDescRes);
480    }
481
482    void setActionBarDescription(int contentDescRes) {
483        if (mActivityImpl != null) {
484            mActivityImpl.setActionBarDescription(contentDescRes);
485            return;
486        }
487        mSetIndicatorInfo = IMPL
488                .setActionBarDescription(mSetIndicatorInfo, mActivity, contentDescRes);
489    }
490
491    private class SlideDrawable extends InsetDrawable implements Drawable.Callback {
492        private final boolean mHasMirroring = Build.VERSION.SDK_INT > 18;
493        private final Rect mTmpRect = new Rect();
494
495        private float mPosition;
496        private float mOffset;
497
498        SlideDrawable(Drawable wrapped) {
499            super(wrapped, 0);
500        }
501
502        /**
503         * Sets the current position along the offset.
504         *
505         * @param position a value between 0 and 1
506         */
507        public void setPosition(float position) {
508            mPosition = position;
509            invalidateSelf();
510        }
511
512        public float getPosition() {
513            return mPosition;
514        }
515
516        /**
517         * Specifies the maximum offset when the position is at 1.
518         *
519         * @param offset maximum offset as a fraction of the drawable width,
520         *            positive to shift left or negative to shift right.
521         * @see #setPosition(float)
522         */
523        public void setOffset(float offset) {
524            mOffset = offset;
525            invalidateSelf();
526        }
527
528        @Override
529        public void draw(Canvas canvas) {
530            copyBounds(mTmpRect);
531            canvas.save();
532
533            // Layout direction must be obtained from the activity.
534            final boolean isLayoutRTL = ViewCompat.getLayoutDirection(
535                    mActivity.getWindow().getDecorView()) == ViewCompat.LAYOUT_DIRECTION_RTL;
536            final int flipRtl = isLayoutRTL ? -1 : 1;
537            final int width = mTmpRect.width();
538            canvas.translate(-mOffset * width * mPosition * flipRtl, 0);
539
540            // Force auto-mirroring if it's not supported by the platform.
541            if (isLayoutRTL && !mHasMirroring) {
542                canvas.translate(width, 0);
543                canvas.scale(-1, 1);
544            }
545
546            super.draw(canvas);
547            canvas.restore();
548        }
549    }
550}
551