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