导航架构组件 - 如何使用导航控制器设置/更改自定义后退或汉堡图标?

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 并使用AppCompatActivitydrawerToggleDelegate 属性来更改图标。这里,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

使用导航架构组件添加(而不是替换)片段

如何使用导航架构组件创建 BottomSheetDialogFragment?

使用导航架构时如何设置“允许重新排序”?

如何同时使用导航抽屉和底部导航 - 导航架构组件