Disable hamburger to back arrow animation on Toolbar
Asked Answered
P

8

30

It's very easy to implement Toolbar with hamburger to back arrow animation. In my opinion this animation is pointless because as per material design spec navigation drawer covers the Toolbar when opened. My question is how to properly disable this animation and show either hamburger or back arrow using getSupportActionBar().setDisplayHomeAsUpEnabled(true);

This is how I did it, but it looks like a dirty hack:

mDrawerToggle.setDrawerIndicatorEnabled(false);

if (showHomeAsUp) {
    mDrawerToggle.setHomeAsUpIndicator(R.drawable.lib_ic_arrow_back_light);
    mDrawerToggle.setToolbarNavigationClickListener(view -> finish());
} else {
    mDrawerToggle.setHomeAsUpIndicator(R.drawable.lib_ic_menu_light);
    mDrawerToggle.setToolbarNavigationClickListener(view -> toggleDrawer());
}

Any clues how this should be properly implemented to use just setDisplayHomeAsUpEnabled to switch between hamburger and back arrow icons?

Pup answered 25/11, 2014 at 1:17 Comment(0)
F
48

This will disable the animation, when creating the drawerToggle, override onDrawerSlide():

drawerToggle = new ActionBarDrawerToggle(this, drawerLayout,
        getToolbar(), R.string.open, R.string.close) {

    @Override
    public void onDrawerClosed(View view) {
        super.onDrawerClosed(view);
    }

    @Override
    public void onDrawerOpened(View drawerView) {
        super.onDrawerOpened(drawerView);
    }

    @Override
    public void onDrawerSlide(View drawerView, float slideOffset) {
        super.onDrawerSlide(drawerView, 0); // this disables the animation 
    }
};

If you want to remove the arrow completely, you can add

 super.onDrawerSlide(drawerView, 0); // this disables the arrow @ completed state

at the end of the onDrawerOpened function.

Falcongentle answered 15/12, 2014 at 15:23 Comment(3)
This worked for me to disable the animation. However, setDisplayHomeAsUpEnabled doesn't work anymore. Ik keeps showing the hamburger. Luckily for me I don't need the back arrow.Limbus
Sadly, this could be true. I only use homeasup at places where the hamburger is not used at all.Falcongentle
also you can remove arrow completely by commenting super.onDrawerOpened(drawerView); instead of add super.onDrawerSlide(drawerView, 0); at the end of the onDrawerOpened function.Teresaterese
L
12

In my opinion this animation is pointless

Well, ActionBarDrawerToggle is meant to be animated.

From the docs:

You can customize the the animated toggle by defining the drawerArrowStyle in your ActionBar theme.

Any clues how this should be properly implemented to use just setDisplayHomeAsUpEnabled to switch between hamburger and back arrow icons?

The ActionBarDrawerToggle is just a fancy way of calling ActionBar.setHomeAsUpIndicator. So, either way you're going to have to call ActionBar.setDisplayHomeAsUpEnabled to true in order to display it.

If you're convinced that you have to use it, then I'd suggest just calling ActionBarDrawerToggle.onDrawerOpened(View drawerView) and ActionBarDrawerToggle.onDrawerClosed(View drawerView) respectively.

This will set the DrawerIndicator position to 1 or 0, switching between the arrow and the hamburger states of the DrawerArrowDrawable.

And in your case, there's no need to even attach an ActionBarDrawerToggle as a DrawerLayout.DrawerListener. As in:

mYourDrawer.setDrawerListener(mYourDrawerToggle);

But a much more forward approach would be to call ActionBar.setHomeAsUpIndicator once and apply your own hamburger icon, you could also do this via a style. Then when you want to display the back arrow, just call ActionBar.setDisplayHomeAsUpEnabled and let AppCompat or the framework handle the rest. From the comments you've made, I'm pretty sure this is what you're looking for.

If you're unsure which icon to use, the default DrawerArrowDrawable size is 24dp, which means you'd want to grab the ic_menu_white_24dp or ic_menu_black_24dp from the navigation icon set in Google's official Material design icon pack.

You could also copy the DrawerArrowDrawable into your project and let then toggle the arrow or hamburger states as you need them. It's self contained, minus a few resources.

Linares answered 28/11, 2014 at 17:38 Comment(4)
Maybe the ActionBarDrawerToggle is meant to be animated, but if you look at the Google's apps, which follows material design rules, this animation is disabled. I need the drawer listener because when the drawer is opened/closed action buttons visibility must change. I'm already using the ic_menu_white_24dp resource (just different name). If you set your own resource icon the setDisplayHomeAsUpEnabled method doesn't work any more. To display back arrow I have to swap the icon to lib_ic_arrow_back_light. Sorry but after reading your answer I still don't know which params set to true or falsePup
Thank you. I actually prefer the answer's first suggestion because I don't have to supply a custom resource that might not match the framework style going forward.Tilburg
@Pup Oh, I see. The fact is, you have to call ActionBar.setHomeAsUpIndicator to display the icon you want, so either swap out the hamburger and back arrow resources, copy over the DrawerArrowDrawable and let it draw them for you, or call ActionBarDrawerToggle.onDrawerOpened, etc like I mentioned. You're implying that Google's app's actually use the ActionBarDrawerToggle, but that may not be the case.Linares
@Linares You're right, Google might not use the toggle at all. I will try your solution if I find some spare time. Jeff Gilfelt - the back arrow from ActionBarDrawerToggle is different than the one from resources, just for your information. If you're using Activities without the Toggle keep in mind that back arrow will differ.Pup
L
4

I had a similar requirement and spent some time going through ActionBarDrawerToggle code. What you currently have is the best way forward.

More to come:

The hamburger to arrow animation is provided by a drawable implementation - DrawerArrowDrawableToggle. Currently, we don't have much control over how this drawable reacts to drawer states. Here's what the package-access constructor for actionVarDrawerToggle says:

/**
 * In the future, we can make this constructor public if we want to let developers customize
 * the
 * animation.
 */
<T extends Drawable & DrawerToggle> ActionBarDrawerToggle(Activity activity, Toolbar toolbar,
        DrawerLayout drawerLayout, T slider,
        @StringRes int openDrawerContentDescRes,
        @StringRes int closeDrawerContentDescRes)

By providing your own implementation of slider, you can control how it reacts to drawer states. The interface that slider must implement:

/**
 * Interface for toggle drawables. Can be public in the future
 */
static interface DrawerToggle {

    public void setPosition(float position);

    public float getPosition();
}

setPosition(float) is the highlight here - all drawer state changes call it to update the drawer indicator.

For the behavior you want, your slider implementation's setPosition(float position) would do nothing.

You will still need:

if (showHomeAsUp) {
    mDrawerToggle.setDrawerIndicatorEnabled(false);
    // Can be set in theme
    mDrawerToggle.setHomeAsUpIndicator(R.drawable.lib_ic_arrow_back_light);
    mDrawerToggle.setToolbarNavigationClickListener(view -> finish());
}

If you don't setDrawerIndicatorEnabled(false), the OnClickListener you set with setToolbarNavigationClickListener(view -> finish()); will not fire.

What can we do right now?

On closer inspection, I find that there is a provision for your requirement in ActionBarDrawerToggle. I find this provision even more of an hack than what you currently have. But, I'll let you decide.

ActionBarDrawerToggle lets you have some control over the drawer indicator through interface Delegate. You can have your activity implement this interface in the following manner:

public class TheActivity extends ActionBarActivity implements ActionBarDrawerToggle.Delegate {
....

    @Override
    public void setActionBarUpIndicator(Drawable drawableNotUsed, int i) {

        // First, we're not using the passed drawable, the one that animates

        // Second, we check if `displayHomeAsUp` is enabled
        final boolean displayHomeAsUpEnabled = (getSupportActionBar().getDisplayOptions()
            & ActionBar.DISPLAY_HOME_AS_UP) == ActionBar.DISPLAY_HOME_AS_UP;

        // We'll control what happens on navigation-icon click
        mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (displayHomeAsUpEnabled) {
                    finish();
                } else {
                    // `ActionBarDrawerToggle#toggle()` is private.
                    // Extend `ActionBarDrawerToggle` and make provision
                    // for toggling.
                    mDrawerToggle.toggleDrawer();
                }
            }
        });

        // I will talk about `mToolbarnavigationIcon` later on.

        if (displayHomeAsUpEnabled) {
            mToolbarNavigationIcon.setIndicator(
                          CustomDrawerArrowDrawable.HOME_AS_UP_INDICATOR);
        } else {
            mToolbarNavigationIcon.setIndicator(
                          CustomDrawerArrowDrawable.DRAWER_INDICATOR);
        }

        mToolbar.setNavigationIcon(mToolbarNavigationIcon);
        mToolbar.setNavigationContentDescription(i);
    }

    @Override
    public void setActionBarDescription(int i) {
        mToolbar.setNavigationContentDescription(i);
    }

    @Override
    public Drawable getThemeUpIndicator() {
        final TypedArray a = mToolbar.getContext()
            .obtainStyledAttributes(new int[]{android.R.attr.homeAsUpIndicator});
        final Drawable result = a.getDrawable(0);
        a.recycle();
        return result;
    }

    @Override
    public Context getActionBarThemedContext() {
        return mToolbar.getContext();
    }

    ....
}

ActionBarDrawerToggle will use setActionBarUpIndicator(Drawable, int) provided here. Since, we are ignoring the Drawable being passed, we have full control over what will be displayed.

Catch: ActionBarDrawerToggle will let our Activity act as a delegate if we pass the Toolbar parameter as null here:

public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout,
        Toolbar toolbar, @StringRes int openDrawerContentDescRes,
        @StringRes int closeDrawerContentDescRes) { .... }

And, you will need to override getV7DrawerToggleDelegate() in your activity:

@Nullable
@Override
public ActionBarDrawerToggle.Delegate getV7DrawerToggleDelegate() {
    return this;
}

As you can see, going about the proper way is a lot of extra work. And we're not done yet.

The animating DrawerArrowDrawableToggle can be styled using these attributes. If you want your drawable states(homeAsUp & hamburger) exactly like the defaults, you will need to implement it as such:

/**
 * A drawable that can draw a "Drawer hamburger" menu or an Arrow
 */
public class CustomDrawerArrowDrawable extends Drawable {

    public static final float DRAWER_INDICATOR = 0f;

    public static final float HOME_AS_UP_INDICATOR = 1f;

    private final Activity mActivity;

    private final Paint mPaint = new Paint();

    // The angle in degress that the arrow head is inclined at.
    private static final float ARROW_HEAD_ANGLE = (float) Math.toRadians(45);
    private final float mBarThickness;
    // The length of top and bottom bars when they merge into an arrow
    private final float mTopBottomArrowSize;
    // The length of middle bar
    private final float mBarSize;
    // The length of the middle bar when arrow is shaped
    private final float mMiddleArrowSize;
    // The space between bars when they are parallel
    private final float mBarGap;

    // Use Path instead of canvas operations so that if color has transparency, overlapping sections
    // wont look different
    private final Path mPath = new Path();
    // The reported intrinsic size of the drawable.
    private final int mSize;

    private float mIndicator;

    /**
     * @param context used to get the configuration for the drawable from
     */
    public CustomDrawerArrowDrawable(Activity activity, Context context) {
        final TypedArray typedArray = context.getTheme()
            .obtainStyledAttributes(null, R.styleable.DrawerArrowToggle,
                    R.attr.drawerArrowStyle,
                    R.style.Base_Widget_AppCompat_DrawerArrowToggle);
        mPaint.setAntiAlias(true);
        mPaint.setColor(typedArray.getColor(R.styleable.DrawerArrowToggle_color, 0));
        mSize = typedArray.getDimensionPixelSize(R.styleable.DrawerArrowToggle_drawableSize, 0);
        mBarSize = typedArray.getDimension(R.styleable.DrawerArrowToggle_barSize, 0);
        mTopBottomArrowSize = typedArray
            .getDimension(R.styleable.DrawerArrowToggle_topBottomBarArrowSize, 0);
        mBarThickness = typedArray.getDimension(R.styleable.DrawerArrowToggle_thickness, 0);
        mBarGap = typedArray.getDimension(R.styleable.DrawerArrowToggle_gapBetweenBars, 0);

        mMiddleArrowSize = typedArray
            .getDimension(R.styleable.DrawerArrowToggle_middleBarArrowSize, 0);
        typedArray.recycle();

        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeCap(Paint.Cap.SQUARE);
        mPaint.setStrokeWidth(mBarThickness);

        mActivity = activity;
    }

    public boolean isLayoutRtl() {
        return ViewCompat.getLayoutDirection(mActivity.getWindow().getDecorView())
            == ViewCompat.LAYOUT_DIRECTION_RTL;
    }

    @Override
    public void draw(Canvas canvas) {
        Rect bounds = getBounds();
        final boolean isRtl = isLayoutRtl();
        // Interpolated widths of arrow bars
        final float arrowSize = lerp(mBarSize, mTopBottomArrowSize, mIndicator);
        final float middleBarSize = lerp(mBarSize, mMiddleArrowSize, mIndicator);
        // Interpolated size of middle bar
        final float middleBarCut = lerp(0, mBarThickness / 2, mIndicator);
        // The rotation of the top and bottom bars (that make the arrow head)
        final float rotation = lerp(0, ARROW_HEAD_ANGLE, mIndicator);

        final float topBottomBarOffset = lerp(mBarGap + mBarThickness, 0, mIndicator);
        mPath.rewind();

        final float arrowEdge = -middleBarSize / 2;
        // draw middle bar
        mPath.moveTo(arrowEdge + middleBarCut, 0);
        mPath.rLineTo(middleBarSize - middleBarCut, 0);

        final float arrowWidth = Math.round(arrowSize * Math.cos(rotation));
        final float arrowHeight = Math.round(arrowSize * Math.sin(rotation));

        // top bar
        mPath.moveTo(arrowEdge, topBottomBarOffset);
        mPath.rLineTo(arrowWidth, arrowHeight);

        // bottom bar
        mPath.moveTo(arrowEdge, -topBottomBarOffset);
        mPath.rLineTo(arrowWidth, -arrowHeight);
        mPath.moveTo(0, 0);
        mPath.close();

        canvas.save();

        if (isRtl) {
            canvas.rotate(180, bounds.centerX(), bounds.centerY());
        }
        canvas.translate(bounds.centerX(), bounds.centerY());
        canvas.drawPath(mPath, mPaint);

        canvas.restore();
    }

    @Override
    public void setAlpha(int i) {
        mPaint.setAlpha(i);
    } 

    // override
    public boolean isAutoMirrored() {
        // Draws rotated 180 degrees in RTL mode.
        return true;
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
        mPaint.setColorFilter(colorFilter);
    }

    @Override
    public int getIntrinsicHeight() {
        return mSize;
    }

    @Override
    public int getIntrinsicWidth() {
        return mSize;
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    public void setIndicator(float indicator) {
        mIndicator = indicator;
        invalidateSelf();
    }

    /**
     * Linear interpolate between a and b with parameter t.
     */
    private static float lerp(float a, float b, float indicator) {
        if (indicator == HOME_AS_UP_INDICATOR) {
            return b;
        } else {
            return a;
        }
    }
}

CustomDrawerArrowDrawable's implementation has been borrowed from AOSP, and stripped down to allow drawing of only two states: homeAsUp & hamburger. You can toggle between these states by calling setIndicator(float). We use this in the Delegate we implemented. Moreover, using CustomDrawerArrowDrawable will allow you to style it in xml: barSize, color etc. Even though you don't need this, the implementation above lets you provide custom animations for drawer opening and closing.

I honestly don't know if I should recommend this.


If you call ActionBarDrawerToggle#setHomeAsUpIndicator(...) with argument null, it should pick the drawable defined in your theme:

<item name="android:homeAsUpIndicator">@drawable/some_back_drawable</item>

Currently, this does not happen because of a possible bug in ToolbarCompatDelegate#getThemeUpIndicator():

@Override
public Drawable getThemeUpIndicator() {
    final TypedArray a = mToolbar.getContext()
                 // Should be new int[]{android.R.attr.homeAsUpIndicator}
                .obtainStyledAttributes(new int[]{android.R.id.home});
    final Drawable result = a.getDrawable(0);
    a.recycle();
    return result;
}

Bug report that loosely discusses this (read Case 4): Link


If you decide to stick with the solution you already have, please consider using CustomDrawerArrowDrawable in place of pngs(R.drawable.lib_ic_arrow_back_light & R.drawable.lib_ic_menu_light). You won't be needing multiple drawables for density/size buckets and styling would be done in xml. Also, the final product will be the same as the framework's.

mDrawerToggle.setDrawerIndicatorEnabled(false);

CustomDrawerArrowDrawable toolbarNavigationIcon 
                = new CustomDrawerArrowDrawable(this, mToolbar.getContext());    

if (showHomeAsUp) {
    toolbarNavigationIcon.setIndicator(
                           CustomDrawerArrowDrawable.HOME_AS_UP_INDICATOR);
    mDrawerToggle.setToolbarNavigationClickListener(view -> finish());
} else {
    mToolbarNavigationIcon.setIndicator(
                           CustomDrawerArrowDrawable.DRAWER_INDICATOR);
    mDrawerToggle.setToolbarNavigationClickListener(view -> toggleDrawer());
}

mDrawerToggle.setHomeAsUpIndicator(toolbarNavigationIcon);
Lissalissak answered 28/11, 2014 at 18:4 Comment(2)
I've also spend some time on this and found this is the only but ugly solution. What do you mean by "can be set in theme"? You want to use custom attrs or some built-in style? Remember that I need to be able to switch between two different drawables. We'll wait some time with accepting the answer, maybe somebody has found a better approach.Pup
@Pup The attribute you would set in theme is android:homeAsUpIndicator. After this, you can call mDrawerToggle.setHomeAsUpIndicator(null) and the drawable given in xml would be used. This currently does not happen because of a possible bug. I have updated my answer with some more information and added a link to the bug report.Lissalissak
H
4

This is my function for controlling the ActionBarDrawableToggle located in the NavigationDrawerFragment, which I call in the onActivityCreated callback of every fragment. post functions are necessary. The hamburger icon changes into the back arrow and the back arrow is clickable. Orientation changes are properly handled by the handlers.

...

import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.app.ActionBarDrawerToggle;

...

public class NavigationDrawerFragment extends Fragment
{
    private ActionBarDrawerToggle mDrawerToggle;

    ...

    public void syncDrawerState()
    {
       new Handler().post(new Runnable()
        {
            @Override
            public void run()
            {
                final ActionBar actionBar = activity.getSupportActionBar();
                if (activity.getSupportFragmentManager().getBackStackEntryCount() > 1 && (actionBar.getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) != ActionBar.DISPLAY_HOME_AS_UP)
                {
                    new Handler().post(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            mDrawerToggle.setDrawerIndicatorEnabled(false);
                            actionBar.setDisplayHomeAsUpEnabled(true);
                            mDrawerToggle.setToolbarNavigationClickListener(onToolbarNavigationClickListener());
                        }
                    });
                } else if (activity.getSupportFragmentManager().getBackStackEntryCount() <= 1 && (actionBar.getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) == ActionBar.DISPLAY_HOME_AS_UP)
                {
                    actionBar.setHomeButtonEnabled(false);
                    actionBar.setDisplayHomeAsUpEnabled(false);
                    mDrawerToggle.setDrawerIndicatorEnabled(true);
                    mDrawerToggle.syncState();
                }
            }
        });      
    }
}

This is just my onActivityCreated method in my base fragment.

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState)
{
    super.onActivityCreated(savedInstanceState);
    navigationDrawerFragment.syncDrawerState();
}
Hembree answered 3/12, 2014 at 12:56 Comment(1)
Holy... I found that answear after 2h. I'm not sure how exactly you are using that but I implement it inside main activity in OnBackStackChangedListener and everything start working. For everyone who handling fragments stack in main activity: pastebin.com/YqKBTiXaCampney
A
4

Now there is dedicated method to disable the animation: toggle.setDrawerSlideAnimationEnabled(false)

Here's a snippet I use:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    [...]

    ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
            this, drawerLayout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
    toggle.setDrawerSlideAnimationEnabled(false);
    drawer.addDrawerListener(toggle);
    toggle.syncState();
}
Aubreir answered 8/7, 2017 at 9:5 Comment(0)
F
1

Disabling the supper call in onDrawerSlide() method will stop the animation between Arrow and Burger. You will only see the switching (without animation) when drawer is fully open or fully closed.

mActionBarDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, mToolbar, R.string.open, R.string.closed) {
            @Override
            public void onDrawerSlide(View drawerView, float slideOffset) {
                  //super.onDrawerSlide(drawerView, slideOffset);
            }
        };
mDrawerLayout.setDrawerListener(mActionBarDrawerToggle);
Fescue answered 26/11, 2014 at 17:1 Comment(6)
I've tried it like this but back arrow is visible after screen rotation. Open the drawer, rotate the screen, close drawer - a back arrow will be visible instead of hamburger.Pup
I cannot reproduce. Seems other code is related to this case.Fescue
I'm calling mDrawerToggle.syncState() in onPostCreate and invalidateOptionsMenu() in onDrawerOpened/Closed. Do you also call these methods?Pup
Without mDrawerToggle.syncState() the hamburger icon doesn't even show, invalidateOptionsMenu changes nothing. Could you please post entire code (or sample sample project on GitHub).Pup
@Pup As promised, here is layout pastebin.com/7PTsbN5D and MainActivity pastebin.com/4r2vRSHVFescue
Ok maybe my question is not clear. I want to control whether to show back arrow or hamburger with setDisplayHomeAsUpEnabled. I don't want it to change from hamburger to back arrow automatically. Look at the Gmail app. In the main activity the hamburger is displayed (no matter if drawer is opened or not). When you are in activity with email only back arrow is displayed. That's what I want to achieve and currently I've not found better way except that I've posted in the question.Pup
U
1

If you don't want the animation, don't use ActionBarDrawerToggle. Use the code below instead.

toolbar.setNavigationIcon(R.drawable.ic_menu);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    drawer.openDrawer(GravityCompat.START);
                }
            });
Unkind answered 28/10, 2016 at 10:51 Comment(2)
Please read the question. It says: "My question is how to properly disable this animation and show either hamburger or back arrow using getSupportActionBar().setDisplayHomeAsUpEnabled(true);"Pup
You can set the hamburger or back arrow icon using Toolbar.setNavigationIcon() and the icons from google/material-design-iconsFascinate
C
0

To remove the hamberger menu animation, you can do like:

 ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, mDrawer,  mToolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);

 toggle.setDrawerSlideAnimationEnabled(false); 
Ceramics answered 30/4, 2019 at 9:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.