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"):
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
ismLogoView
, and the Reflective method can be altered to look for that. - The
Toolbar
class offers thesetLogoDescription()
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
.
- The logo is an