如何使用导航架构组件在应用程序栏中设置标题

59

我正在尝试使用导航架构组件,并且现在在设置标题方面遇到了困难。如何以编程方式设置标题,以及它是如何工作的?

为了澄清我的问题,让我们举个例子,假设我设置了一个简单的应用程序,其中MainActivity托管导航主控制器,MainFragment有一个按钮,单击该按钮后进入DetailFragment

这与stackoverflow上多个应用栏问题中的相同代码。

MainActivity

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        // Setting up a back button
        NavController navController = Navigation.findNavController(this, R.id.nav_host);
        NavigationUI.setupActionBarWithNavController(this, navController);
    }

    @Override
    public boolean onSupportNavigateUp() {
        return Navigation.findNavController(this, R.id.nav_host).navigateUp();
    }
}

MainFragment

--->

MainFragment

(无需翻译,已是中文)
public class MainFragment extends Fragment {

    public MainFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_main, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        Button buttonOne = view.findViewById(R.id.button_one);
        buttonOne.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.detailFragment));
    }

}
DetailFragment
public class DetailFragment extends Fragment {

    public DetailFragment() {
        // Required empty public constructor
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_detail, container, false);
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:animateLayoutChanges="true"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:animateLayoutChanges="true"
        android:theme="@style/AppTheme.AppBarOverlay">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </com.google.android.material.appbar.AppBarLayout>

    <fragment
        android:id="@+id/nav_host"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="top"
        android:layout_marginTop="?android:attr/actionBarSize"
        app:defaultNavHost="true"
        app:layout_anchor="@id/bottom_appbar"
        app:layout_anchorGravity="top"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        app:navGraph="@navigation/mobile_navigation" />

    <com.google.android.material.bottomappbar.BottomAppBar
        android:id="@+id/bottom_appbar"
        android:layout_width="match_parent"
        android:layout_height="?android:attr/actionBarSize"
        android:layout_gravity="bottom" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_anchor="@id/bottom_appbar" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

导航.xml

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mobile_navigation"
    app:startDestination="@id/mainFragment">
    <fragment
        android:id="@+id/mainFragment"
        android:name="com.example.MainFragment"
        android:label="fragment_main"
        tools:layout="@layout/fragment_main" >
        <action
            android:id="@+id/toAccountFragment"
            app:destination="@id/detailFragment" />
    </fragment>
    <fragment
        android:id="@+id/detailFragment"
        android:name="com.example.DetailFragment"
        android:label="fragment_account"
        tools:layout="@layout/fragment_detail" />
</navigation>

当我启动我的应用程序时,标题为“MainActivity”。通常它会显示包含前往DetailFragment按钮的MainFragment。在DialogFragment中,我已将标题设置为:

getActivity().getSupportActionBar().setTitle("Detail");

第一个问题:MainFragment上点击按钮进入DetailFragment后,标题会变为"Detail"。但是当点击返回按钮时,标题会变为"fragment_main"。所以我在MainFragment中添加了这行代码:

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    // ...

    //Showing the title 
    Navigation.findNavController(view)
      .getCurrentDestination().setLabel("Hello");
}

现在从DetailFragment返回到MainFragment时,标题会更改为“Hello”。但这里出现了第二个问题,当我关闭应用程序并重新启动时,标题会再次更改回“MainActivity”,而不是应该显示“Hello”,知道吗?

好的,那么在MainFrgment中添加setTitle("Hello")也不起作用。例如,活动开始时标题是“Hello”,转到DetailsFragment并再次按下返回按钮时,标题会回到“fragment_main”。

唯一的解决方案是在MainFragment中同时使用setTitle("Hello")Navigation.findNavController(view).getCurrentDestination().setLabel("Hello")

那么使用导航组件显示片段标题的正确方法是什么?


3
可能是重复的问题 https://dev59.com/hlUL5IYBdhLWcg3wQWOD#53830472 - Agna JirKon Rx
这个回答解决了你的问题吗?使用AndroidX Navigation从Fragment动态设置ActionBar标题 - AdamHurwitz
19个回答

59

实际上这是因为:

android:label="fragment_main"

您在xml文件中设置的内容。

那么,使用导航组件正确展示Fragment标题的方法是什么?

setTitle()在这里有效。不过,因为您为这些Fragment设置了标签,当重新创建Activity时可能会再次显示标签。解决方案可能是删除android:label,然后使用代码实现:

((AppCompatActivity) getActivity()).getSupportActionBar().setTitle("your title");
或者:
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle("your subtitle");

onCreateView() 方法中。


找到了一种解决方法:

interface TempToolbarTitleListener {
    fun updateTitle(title: String)
}

class MainActivity : AppCompatActivity(), TempToolbarTitleListener {

    ...

    override fun updateTitle(title: String) {
        binding.toolbar.title = title
    }
}

然后:

(activity as TempToolbarTitleListener).updateTitle("custom title")

还要检查这个:使用AndroidX Navigation从Fragment设置动态ActionBar标题


我已经尝试过这个方法,结果是如果我在MainFragment中不使用Navigation.findNavController(view).getCurrentDestination().setLabel("Hello"),那么标题将保持为“Detail”。而且不知道为什么,有人给问题点了个踩 :p。 - Rajarshi
我想在这里明确一下,除了使用setTitleNavigation.findNavController(view).getCurrentDestination().setLabel("Hello")一起添加之外,似乎没有其他方法可以获取标题,即使我启动应用程序或按返回按钮。那么难道没有一行代码的可行方式吗?好的,让我更新问题。 - Rajarshi
3
导航组件存在问题。 - Rajarshi
手动编辑标题栏,或者假设 getCurrentDestination 的内容会使 NavController 失去同步。正确的解决方案在 https://dev59.com/hlUL5IYBdhLWcg3wQWOD#54504353。 - gladed

38

由于API已经更改,其他人仍在回答这个问题,让我回答我的问题

首先,从navigation.xml(又名导航图)中删除您希望更改标题的片段的android:label

现在,您可以通过调用Fragment内的方法来更改标题。

(requireActivity() as MainActivity).title = "My title"

但是您应该使用首选的方法,即在MainActivity中使用NavController.addOnDestinationChangedListener API。例如:

NavController.OnDestinationChangedListener { controller, destination, arguments ->
// compare destination id
    title = when (destination.id) {
        R.id.someFragment -> "My title"
        else -> "Default title"
    }

//    if (destination == R.id.someFragment) {
//        title = "My title"
//    } else {
//        title = "Default Title"        
//    }
}

1
如果您不想要任何标题,就不能提供null或空字符串。相反,您必须将destination.label设置为" "。我已经在您的代码中添加了此示例(在我的更改经过同行评审后应该可见)。 - muetzenflo
1
@muetzenflo 这个问题已经在 Navigation UI "2.3.0-alpha04" 中修复了。 - Ally
1
或者您可以直接设置destination.label,appBar标签会随之更改。在调用NavigationUI.setupActionBarWithNavController(...)之前必须设置OnDestinationChangedListener。 - Vít Kapitola
2
即使从导航图中删除标签,(requireActivity() as MainActivity).title也不会更改标签。也许API功能最近发生了变化。 - AdamHurwitz
或者只需在导航应用程序图中为所有片段添加标签,并设置 title = destination.label,如下所示:findNavController(R.id.nav_host_fragment).addOnDestinationChangedListener { controller, destination, arguments -> title = destination.label } - MiStr

23

如果您没有指定应用栏(默认应用栏),则可以在片段中使用此代码。

(activity as MainActivity).supportActionBar?.title = "Your Custom Title"

请记得从您的导航图中删除android:label属性

愉快的编程 ^-^


对我来说关键是从导航 XML 文件中的 Fragment 中删除 android:label 属性。然后,我可以使用 toolbar?.title = "my new name" 动态更改 Fragment 的标题。 - MatJB

15

从经验来看,NavController.addOnDestinationChangedListener似乎表现不错。下面是在我的MainActivity上的例子:

navController.addOnDestinationChangedListener{ controller, destination, arguments ->
        title = when (destination.id) {
           R.id.navigation_home -> "My title"
            R.id.navigation_task_start -> "My title2"
            R.id.navigation_task_finish -> "My title3"
            R.id.navigation_status -> "My title3"
            R.id.navigation_settings -> "My title4"
            else -> "Default title"
        }

    }

4
如果有人像我一样太愚蠢而且想知道为什么它不起作用:你必须在底部添加supportActionBar?.title = title以将标题应用到操作栏。 - Max90
你也可以使用 destination.setLabel() 来设置标题,但必须在调用 NavigationUI.setupActionBarWithNavController(...); 方法之前完成。 - Vít Kapitola

14

现在使用Kotlin和 androidx.navigation:navigation-ui-ktx 可以更轻松地实现这一点:

import androidx.navigation.ui.setupActionBarWithNavController

class MainActivity : AppCompatActivity() {

    private val navController: NavController
        get() = findNavController(R.id.nav_host_fragment)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this,R.layout.activity_main)
        setSupportActionBar(binding.toolbar)
        setupActionBarWithNavController(navController) // <- the most important line
    }

    // Required by the docs for setupActionBarWithNavController(...)
    override fun onSupportNavigateUp() = navController.navigateUp()
}

就是这样。别忘了在你的导航图中指定 android:label


这是正确的答案! - soggypants

9
您可以使用导航图xml文件,并将片段的标签设置为参数。 然后,在父片段中,您可以使用SafeArgs传递参数(请按照https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args上的指南设置SafeArgs),并提供默认值以避免标题为空或为空。
<!--this is originating fragment-->
<fragment
        android:id="@+id/fragmentA"
        android:name=".ui.FragmentA"
        tools:layout="@layout/fragment_a">
        <action
            android:id="@+id/fragmentBAction"
            app:destination="@id/fragmentB" />
</fragment>

<!--set the fragment's title to a string passed as an argument-->
<!--this is a destination fragment (assuming you're navigating FragmentA to FragmentB)-->
<fragment
    android:id="@+id/fragmentB"
    android:name="ui.FragmentB"
    android:label="{myTitle}"
    tools:layout="@layout/fragment_b">
    <argument
        android:name="myTitle"
        android:defaultValue="defaultTitle"
        app:argType="string" />
</fragment>

在原始碎片中:

public void onClick(View view) {
     String text = "text to pass as a title";
     FragmentADirections.FragmentBAction action = FragmentADirections.fragmentBAction();
     action.setMyTitle(text);
     Navigation.findNavController(view).navigate(action);
}

FragmentADirections和FragmentBAction - 这些类是从您的nav_graph.xml文件中的ID自动生成的。在“your action ID + Action”类型的类中,您可以找到自动生成的setter方法,您可以使用这些方法传递参数。

在您的接收目的地中,您调用自动生成的类{您的接收片段ID}+Args

FragmentBArgs.fromBundle(requireArguments()).getMyTitle();

请参考官方指南: https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args

1
你能否提供完整的建议,包括使用SafeArgs为xml文件提供参数所需的命令? - Aliton Oliveira

5
NavigationUI.setupActionBarWithNavController(this, navController)

不要忘记在导航图中为您的片段指定 android:label

要返回上一页:

override fun onSupportNavigateUp(): Boolean {
    return NavigationUI.navigateUp(navController, null)
}

5

我花了几个小时尝试做一件非常简单的事情,即在从主片段导航到特定项目时更改详细片段的标题栏。我爸爸经常对我说:K.I.S.S. 简单点,孩子。 使用setTitle()方法会导致崩溃,接口也很麻烦,最后想出了一种(非常)简单的解决方案,适用于最新的片段导航实现。 在主片段中解析NavController,获取NavGraph,找到目标节点,设置标题,最后导航到它。

//----------------------------------------------------------------------------------------------

私有的void navigateToDetail() {

NavController navController = NavHostFragment.findNavController(FragmentMaster.this);
navController.getGraph().findNode(R.id.FragmentDetail).setLabel("Master record detail");
navController.navigate(R.id.action_FragmentMaster_to_FragmentDetail,null);

}


谢谢你,非常有效且简单 :) - Rezaul Islam
为什么不简单地正确使用setTitle呢?如果你在每个片段的onCreate中设置它,它就不应该崩溃。 - David

3

android:label="{title_action}"

<?xml version="1.0" encoding="utf-8"?>
<navigation 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph"
    app:startDestination="@id/FirstFragment">

    <fragment
        android:id="@+id/FirstFragment"
        android:name="com.example.contact2.FirstFragment"
        android:label="@string/first_fragment_label"
        tools:layout="@layout/fragment_first">

        <action
            android:id="@+id/action_FirstFragment_to_SecondFragment"
            app:destination="@id/SecondFragment"
            app:enterAnim="@anim/nav_default_enter_anim"
            app:exitAnim="@anim/nav_default_exit_anim"
            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
            app:popExitAnim="@anim/nav_default_pop_exit_anim" />
    </fragment>
    <fragment
        android:id="@+id/SecondFragment"
        android:name="com.example.contact2.SecondFragment"
        android:label="{title_action}"
        tools:layout="@layout/fragment_second">

        <action
            android:id="@+id/action_SecondFragment_to_FirstFragment"
            app:destination="@id/FirstFragment"
            app:enterAnim="@anim/nav_default_enter_anim"
            app:exitAnim="@anim/nav_default_exit_anim"
            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
            app:popExitAnim="@anim/nav_default_pop_exit_anim" />
        <argument
            android:name="title_action"
            app:argType="string"
            app:nullable="true" />
    </fragment>
</navigation>

1
val args = Bundle() - Ahmad Abdullah
1
args.putString("title_action", "我的自定义标题") - Ahmad Abdullah
1
findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment, args) - Ahmad Abdullah

2
API可能已经发生了变化,但现在你可以在导航图标签中指示对应于应用程序资源中的字符串的引用。

enter image description here

如果您想要为片段设置静态标题,这非常有用。


1
如果我想重复使用这个片段并需要更改标签,我该如何以编程的方式实现? - Shikhar

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