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