/* * Copyright 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.legacy.app; import android.app.ActionBar; import android.app.Activity; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.InsetDrawable; import android.os.Build; import android.util.Log; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.content.ContextCompat; import androidx.core.view.GravityCompat; import androidx.core.view.ViewCompat; import androidx.drawerlayout.widget.DrawerLayout; import java.lang.reflect.Method; /** * This class provides a handy way to tie together the functionality of * {@link DrawerLayout} and the framework ActionBar to implement the recommended * design for navigation drawers. * *

To use ActionBarDrawerToggle, create one in your Activity and call through * to the following methods corresponding to your Activity callbacks:

* * * *

Call {@link #syncState()} from your Activity's * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} to synchronize the indicator * with the state of the linked DrawerLayout after onRestoreInstanceState * has occurred.

* *

ActionBarDrawerToggle can be used directly as a * {@link DrawerLayout.DrawerListener}, or if you are already providing your own listener, * call through to each of the listener methods from your own.

* */ @Deprecated public class ActionBarDrawerToggle implements DrawerLayout.DrawerListener { /** * Allows an implementing Activity to return an {@link ActionBarDrawerToggle.Delegate} to use * with ActionBarDrawerToggle. * * @deprecated Use ActionBarDrawerToggle.DelegateProvider in support-v7-appcompat. */ @Deprecated public interface DelegateProvider { /** * @return Delegate to use for ActionBarDrawableToggles, or null if the Activity * does not wish to override the default behavior. */ @Nullable Delegate getDrawerToggleDelegate(); } /** * @deprecated Use ActionBarDrawerToggle.DelegateProvider in support-v7-appcompat. */ @Deprecated public interface Delegate { /** * @return Up indicator drawable as defined in the Activity's theme, or null if one is not * defined. */ @Nullable Drawable getThemeUpIndicator(); /** * Set the Action Bar's up indicator drawable and content description. * * @param upDrawable - Drawable to set as up indicator * @param contentDescRes - Content description to set */ void setActionBarUpIndicator(Drawable upDrawable, @StringRes int contentDescRes); /** * Set the Action Bar's up indicator content description. * * @param contentDescRes - Content description to set */ void setActionBarDescription(@StringRes int contentDescRes); } private static final String TAG = "ActionBarDrawerToggle"; private static final int[] THEME_ATTRS = new int[] { android.R.attr.homeAsUpIndicator }; /** Fraction of its total width by which to offset the toggle drawable. */ private static final float TOGGLE_DRAWABLE_OFFSET = 1 / 3f; // android.R.id.home as defined by public API in v11 private static final int ID_HOME = 0x0102002c; final Activity mActivity; private final Delegate mActivityImpl; private final DrawerLayout mDrawerLayout; private boolean mDrawerIndicatorEnabled = true; private boolean mHasCustomUpIndicator; private Drawable mHomeAsUpIndicator; private Drawable mDrawerImage; private SlideDrawable mSlider; private final int mDrawerImageResource; private final int mOpenDrawerContentDescRes; private final int mCloseDrawerContentDescRes; private SetIndicatorInfo mSetIndicatorInfo; /** * Construct a new ActionBarDrawerToggle. * *

The given {@link Activity} will be linked to the specified {@link DrawerLayout}. * The provided drawer indicator drawable will animate slightly off-screen as the drawer * is opened, indicating that in the open state the drawer will move off-screen when pressed * and in the closed state the drawer will move on-screen when pressed.

* *

String resources must be provided to describe the open/close drawer actions for * accessibility services.

* * @param activity The Activity hosting the drawer * @param drawerLayout The DrawerLayout to link to the given Activity's ActionBar * @param drawerImageRes A Drawable resource to use as the drawer indicator * @param openDrawerContentDescRes A String resource to describe the "open drawer" action * for accessibility * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action * for accessibility */ public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, @DrawableRes int drawerImageRes, @StringRes int openDrawerContentDescRes, @StringRes int closeDrawerContentDescRes) { this(activity, drawerLayout, !assumeMaterial(activity), drawerImageRes, openDrawerContentDescRes, closeDrawerContentDescRes); } private static boolean assumeMaterial(Context context) { return context.getApplicationInfo().targetSdkVersion >= 21 && (Build.VERSION.SDK_INT >= 21); } /** * Construct a new ActionBarDrawerToggle. * *

The given {@link Activity} will be linked to the specified {@link DrawerLayout}. * The provided drawer indicator drawable will animate slightly off-screen as the drawer * is opened, indicating that in the open state the drawer will move off-screen when pressed * and in the closed state the drawer will move on-screen when pressed.

* *

String resources must be provided to describe the open/close drawer actions for * accessibility services.

* * @param activity The Activity hosting the drawer * @param drawerLayout The DrawerLayout to link to the given Activity's ActionBar * @param animate True to animate the drawer indicator along with the drawer's position. * Material apps should set this to false. * @param drawerImageRes A Drawable resource to use as the drawer indicator * @param openDrawerContentDescRes A String resource to describe the "open drawer" action * for accessibility * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action * for accessibility */ public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, boolean animate, @DrawableRes int drawerImageRes, @StringRes int openDrawerContentDescRes, @StringRes int closeDrawerContentDescRes) { mActivity = activity; // Allow the Activity to provide an impl if (activity instanceof DelegateProvider) { mActivityImpl = ((DelegateProvider) activity).getDrawerToggleDelegate(); } else { mActivityImpl = null; } mDrawerLayout = drawerLayout; mDrawerImageResource = drawerImageRes; mOpenDrawerContentDescRes = openDrawerContentDescRes; mCloseDrawerContentDescRes = closeDrawerContentDescRes; mHomeAsUpIndicator = getThemeUpIndicator(); mDrawerImage = ContextCompat.getDrawable(activity, drawerImageRes); mSlider = new SlideDrawable(mDrawerImage); mSlider.setOffset(animate ? TOGGLE_DRAWABLE_OFFSET : 0); } /** * Synchronize the state of the drawer indicator/affordance with the linked DrawerLayout. * *

This should be called from your Activity's * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} method to synchronize after * the DrawerLayout's instance state has been restored, and any other time when the state * may have diverged in such a way that the ActionBarDrawerToggle was not notified. * (For example, if you stop forwarding appropriate drawer events for a period of time.)

*/ public void syncState() { if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) { mSlider.setPosition(1); } else { mSlider.setPosition(0); } if (mDrawerIndicatorEnabled) { setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ? mCloseDrawerContentDescRes : mOpenDrawerContentDescRes); } } /** * Set the up indicator to display when the drawer indicator is not * enabled. *

* If you pass null to this method, the default drawable from * the theme will be used. * * @param indicator A drawable to use for the up indicator, or null to use * the theme's default * @see #setDrawerIndicatorEnabled(boolean) */ public void setHomeAsUpIndicator(Drawable indicator) { if (indicator == null) { mHomeAsUpIndicator = getThemeUpIndicator(); mHasCustomUpIndicator = false; } else { mHomeAsUpIndicator = indicator; mHasCustomUpIndicator = true; } if (!mDrawerIndicatorEnabled) { setActionBarUpIndicator(mHomeAsUpIndicator, 0); } } /** * Set the up indicator to display when the drawer indicator is not * enabled. *

* If you pass 0 to this method, the default drawable from the theme will * be used. * * @param resId Resource ID of a drawable to use for the up indicator, or 0 * to use the theme's default * @see #setDrawerIndicatorEnabled(boolean) */ public void setHomeAsUpIndicator(int resId) { Drawable indicator = null; if (resId != 0) { indicator = ContextCompat.getDrawable(mActivity, resId); } setHomeAsUpIndicator(indicator); } /** * Enable or disable the drawer indicator. The indicator defaults to enabled. * *

When the indicator is disabled, the ActionBar will revert to displaying * the home-as-up indicator provided by the Activity's theme in the * android.R.attr.homeAsUpIndicator attribute instead of the animated * drawer glyph.

* * @param enable true to enable, false to disable */ public void setDrawerIndicatorEnabled(boolean enable) { if (enable != mDrawerIndicatorEnabled) { if (enable) { setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ? mCloseDrawerContentDescRes : mOpenDrawerContentDescRes); } else { setActionBarUpIndicator(mHomeAsUpIndicator, 0); } mDrawerIndicatorEnabled = enable; } } /** * @return true if the enhanced drawer indicator is enabled, false otherwise * @see #setDrawerIndicatorEnabled(boolean) */ public boolean isDrawerIndicatorEnabled() { return mDrawerIndicatorEnabled; } /** * This method should always be called by your Activity's * {@link Activity#onConfigurationChanged(android.content.res.Configuration) onConfigurationChanged} * method. * * @param newConfig The new configuration */ public void onConfigurationChanged(Configuration newConfig) { // Reload drawables that can change with configuration if (!mHasCustomUpIndicator) { mHomeAsUpIndicator = getThemeUpIndicator(); } mDrawerImage = ContextCompat.getDrawable(mActivity, mDrawerImageResource); syncState(); } /** * This method should be called by your Activity's * {@link Activity#onOptionsItemSelected(android.view.MenuItem) onOptionsItemSelected} method. * If it returns true, your onOptionsItemSelected method should return true and * skip further processing. * * @param item the MenuItem instance representing the selected menu item * @return true if the event was handled and further processing should not occur */ public boolean onOptionsItemSelected(MenuItem item) { if (item != null && item.getItemId() == ID_HOME && mDrawerIndicatorEnabled) { if (mDrawerLayout.isDrawerVisible(GravityCompat.START)) { mDrawerLayout.closeDrawer(GravityCompat.START); } else { mDrawerLayout.openDrawer(GravityCompat.START); } return true; } return false; } /** * {@link DrawerLayout.DrawerListener} callback method. If you do not use your * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call * through to this method from your own listener object. * * @param drawerView The child view that was moved * @param slideOffset The new offset of this drawer within its range, from 0-1 */ @Override public void onDrawerSlide(View drawerView, float slideOffset) { float glyphOffset = mSlider.getPosition(); if (slideOffset > 0.5f) { glyphOffset = Math.max(glyphOffset, Math.max(0.f, slideOffset - 0.5f) * 2); } else { glyphOffset = Math.min(glyphOffset, slideOffset * 2); } mSlider.setPosition(glyphOffset); } /** * {@link DrawerLayout.DrawerListener} callback method. If you do not use your * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call * through to this method from your own listener object. * * @param drawerView Drawer view that is now open */ @Override public void onDrawerOpened(View drawerView) { mSlider.setPosition(1); if (mDrawerIndicatorEnabled) { setActionBarDescription(mCloseDrawerContentDescRes); } } /** * {@link DrawerLayout.DrawerListener} callback method. If you do not use your * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call * through to this method from your own listener object. * * @param drawerView Drawer view that is now closed */ @Override public void onDrawerClosed(View drawerView) { mSlider.setPosition(0); if (mDrawerIndicatorEnabled) { setActionBarDescription(mOpenDrawerContentDescRes); } } /** * {@link DrawerLayout.DrawerListener} callback method. If you do not use your * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call * through to this method from your own listener object. * * @param newState The new drawer motion state */ @Override public void onDrawerStateChanged(int newState) { } private Drawable getThemeUpIndicator() { if (mActivityImpl != null) { return mActivityImpl.getThemeUpIndicator(); } if (Build.VERSION.SDK_INT >= 18) { final ActionBar actionBar = mActivity.getActionBar(); final Context context; if (actionBar != null) { context = actionBar.getThemedContext(); } else { context = mActivity; } final TypedArray a = context.obtainStyledAttributes(null, THEME_ATTRS, android.R.attr.actionBarStyle, 0); final Drawable result = a.getDrawable(0); a.recycle(); return result; } else { final TypedArray a = mActivity.obtainStyledAttributes(THEME_ATTRS); final Drawable result = a.getDrawable(0); a.recycle(); return result; } } private void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) { if (mActivityImpl != null) { mActivityImpl.setActionBarUpIndicator(upDrawable, contentDescRes); return; } if (Build.VERSION.SDK_INT >= 18) { final ActionBar actionBar = mActivity.getActionBar(); if (actionBar != null) { actionBar.setHomeAsUpIndicator(upDrawable); actionBar.setHomeActionContentDescription(contentDescRes); } } else { if (mSetIndicatorInfo == null) { mSetIndicatorInfo = new SetIndicatorInfo(mActivity); } if (mSetIndicatorInfo.mSetHomeAsUpIndicator != null) { try { final ActionBar actionBar = mActivity.getActionBar(); mSetIndicatorInfo.mSetHomeAsUpIndicator.invoke(actionBar, upDrawable); mSetIndicatorInfo.mSetHomeActionContentDescription.invoke( actionBar, contentDescRes); } catch (Exception e) { Log.w(TAG, "Couldn't set home-as-up indicator via JB-MR2 API", e); } } else if (mSetIndicatorInfo.mUpIndicatorView != null) { mSetIndicatorInfo.mUpIndicatorView.setImageDrawable(upDrawable); } else { Log.w(TAG, "Couldn't set home-as-up indicator"); } } } private void setActionBarDescription(int contentDescRes) { if (mActivityImpl != null) { mActivityImpl.setActionBarDescription(contentDescRes); return; } if (Build.VERSION.SDK_INT >= 18) { final ActionBar actionBar = mActivity.getActionBar(); if (actionBar != null) { actionBar.setHomeActionContentDescription(contentDescRes); } } else { if (mSetIndicatorInfo == null) { mSetIndicatorInfo = new SetIndicatorInfo(mActivity); } if (mSetIndicatorInfo.mSetHomeAsUpIndicator != null) { try { final ActionBar actionBar = mActivity.getActionBar(); mSetIndicatorInfo.mSetHomeActionContentDescription.invoke( actionBar, contentDescRes); // For API 19 and earlier, we need to manually force the // action bar to generate a new content description. actionBar.setSubtitle(actionBar.getSubtitle()); } catch (Exception e) { Log.w(TAG, "Couldn't set content description via JB-MR2 API", e); } } } } private static class SetIndicatorInfo { Method mSetHomeAsUpIndicator; Method mSetHomeActionContentDescription; ImageView mUpIndicatorView; SetIndicatorInfo(Activity activity) { try { mSetHomeAsUpIndicator = ActionBar.class.getDeclaredMethod("setHomeAsUpIndicator", Drawable.class); mSetHomeActionContentDescription = ActionBar.class.getDeclaredMethod( "setHomeActionContentDescription", Integer.TYPE); // If we got the method we won't need the stuff below. return; } catch (NoSuchMethodException e) { // Oh well. We'll use the other mechanism below instead. } final View home = activity.findViewById(android.R.id.home); if (home == null) { // Action bar doesn't have a known configuration, an OEM messed with things. return; } final ViewGroup parent = (ViewGroup) home.getParent(); final int childCount = parent.getChildCount(); if (childCount != 2) { // No idea which one will be the right one, an OEM messed with things. return; } final View first = parent.getChildAt(0); final View second = parent.getChildAt(1); final View up = first.getId() == android.R.id.home ? second : first; if (up instanceof ImageView) { // Jackpot! (Probably...) mUpIndicatorView = (ImageView) up; } } } private class SlideDrawable extends InsetDrawable implements Drawable.Callback { private final boolean mHasMirroring = Build.VERSION.SDK_INT > 18; private final Rect mTmpRect = new Rect(); private float mPosition; private float mOffset; SlideDrawable(Drawable wrapped) { super(wrapped, 0); } /** * Sets the current position along the offset. * * @param position a value between 0 and 1 */ public void setPosition(float position) { mPosition = position; invalidateSelf(); } public float getPosition() { return mPosition; } /** * Specifies the maximum offset when the position is at 1. * * @param offset maximum offset as a fraction of the drawable width, * positive to shift left or negative to shift right. * @see #setPosition(float) */ public void setOffset(float offset) { mOffset = offset; invalidateSelf(); } @Override public void draw(@NonNull Canvas canvas) { copyBounds(mTmpRect); canvas.save(); // Layout direction must be obtained from the activity. final boolean isLayoutRTL = ViewCompat.getLayoutDirection( mActivity.getWindow().getDecorView()) == ViewCompat.LAYOUT_DIRECTION_RTL; final int flipRtl = isLayoutRTL ? -1 : 1; final int width = mTmpRect.width(); canvas.translate(-mOffset * width * mPosition * flipRtl, 0); // Force auto-mirroring if it's not supported by the platform. if (isLayoutRTL && !mHasMirroring) { canvas.translate(width, 0); canvas.scale(-1, 1); } super.draw(canvas); canvas.restore(); } } }