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