简体   繁体   中英

Disable hamburger to back arrow animation on Toolbar

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?

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.

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.

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);

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();
}

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();
}

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);

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);
                }
            });

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); 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM