Get reference to drawer toggle in support actionbar

I use ShowcaseView library for app tutorial. I need to get a reference to Navigation Drawer toggle button(aka "burger button"):

enter image description here

I use Toolbar as Actionbar, and I have no idea how to get this button. Usually to toggle drawer I use this:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == android.R.id.home) {
            toggleDrawer(Gravity.START);
        }
}

But when I use Device Monitor to make snapshot of the screen, there's no view with id "home".

Any suggestions?


Solution 1:

That is what's called the navigation button, and it's actually an ImageButton nested inside the Toolbar. Unfortunately, there is no public method or field by which to get reference to it, so we have to go roundabout.

There are several different approaches to this, of varying degrees of effectiveness and prudence. Take your pick.


Iterative

If you have control over it, and are able to set the toggle first thing, then directly afterward the navigation button should be the first (and possibly only) ImageButton child of the Toolbar. If you're confident that it is, then this is probably the most straightforward method.

Java

static ImageButton getNavigationButton(Toolbar toolbar) {
    for (int i = 0; i < toolbar.getChildCount(); ++i) {
        final View child = toolbar.getChildAt(i);
        if (child instanceof ImageButton) {
            return (ImageButton) child;
        }
    }
    return null;
}

Kotlin

val Toolbar.navigationButton: ImageButton?
    get() =
        children.firstOrNull { it is ImageButton } as ImageButton?

Reflective

This method has the advantage of absolute certainty. However, it is reflection, so, ya know, whatever your thoughts are on that.

Java

static ImageButton getNavigationButton(Toolbar toolbar) {
    try {
        final Field mNavButtonView =
                Toolbar.class.getDeclaredField("mNavButtonView");
        mNavButtonView.setAccessible(true);
        return (ImageButton) mNavButtonView.get(toolbar);
    } catch (Exception e) {
        return null;
    }
}

Kotlin

val Toolbar.navigationButton: ImageButton?
    get() =
        try {
            Toolbar::class.java
                .getDeclaredField("mNavButtonView").apply {
                    isAccessible = true
                }.get(this) as ImageButton?
        } catch (e: Exception) {
            null
        }

Find by Content Description

This is the first of several methods that accomplish the task by setting some property of the navigation button with a special value. This one temporarily sets its content description to a unique value, and utilizes the ViewGroup#findViewsWithText() method to look for it before restoring the original description.

This and the Find by Tag example both use this string resource, the value of which can be whatever you like, really:

<string name="toolbar_navigation_button_locator">ToolbarNavigationButtonLocator</string>

Java

static ImageButton getNavigationButton(Toolbar toolbar) {
    final CharSequence originalDescription =
            toolbar.getNavigationContentDescription();
    final CharSequence locator =
            toolbar.getResources()
                    .getText(R.string.toolbar_navigation_button_locator);
    toolbar.setNavigationContentDescription(locator);
    final ArrayList<View> views = new ArrayList<>();
    toolbar.findViewsWithText(
            views,
            locator,
            View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
    toolbar.setNavigationContentDescription(originalDescription);
    for (View view : views) {
        if (view instanceof ImageButton) {
            return (ImageButton) view;
        }
    }
    return null;
}

Kotlin

val Toolbar.navigationButton: ImageButton?
    get() {
        val originalDescription = navigationContentDescription
        val locator =
            resources.getText(R.string.toolbar_navigation_button_locator)
        navigationContentDescription = locator
        val views = ArrayList<View>()
        findViewsWithText(
            views,
            locator,
            View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION
        )
        navigationContentDescription = originalDescription
        return views.firstOrNull { it is ImageButton } as ImageButton?
    }

Find by Tag

This method takes advantage of being able to style the navigation button through the toolbarNavigationButtonStyle theme attribute. In the specified style, we set the android:tag attribute to our unique locator string, and use the View#findViewWithTag() method to grab it at runtime.

<style name="Theme.YourApp" parent="...">
    ...
    <item name="toolbarNavigationButtonStyle">@style/Widget.App.Button.Navigation</item>
</style>

<style name="Widget.App.Button.Navigation" parent="Widget.AppCompat.Toolbar.Button.Navigation">
    <item name="android:tag">@string/toolbar_navigation_button_locator</item>
</style>

Java

static ImageButton getNavigationButton(Toolbar toolbar) {
    final CharSequence tag =
        toolbar.getResources()
                .getText(R.string.toolbar_navigation_button_locator);
    return toolbar.findViewWithTag(tag);
}

Kotlin

val Toolbar.navigationButton: ImageButton?
    get() =
        findViewWithTag(
            resources
                .getText(R.string.toolbar_navigation_button_locator)
        )

Find by ID

Using the same styling technique as the tag method, we can instead set the android:id attribute, and possibly get this working with the familiar findViewById() functionality. However, if they ever do set an ID on that button – even if just for internal use – this will very likey fail, or break something in the Toolbar.

We first define an ID to assign the button:

<item name="navigation_button" type="id" />

Then alter the previous theme setup to set the ID rather than the tag:

<style name="Theme.YourApp" parent="...">
    ...
    <item name="toolbarNavigationButtonStyle">@style/Widget.App.Button.Navigation</item>
</style>

<style name="Widget.App.Button.Navigation" parent="Widget.AppCompat.Toolbar.Button.Navigation">
    <item name="android:id">@id/navigation_button</item>
</style>

Just for completeness' sake:

Java

static ImageButton getNavigationButton(Toolbar toolbar) {
    return toolbar.findViewById(R.id.navigation_button);
}

Kotlin

val Toolbar.navigationButton: ImageButton?
    get() = findViewById(R.id.navigation_button)

Drawable Callback

This one takes advantage of the fact that an ImageButton will set itself as its source Drawable's Callback. We create a throwaway Drawable to temporarily set as the navigation icon, check if the Callback object is our ImageButton, and restore the original icon.

This one's more of an outside-the-box, proof-of-concept-type thing, I'd say.

Java

static ImageButton getNavigationButton(Toolbar toolbar) {
    final Drawable originalIcon = toolbar.getNavigationIcon();
    final ColorDrawable temporaryDrawable = new ColorDrawable(0);
    toolbar.setNavigationIcon(temporaryDrawable);
    Object callback = temporaryDrawable.getCallback();
    toolbar.setNavigationIcon(originalIcon);
    if (callback instanceof ImageButton) {
        return (ImageButton) callback;
    }
    else {
        return null;
    }
}

Kotlin

val Toolbar.navigationButton: ImageButton?
    get() {
        val originalIcon: Drawable? = navigationIcon
        val temporaryDrawable = ColorDrawable(0)
        navigationIcon = temporaryDrawable
        val callback: Any? = temporaryDrawable.callback
        navigationIcon = originalIcon
        return if (callback is ImageButton) {
            callback
        } else {
            null
        }
    }

Notes:

  • The navigation button is instantiated dynamically on demand. This means that something must have set some navigation button property before you can find it, whether that something is a theme setting, or some code of your own. This isn't a problem for the Find by Content Description, Find by Tag, and Drawable Callback options, as they begin by setting such a property. For the Iterative and Reflective methods, however, you might need to take care with your timing.

  • Previous revisions of this answer assumed that some setups might be using the Toolbar's logo for the toggle, in lieu of the navigation button. This is highly unlikely, and so its mention is moved to this footnote, if only to retain the knowledge somewhere accessible.

    • The logo is an ImageView, and the same advice in the Iterative option applies.
    • The field name for the View is mLogoView, and the Reflective method can be altered to look for that.
    • The Toolbar class offers the setLogoDescription() method, which can be used with the Find by Content Description method to find the logo instead.
    • There is also the setLogo() method to utilize with the Drawable Callback technique.
    • The Find by Tag option is inapplicable to the logo View.