导航架构组件 - 如何使用导航控制器设置/更改自定义后退或汉堡图标?
Posted
技术标签:
【中文标题】导航架构组件 - 如何使用导航控制器设置/更改自定义后退或汉堡图标?【英文标题】:Navigation Architecture Component - how to set/change custom back or hamburger icon with navigation controller? 【发布时间】:2019-04-03 07:29:54 【问题描述】:我正在尝试实现Jetpack 提供的新引入的Navigation Architecture Component。就管理应用的导航流程而言,它非常酷且有用。
我已经使用 MainActivity 中的工具栏设置了基本导航,包括抽屉布局,如下所示:
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)
使用这种布局:
<LinearLayout
android:layout_
android:layout_
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_
android:layout_
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_
android:layout_
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_
android:layout_
app:defaultNavHost="true"
app:navGraph="@navigation/nav_main"/>
</LinearLayout>
它工作鳍。但是,真正的问题是何时为应用提供自定义设计,
如何为汉堡或后退图标设置自定义图标?
目前由 NavigatoinController 自己处理。
我已经尝试了以下选项,但它不起作用:
app:navigationIcon="@drawable/ic_home_black_24dp" //1
supportActionBar.setHomeAsUpIndicator(R.drawable.ic_android_black_24dp) //2
谢谢!
【问题讨论】:
试试这个***.com/questions/52172111/… 嗨@NileshRathod,请阅读问题,图标显示正确。但我想要的是使用我自己的图标矢量/png 【参考方案1】:导航版本1.0.0-alpha08
也有同样的问题。我已经通过重新实现 setupActionBarWithNavController 来提供我需要的自定义行为来解决它。
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)
您需要做的就是将 onDestinationChangedListener 添加到您的导航控制器,并为每个目的地设置标题和图标。
此代码应放在包含导航图的活动的 onCreate() 方法中,并且应从代码中删除 NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
。
请注意,这将删除导航抽屉的默认功能。
我希望这会有所帮助!
【讨论】:
我认为这是最好的解决方案,现在应该被接受为正确答案。 我认为@bdemuth 提供的解决方案比这个更优雅【参考方案2】:导航组件使用 DrawerArrowDrawable 作为底层实现,它实际上在汉堡图标和返回箭头图标之间进行动画处理。两个图标的外观都可以通过样式进行一定程度的修改,见
How to style the DrawerArrowToggle from Android appcompat v7 21 library
对于我们的应用,我们使用以下属性来获得一个细长的箭头:
<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>
必须在应用的主题中设置此样式,如下所示:
<item name="drawerArrowStyle">@style/ToolbarArrow</item>
【讨论】:
【参考方案3】:我最近遇到了同样的问题。我想使用NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
的默认设置,也不想在每个片段中编写逻辑。这就是在项目中使用导航组件的用例。在挖掘了导航组件的源代码后,我想出了以下解决方案:
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
)
我已将DestinationChangedListener
添加到navController
并使用AppCompatActivity
的drawerToggleDelegate
属性来更改图标。这里,R.id.nav_home
& R.id.nav_gallery
是我的***目的地;将为其显示自定义汉堡包,对于其他人,将显示自定义后退图标。
这种方法的唯一缺点是您将丢失默认动画。
但是你可以很容易地在这里实现你的逻辑来获得动画效果。
【讨论】:
你从哪里得到drawerToggleDelegate
?
@Angelina drawerToggleDelete
来自AppCompatActivity
【参考方案4】:
我在使用导航版本1.0.0-alpha07
时遇到了同样的问题。我找到了以下解决方法:
在我的活动中,我将工具栏视图都设置为作为支持操作栏和导航控制器:
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
toolbar.setupWithNavController(navController, appBarConfiguration)
然后在我想要替换箭头图标的片段的onViewCreated
中,我设置图标并设置为启用(在onAttached
中可能可以这样做):
(requireActivity() as AppCompatActivity).supportActionBar?.apply
setHomeAsUpIndicator(R.drawable.ic_close_24dp)
setDisplayHomeAsUpEnabled(true)
当我这样做时,导航组件似乎仍然可以正确处理后台堆栈、动画和删除开始片段上的图标。希望在离开 alpha 之前,导航组件支持自定义向上图标,而不必求助于旧式操作栏 API。
【讨论】:
这个解决方案不好。有时(但仅有时)您可以在看到自己的图标之前看到导航库用作其默认实现的 DrawerArrow 图标。不幸的是,这是不可接受的。【参考方案5】:其实我不知道这是谷歌的功能还是bug。
在给出更改自定义后退图标的解决方案之前。让我们看看 Google 是如何实现的。
一切从我们调用开始:
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() 是 Toolbar 的扩展。
fun Toolbar.setupWithNavController(
navController: NavController,
configuration: AppBarConfiguration = AppBarConfiguration(navController.graph)
)
NavigationUI.setupWithNavController(this, navController, configuration)
在 NavigationUI 中,您可以看到 Google 只是监听目的地的变化,并在用户点击返回按钮时设置点击事件。
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);
);
再详细点,你可以看到这个函数:
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);
这里是 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);
我刚刚看到每次目的地更改时,目的地都不是根页面。 Google 创建了一个 DrawerArrowDrawable 对象,然后 Google 将此图标设置为导航图标。我认为这就是工具栏不显示我们的自定义图标的原因。
在这一点上,我认为我们有一些解决方案来处理它。 一个简单的解决方案是在 Activity 的 onCreate() 方法中添加 addOnDestinationChangedListener() 方法,并且每当用户更改到新页面(目标)时,我们将检查并决定是否显示自定义后退图标。像这样:
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)
【讨论】:
【参考方案6】:由于样式 AppTheme 使用样式 Widget.AppCompat.DrawerArrowToggle 作为 navigationUI 汉堡和后退按钮图标,您需要通过从 Widget.AppCompat.DrawerArrowToggle 继承来创建自定义样式。
我将尝试用尽所有您拥有的可能选项以及每个选项的作用:
<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>
最后,您必须确保 AppTheme 现在使用您的自定义样式作为 DrawerArrowToggle,如下所示:
<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>
【讨论】:
【参考方案7】:我遇到了同样的问题。我只是重写了 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
以及以下扩展名:
fun Toolbar.setNavController(navController: NavController)
navController.addOnDestinationChangedListener(ToolbarOnDestinationChangedListener(this))
toolbar.setNavigationOnClickListener navController.navigateUp()
用途:
//Fragment
toolbar.setNavController(findNavController())
此代码不控制图标。但它会考虑标签、参数、目的地的变化。您应该使用 XML 设置图标。
【讨论】:
【参考方案8】:我不知道什么时候可以做到这一点,但我刚刚发现,您可以在拨打toolbar.setupWithNavController(findNavController())
之后拨打toolbar.setNavigationIcon(R.drawable.your_icon)
。这将删除默认的 DrawerArrowDrawable 并立即用您的自定义 drawable 覆盖它。无需重写任何 sdk 代码或做一些疯狂的其他事情
带有默认 findViewById 的代码
fun Fragment.initToolbarWithCustomDrawable(@DrawableRes resId: Int)
val toolbar = requireView().findViewById<MaterialToolbar>(R.id.your_toolbar)
with(toolbar)
setupWithNavController(findNavController(), AppBarConfiguration(findNavController().graph))
setNavigationIcon(resId)
没有findViewById的代码
fun Fragment.initToolbarWithCustomDrawable(
toolbar: Toolbar
@DrawableRes resId: Int
)
with(toolbar)
setupWithNavController(findNavController(), AppBarConfiguration(findNavController().graph))
setNavigationIcon(resId)
【讨论】:
以上是关于导航架构组件 - 如何使用导航控制器设置/更改自定义后退或汉堡图标?的主要内容,如果未能解决你的问题,请参考以下文章
如何在更改全屏/上方导航时使用导航组件 navhostfragment