Reputation: 2423
I am trying to implement the newly introduced Navigation Architecture Component provided with Jetpack. as far it's very cool and useful for managing navigation flow of your app.
I have already setup the basic navigation including drawer layout with the toolbar in MainActivity like this:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navController = Navigation.findNavController(this, R.id.mainNavFragment)
// Set up ActionBar
setSupportActionBar(toolbar)
NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
// Set up navigation menu
navigationView.setupWithNavController(navController)
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(Navigation.findNavController(this, R.id.mainNavFragment), drawerLayout)
}
}
With this layout:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:navigationIcon="@drawable/ic_home_black_24dp"/>
</android.support.design.widget.AppBarLayout>
<fragment
android:id="@+id/mainNavFragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_main"/>
</LinearLayout>
It works fins. But, the real question is when provided custom design for an app,
how can I set custom icon for hamburger or the back icon?
which is as of now, being handled by NavigatoinController itself.
I already tried options below, but it doesn't work:
app:navigationIcon="@drawable/ic_home_black_24dp" //1
supportActionBar.setHomeAsUpIndicator(R.drawable.ic_android_black_24dp) //2
Thanks!
Upvotes: 10
Views: 10926
Reputation: 4712
I don't know since when this is possible but I just found out, that you can call toolbar.setNavigationIcon(R.drawable.your_icon)
after calling toolbar.setupWithNavController(findNavController())
. This will remove the default DrawerArrowDrawable and instantly override it with your custom drawable. There is no need to rewrite any sdk code or do some crazy other stuff
fun Fragment.initToolbarWithCustomDrawable(@DrawableRes resId: Int) {
val toolbar = requireView().findViewById<MaterialToolbar>(R.id.your_toolbar)
with(toolbar) {
setupWithNavController(findNavController(), AppBarConfiguration(findNavController().graph))
setNavigationIcon(resId)
}
}
fun Fragment.initToolbarWithCustomDrawable(
toolbar: Toolbar
@DrawableRes resId: Int
) {
with(toolbar) {
setupWithNavController(findNavController(), AppBarConfiguration(findNavController().graph))
setNavigationIcon(resId)
}
}
Upvotes: -1
Reputation: 2124
Actually, I don't know this is a feature or a bug of Google.
Before giving my solution to change a custom back icon. Let see how Google implemented.
Everything starts when we call :
val navController = findNavController(R.id.nav_host_fragment)
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.homeFragment,
R.id.categoryFragment,
R.id.cartFragment,
R.id.myAccountFragment,
R.id.moreFragment
),
)
toolbar.setupWithNavController(navController, appBarConfiguration)
setupWithNavController() is an extension of Toolbar.
fun Toolbar.setupWithNavController(
navController: NavController,
configuration: AppBarConfiguration = AppBarConfiguration(navController.graph)
) {
NavigationUI.setupWithNavController(this, navController, configuration)
}
And in the NavigationUI, you can see that Google just listen the change of destination and set a click event when the user click on back button.
public static void setupWithNavController(@NonNull Toolbar toolbar,
@NonNull final NavController navController,
@NonNull final AppBarConfiguration configuration) {
navController.addOnDestinationChangedListener(
new ToolbarOnDestinationChangedListener(toolbar, configuration));
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
navigateUp(navController, configuration);
}
});
}
And go more detail, you can see this function:
private void setActionBarUpIndicator(boolean showAsDrawerIndicator) {
boolean animate = true;
if (mArrowDrawable == null) {
mArrowDrawable = new DrawerArrowDrawable(mContext);
// We're setting the initial state, so skip the animation
animate = false;
}
setNavigationIcon(mArrowDrawable, showAsDrawerIndicator
? R.string.nav_app_bar_open_drawer_description
: R.string.nav_app_bar_navigate_up_description);
float endValue = showAsDrawerIndicator ? 0f : 1f;
if (animate) {
float startValue = mArrowDrawable.getProgress();
if (mAnimator != null) {
mAnimator.cancel();
}
mAnimator = ObjectAnimator.ofFloat(mArrowDrawable, "progress",
startValue, endValue);
mAnimator.start();
} else {
mArrowDrawable.setProgress(endValue);
}
}
and here is setNavigationIcon:
@Override
protected void setNavigationIcon(Drawable icon,
@StringRes int contentDescription) {
Toolbar toolbar = mToolbarWeakReference.get();
if (toolbar != null) {
boolean useTransition = icon == null && toolbar.getNavigationIcon() != null;
toolbar.setNavigationIcon(icon);
toolbar.setNavigationContentDescription(contentDescription);
if (useTransition) {
TransitionManager.beginDelayedTransition(toolbar);
}
}
}
I just saw that every time a destination change, and the destination is not a root page. Google creates a DrawerArrowDrawable object and then Google set this icon for navigation Icon. I think it is reason why Toolbar doesn't show our custom icon.
At this point, I think we have some solutions to deal with it. One simple solution is add addOnDestinationChangedListener() method in onCreate() method of Activity and whenever the user changes to new page (destination) where we will check and decide whether we will show the custom back icon or not. Like this:
val navController = findNavController(R.id.nav_host_fragment)
navController.addOnDestinationChangedListener { _, destination, _ ->
(appBarConfiguration.topLevelDestinations.contains(destination.id)) {
toolbar.navigationIcon = null
} else {
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white)
}
}
Upvotes: 2
Reputation: 51
I faced the same problem. I just rewrote some class from the SDK:
class ToolbarOnDestinationChangedListener(toolbar: Toolbar) : OnDestinationChangedListener {
private val toolbarWeakReference = WeakReference(toolbar)
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
if (toolbarWeakReference.get() == null) {
controller.removeOnDestinationChangedListener(this)
return
}
if (destination is FloatingWindow) {
return
}
destination.label?.let {
val title = pickTitleFromArgs(it, arguments)
setTitle(title)
}
}
private fun pickTitleFromArgs(label: CharSequence, arguments: Bundle?): StringBuffer {
val title = StringBuffer()
val matcher = Pattern.compile("\\{(.+?)\\}").matcher(label)
while (matcher.find()) {
val argName = matcher.group(1)
if (arguments != null && arguments.containsKey(argName)) {
matcher.appendReplacement(title, "")
title.append(arguments[argName].toString())
} else {
error("Could not find $argName in $arguments to fill label $label")
}
}
matcher.appendTail(title)
return title
}
private fun setTitle(title: CharSequence) {
toolbarWeakReference.get()?.let {
it.title = title
}
}
}
And following extension:
fun Toolbar.setNavController(navController: NavController) {
navController.addOnDestinationChangedListener(ToolbarOnDestinationChangedListener(this))
toolbar.setNavigationOnClickListener { navController.navigateUp() }
}
Use:
//Fragment
toolbar.setNavController(findNavController())
This code does not control icons. But it considers label, args, destination changes. You should set icons with XML.
Upvotes: 1
Reputation: 1103
Since the style AppTheme uses the style Widget.AppCompat.DrawerArrowToggle for the navigationUI hamburger and back button icons, You need to create your custom style by inheriting from the Widget.AppCompat.DrawerArrowToggle.
I'll try to exhaust all the possible options you have and what each one of them does:
<style name="CustomNavigationIcons" parent="Widget.AppCompat.DrawerArrowToggle">
<!-- The drawing color for the bars -->
<item name="color">@color/colorGrey</item>
<!-- The total size of the drawable -->
<item name="drawableSize">64dp</item>
<!-- The thickness (stroke size) for the bar paint -->
<item name="thickness">3dp</item>
<!-- Whether bars should rotate or not during transition -->
<item name="spinBars">true</item>
<!-- The max gap between the bars when they are parallel to each other -->
<item name="gapBetweenBars">4dp</item>
<!-- The length of the bars when they are parallel to each other -->
<item name="barLength">27dp</item>
<!-- The length of the shaft when formed to make an arrow -->
<item name="arrowShaftLength">23dp</item>
<!-- The length of the arrow head when formed to make an arrow -->
<item name="arrowHeadLength">7dp</item>
</style>
Finally, you have to make sure AppTheme now uses your custom style as the DrawerArrowToggle, like so:
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<!-- Other styles -->
<item name="drawerArrowStyle">@style/CustomNavigationIcons</item>
</style>
Upvotes: 1
Reputation: 71
Navigation Component uses DrawerArrowDrawable as the underlying implementation, which actually animates between hamburger icon and back arrow icon. The appearance of both icons can be modified to a certain degree via styles, see
How to style the DrawerArrowToggle from Android appcompat v7 21 library
For our app, we used the following attributes to obtain a long thin arrow:
<style name="ToolbarArrow" parent="Widget.AppCompat.DrawerArrowToggle">
<item name="color">@color/primary</item>
<item name="drawableSize">32dp</item>
<item name="arrowShaftLength">22dp</item>
<item name="thickness">1.7dp</item>
<item name="arrowHeadLength">8dp</item>
</style>
This style must be set in your app's theme, like this:
<item name="drawerArrowStyle">@style/ToolbarArrow</item>
Upvotes: 7
Reputation: 1888
I have faced the same issue recently. I wanted to use the default setup with NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
and also didn't want to write the logic in every fragment. And that's the use case of having a navigation component in a project. After digging the source code of the navigation component I came up with the following solution:
findNavController(R.id.nav_host_fragment)
.addOnDestinationChangedListener { _, destination, _ ->
when (destination.id) {
R.id.nav_home,
R.id.nav_gallery -> {
drawerToggleDelegate!!
.setActionBarUpIndicator(
ContextCompat.getDrawable(
this,
R.drawable.custom_hamburger
),
androidx.navigation.ui.R.string.nav_app_bar_open_drawer_description
)
}
else -> {
drawerToggleDelegate!!
.setActionBarUpIndicator(
ContextCompat.getDrawable(
this,
R.drawable.custom_back
),
androidx.navigation.ui.R.string.nav_app_bar_navigate_up_description
)
}
}
}
I have added DestinationChangedListener
to the navController
and used drawerToggleDelegate
property of the AppCompatActivity
to change the icon. Here, R.id.nav_home
& R.id.nav_gallery
are my top-level destinations; for which the custom hamburger will be shown and for others, the custom back icon will be shown.
The only drawback of this approach is that you will lose the default animation.
But you can implement your logic here pretty easily to get the animation effect.
Upvotes: 4
Reputation: 183
I had the same issue with navigation version 1.0.0-alpha08
. I have solved it by re-implementing setupActionBarWithNavController to provide the custom behaviour that I needed.
setDisplayShowTitleEnabled(false) //Disable the default title
container.findNavController().addOnDestinationChangedListener { _, destination: NavDestination, _ ->
//Set the toolbar text (I've placed a TextView within the AppBar, which is being referenced here)
toolbar_text.text = destination.label
//Set home icon
supportActionBar?.apply {
setDisplayHomeAsUpEnabled((destination.id in NO_HOME_ICON_FRAGMENTS).not())
setHomeAsUpIndicator(if (destination.id in TOP_LEVEL_FRAGMENTS) R.drawable.app_close_ic else 0)
}
}
All you need to do is add an onDestinationChangedListener to your nav controller, and set the title and icon for each destination.
This code should be placed within the onCreate() method within the activity that contains your nav graph, and NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
should be removed from your code.
Note this will remove the default functionality for a navigation drawer.
I hope this helps!
Upvotes: 14
Reputation: 56
I'm having the same issue using navigation version 1.0.0-alpha07
. I've found the following workaround:
In my activity, I set the toolbar view both as the support action bar and with the nav controller:
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
toolbar.setupWithNavController(navController, appBarConfiguration)
Then in onViewCreated
of my fragment where I want to replace the arrow icon, I set the icon and set up as enabled (it's probably possible to do this in onAttached
):
(requireActivity() as AppCompatActivity).supportActionBar?.apply {
setHomeAsUpIndicator(R.drawable.ic_close_24dp)
setDisplayHomeAsUpEnabled(true)
}
The navigation component still seems to correctly handle the backstack, animations, and removing the icon on the start fragment when I do this. Hopefully before leaving alpha the navigation component supports custom up icons without having to resort to the old-style action bar API.
Upvotes: 2