23

My question is the same as this question (which is not a duplicate of this question).

The only answer to that question does not work for me as, rather than changing the default hamburger icon to the left of the activity's title, it just adds an additional hamburger icon to the right of my activity's title.

So how do I actually get this:

Android hamburger icon with badge counter

I've been poking around at it all day, but have got nowhere.

I see that Toolbar has a setNavigationIcon(Drawable drawable) method. Ideally, I would like to use a layout (that contains the hamburger icon and the badge view) instead of a Drawable, but I'm not sure if/how this is achievable - or if there is a better way?

NB - This isn't a question about how to create the badge view. I have already created that and have implemented it on the nav menu items themselves. So I am now just needing to add a similar badge view to the default hamburger icon.

Andrew T.
  • 4,637
  • 8
  • 40
  • 60
ban-geoengineering
  • 17,070
  • 21
  • 157
  • 242

1 Answers1

79

Since version 24.2.0 of the support library, the v7 version of ActionBarDrawerToggle has offered the setDrawerArrowDrawable() method as a means to customize the toggle icon. DrawerArrowDrawable is the class that provides that default icon, and it can be subclassed to alter it as needed.

As an example, the BadgeDrawerArrowDrawable class overrides the draw() method to add a basic red and white badge after the superclass draws itself. This allows the hamburger-arrow animation to be preserved underneath.

import android.content.Context;
import android.graphics.Color;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.support.v7.graphics.drawable.DrawerArrowDrawable;
import java.util.Objects;

public class BadgeDrawerArrowDrawable extends DrawerArrowDrawable {

    // Fraction of the drawable's intrinsic size we want the badge to be.
    private static final float SIZE_FACTOR = .3f;
    private static final float HALF_SIZE_FACTOR = SIZE_FACTOR / 2;

    private Paint backgroundPaint;
    private Paint textPaint;
    private String text;
    private boolean enabled = true;

    public BadgeDrawerArrowDrawable(Context context) {
        super(context);

        backgroundPaint = new Paint();
        backgroundPaint.setColor(Color.RED);
        backgroundPaint.setAntiAlias(true);

        textPaint = new Paint();
        textPaint.setColor(Color.WHITE);
        textPaint.setAntiAlias(true);
        textPaint.setTypeface(Typeface.DEFAULT_BOLD);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setTextSize(SIZE_FACTOR * getIntrinsicHeight());
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);

        if (!enabled) {
            return;
        }

        final Rect bounds = getBounds();
        final float x = (1 - HALF_SIZE_FACTOR) * bounds.width();
        final float y = HALF_SIZE_FACTOR * bounds.height();
        canvas.drawCircle(x, y, SIZE_FACTOR * bounds.width(), backgroundPaint);

        if (text == null || text.length() == 0) {
            return;
        }

        final Rect textBounds = new Rect();
        textPaint.getTextBounds(text, 0, text.length(), textBounds);
        canvas.drawText(text, x, y + textBounds.height() / 2, textPaint);
    }

    public void setEnabled(boolean enabled) {
        if (this.enabled != enabled) {
            this.enabled = enabled;
            invalidateSelf();
        }
    }

    public boolean isEnabled() {
        return enabled;
    }

    public void setText(String text) {
        if (!Objects.equals(this.text, text)) {
            this.text = text;
            invalidateSelf();
        }
    }

    public String getText() {
        return text;
    }

    public void setBackgroundColor(int color) {
        if (backgroundPaint.getColor() != color) {
            backgroundPaint.setColor(color);
            invalidateSelf();
        }
    }

    public int getBackgroundColor() {
        return backgroundPaint.getColor();
    }

    public void setTextColor(int color) {
        if (textPaint.getColor() != color) {
            textPaint.setColor(color);
            invalidateSelf();
        }
    }

    public int getTextColor() {
        return textPaint.getColor();
    }
}

An instance of this can be set on the toggle any time after it's instantiated, and the badge's properties set directly on the drawable as needed.

As the OP noted below, the Context used for the custom DrawerArrowDrawable should be obtained with ActionBar#getThemedContext() or Toolbar#getContext() to ensure the correct style values are used. For example:

private ActionBarDrawerToggle toggle;
private BadgeDrawerArrowDrawable badgeDrawable;
...

toggle = new ActionBarDrawerToggle(this, ...);
badgeDrawable = new BadgeDrawerArrowDrawable(getSupportActionBar().getThemedContext());

toggle.setDrawerArrowDrawable(badgeDrawable);
badgeDrawable.setText("1");
...

screenshots


To simplify things a bit, it might be preferable to subclass ActionBarDrawerToggle as well, and handle everything through the toggle instance.

import android.app.Activity;
import android.content.Context;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.widget.Toolbar;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class BadgeDrawerToggle extends ActionBarDrawerToggle {

    private BadgeDrawerArrowDrawable badgeDrawable;

    public BadgeDrawerToggle(Activity activity, DrawerLayout drawerLayout,
                             int openDrawerContentDescRes,
                             int closeDrawerContentDescRes) {
        super(activity, drawerLayout, openDrawerContentDescRes,
              closeDrawerContentDescRes);
        init(activity);
    }

    public BadgeDrawerToggle(Activity activity, DrawerLayout drawerLayout,
                             Toolbar toolbar, int openDrawerContentDescRes,
                             int closeDrawerContentDescRes) {
        super(activity, drawerLayout, toolbar, openDrawerContentDescRes,
              closeDrawerContentDescRes);
        init(activity);
    }

    private void init(Activity activity) {
        Context c = getThemedContext();
        if (c == null) {
            c = activity;
        }
        badgeDrawable = new BadgeDrawerArrowDrawable(c);
        setDrawerArrowDrawable(badgeDrawable);
    }

    public void setBadgeEnabled(boolean enabled) {
        badgeDrawable.setEnabled(enabled);
    }

    public boolean isBadgeEnabled() {
        return badgeDrawable.isEnabled();
    }

    public void setBadgeText(String text) {
        badgeDrawable.setText(text);
    }

    public String getBadgeText() {
        return badgeDrawable.getText();
    }

    public void setBadgeColor(int color) {
        badgeDrawable.setBackgroundColor(color);
    }

    public int getBadgeColor() {
        return badgeDrawable.getBackgroundColor();
    }

    public void setBadgeTextColor(int color) {
        badgeDrawable.setTextColor(color);
    }

    public int getBadgeTextColor() {
        return badgeDrawable.getTextColor();
    }

    private Context getThemedContext() {
        // Don't freak about the reflection. ActionBarDrawerToggle
        // itself is already using reflection internally.
        try {
            Field mActivityImplField = ActionBarDrawerToggle.class
                .getDeclaredField("mActivityImpl");
            mActivityImplField.setAccessible(true);
            Object mActivityImpl = mActivityImplField.get(this);
            Method getActionBarThemedContextMethod = mActivityImpl.getClass()
                .getDeclaredMethod("getActionBarThemedContext");
            return (Context) getActionBarThemedContextMethod.invoke(mActivityImpl);
        }
        catch (Exception e) {
            return null;
        }
    }
}

With this, the custom badge drawable will be set automatically, and everything toggle-related can be managed through a single object.

BadgeDrawerToggle is a drop-in replacement for ActionBarDrawerToggle, and its constructors are exactly the same.

private BadgeDrawerToggle badgeToggle;
...

badgeToggle = new BadgeDrawerToggle(this, ...);
badgeToggle.setBadgeText("1");
...
Mike M.
  • 37,502
  • 8
  • 98
  • 92
  • 2
    This is still working in 2019 after migrating to AndroidX, but don't forget to add the following in your Proguard config: `-keep class androidx.appcompat.app.ActionBarDrawerToggle { *; }` `-keep class androidx.appcompat.app.ActionBarDrawerToggle$Delegate { *; }` – Eric Sellin Jul 24 '19 at 15:47
  • Is it possible to make this work with `onSupportNavigateUp(): Boolean` still being called? – AndroidKotlinNoob Mar 02 '22 at 10:24
  • @AndroidKotlinNoob It's unclear what you're asking, as this solution does nothing with that `AppCompatActivity` method. If you're actually asking about the Jetpack Navigation component, then that really should be a separate post, but I will mention that the last time I checked, it wasn't possible because the `ActionBarDrawerToggle` that Navigation uses is inaccessible, IIRC. Things may have changed since then, though. I don't use Navigation myself. – Mike M. Mar 02 '22 at 17:16
  • I tried implementing this and it no longer called the `AppCompatActivity` function. – AndroidKotlinNoob Mar 03 '22 at 15:21
  • @AndroidKotlinNoob Sorry, but I really don't see how that's possible. This is basically just setting a drawable on an `ImageButton`. Nobody's mentioned such an issue in the last ~4.5 years, so I would have to guess that your issue is specific to your setup. – Mike M. Mar 03 '22 at 15:29
  • Okay, I found the problem. When passing the toolbar into the constructor of `ActionBarDrawerToggle` it sets a `NavigationOnClickListener` that gets called before any of the activity methods. To prevent this I just had to pass in `null` as the Toolbar. – AndroidKotlinNoob May 30 '22 at 13:38
  • @AndroidKotlinNoob Yeah, that's just how `ActionBarDrawerToggle` works. Nothing specific to the solution here. Glad you got it figured out, though. Cheers! – Mike M. May 30 '22 at 13:45