Add badge counter to hamburger navigation menu icon in Android
Asked Answered
S

1

25

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.

Spanish answered 9/5, 2017 at 23:1 Comment(0)
P
81

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");
...
Pled answered 10/5, 2017 at 7:30 Comment(10)
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 { *; }Flagwaving
Is it possible to make this work with onSupportNavigateUp(): Boolean still being called?Saving
@Saving 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.Pled
I tried implementing this and it no longer called the AppCompatActivity function.Saving
@Saving 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.Pled
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.Saving
@Saving Yeah, that's just how ActionBarDrawerToggle works. Nothing specific to the solution here. Glad you got it figured out, though. Cheers!Pled
@MikeM. Thank your for the answer. I have a weird problem. I use navigation component and flow to update API data. I emit and collect value from API service. When the API data count>0 , I put badge drawable on hamburger icon. the problem is, when I go to nested fragment and the app goes to background , then when I come back to it, Mainactivity calls API again, but this code" mDrawerToggle?.setDrawerArrowDrawable(badgeDrawableHamburger!!)" changes back arrow icon to hamburger in nested fragment then put badge on it. I use toolbar as actionbar , and setSupportActionBar(myToolbar).Runofthemill
@Runofthemill That's likely not specific to this solution. I've not used it much but AFAIK, the Navigation component needs to handle the toggle by itself, so it's not surprising that injecting your own behavior in there might not work quite right. I'm not even sure how you got hold of the toggle, 'cause last I checked it wasn't publicly accessible. If you mean that you're creating and setting your own toggle, I'm pretty sure you're going to have to handle syncing it properly yourself.Pled
@MikeM. you are right. This method may not be suitable for multi-fragment architecture and one activity and navigation component. Unfortunately, the native BadgeDrawable library only supports menuItemId. The solution that came to my mind. I add the badge value only for the fragments of the main page and I don't do this in the rest of the internal fragments. I didn't do any extra work on the toggle. Except for the codes written here. In addition, I do not use the action bar and use the toolbar "mDrawerToggle?.setDrawerArrowDrawable(BadgeDrawerToggle!!)" BadgeDrawerToggle same as yours.Runofthemill

© 2022 - 2024 — McMap. All rights reserved.