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

10
我正在尝试实现最新引入的Navigation Architecture Component,它是Jetpack提供的。这个组件非常酷,有用,可以管理应用程序的导航流程。
我已经设置了基本的导航,包括在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_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>

它运行良好。但是,真正的问题在于提供应用程序的自定义设计时,

我该如何为汉堡或返回图标设置自定义图标?

目前,这由NavigationController本身处理。

输入图片描述

我已尝试下面的选项,但不起作用:

app:navigationIcon="@drawable/ic_home_black_24dp" //1
supportActionBar.setHomeAsUpIndicator(R.drawable.ic_android_black_24dp) //2
谢谢!

请尝试访问此链接:https://dev59.com/SFQK5IYBdhLWcg3wQdvI#52270037 - AskNilesh
嗨@NileshRathod,请阅读问题,图标正常显示。但是我想使用自己的矢量/ PNG图标。 - Harin
顺便说一句,有人提出了一个功能请求,可能会解决这个问题:https://issuetracker.google.com/issues/121078028 - AndroidKotlinNoob
8个回答

14

我使用导航版本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)应从代码中删除。

请注意,这将删除导航抽屉的默认功能。

希望这能帮助到您!


我认为这是最好的解决方案,应该被接受为正确答案。 - Max Kachinkin
我认为@bdemuth提供的解决方案更加优雅,而不是这个。 - Kartik Shah

7
导航组件使用DrawerArrowDrawable作为基础实现,它实际上在汉堡图标和返回箭头图标之间进行动画。两个图标的外观可以通过样式进行一定程度的修改,请参见如何从Android appcompat v7 21库中设置DrawerArrowToggle的样式。对于我们的应用程序,我们使用了以下属性来获取细长的箭头:
    <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>

4

我最近也遇到了同样的问题。我想使用默认设置与 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_homeR.id.nav_gallery是我顶层目的地;对于这些目的地,将显示自定义汉堡包图标,而对于其他目的地,则将显示自定义返回图标。
这种方法唯一的缺点是您将失去默认动画效果。
但是,您可以在此处轻松实现自己的逻辑以获得动画效果。

你从哪里获取了 drawerToggleDelegate - vrgrg
@Angelina的“drawerToggleDelete”来自于“AppCompatActivity”。 - zoha131

2

实际上,我不知道这是谷歌的一个功能还是一个bug。

在提供我的更改自定义返回图标的解决方案之前,请让我们看看谷歌是如何实现的。

一切都始于我们调用:

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对象,然后将此图标设置为导航图标。我认为这就是为什么工具栏不显示我们自定义图标的原因。
目前,我认为我们有一些解决方案来处理它。一个简单的解决方案是在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)

                }
        }

2

我遇到了同样的问题,使用导航版本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图标作为其默认实现。不幸的是,这是不可接受的。 - Lukas1

1

由于样式AppTheme使用样式Widget.AppCompat.DrawerArrowToggle来设置导航UI汉堡包和返回按钮图标,因此您需要通过继承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>

1

我遇到了同样的问题。我只是从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 设置图标。

-1

我不知道从什么时候开始这是可能的,但我刚刚发现,你可以在调用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)
   }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接