12
votes

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?

2

2 Answers

24
votes

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.forEach {
                if (it is ImageButton) {
                    return it
                }
            }
            return null
    }
    
  • 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() {
            Toolbar::class.java
                .getDeclaredField("mNavButtonView").apply {
                    isAccessible = true
                    return get(this@navigationButton) as ImageButton?
                }
        }
    
  • Find by Content Description

    This is the first of three 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 following 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
            views.forEach { v ->
                if (v is ImageButton) {
                    return v
                }
            }
            return null
        }
    
  • 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/Toolbar.Button.Navigation.Tagged</item>
    </style>
    
    <style name="Toolbar.Button.Navigation.Tagged" 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() {
            return run {
                findViewWithTag(
                    resources
                        .getText(R.string.toolbar_navigation_button_locator)
                )
            }
        }
    
  • 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.
0
votes

In your Xml file just add a view in leftmost of toolbar/Layout, and use this view in your showcase target