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

How to set title in app bar with Navigation Architecture Component

我正在试用导航架构组件,但现在在设置标题时遇到了困难。如何以编程方式设置标题及其工作原理?

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

来自 stack-overflow 上 multiple app bars 的另一个问题的相同代码。

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

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>

navigation.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"。与往常一样,它显示 MainFragment,其中包含转到 DetailFragment 的按钮。在 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") 也不行。例如activity开始,标题是"Hello",转到DetailsFragment再按返回键,标题又回到"fragment_main"。

唯一的解决办法是在 MainFragment.

中同时使用 setTitle("Hello")Navigation.findNavController(view).getCurrentDestination().setLabel("Hello")

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

其实是因为:

android:label="fragment_main"

你在xml中设置的。

So what is the proper way to show the title for Fragments using Navigation Component?

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")

也看看这个:

我建议在每个屏幕中包含 AppBar。 为避免代码重复,创建一个构建 AppBar 的助手,将标题作为参数。然后在每个屏幕中调用助手 class.

由于其他人仍在参与回答这个问题,让我回答我自己的问题,因为APIs 从那时起已经发生了变化。

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

现在您可以通过调用

更改 Fragment 的标题
(requireActivity() as MainActivity).title = "My title"

但是您应该使用的首选方式是 MainActivity 中的 API NavController.addOnDestinationChangedListener。一个例子:

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"        
//    }
}

根据经验,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"
        }

    }

如今使用 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

另一种方法可能是这样的:

fun Fragment.setToolbarTitle(title: String) {
    (activity as NavigationDrawerActivity).supportActionBar?.title = title
}

API 可能在提出问题后发生了变化,但现在您可以在导航图标签中指示对应用资源中字符串的引用。

如果您想要片段的静态标题,这将非常有用。

如果您不指定您的应用栏(默认应用栏),您可以在您的片段中使用此代码

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

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

开心码^-^

您可以使用导航图 xml 文件并将片段的标签设置为参数。 然后,在您的 parent 片段中,您可以使用 SafeArgs 传递参数(请按照 https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args 上的指南设置 SafeArgs)并提供默认值以避免标题为 null 或空。

<!--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 - 这些 class 是根据您 nav_graph.xml 文件中的 ID 自动生成的。 在 'your action ID + Action' 中键入 classes 你可以找到 auto-generated setter 方法,你可以用它来传递参数

在你的接收目的地,你调用auto-generated class {你的接收片段ID}+Args

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

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

NavigationUI.setupActionBarWithNavController(this, navController)

Don't forget to specify android:label for your fragments in your nav graphs.

向后导航:

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

删除导航图 xml 文件中的详细片段标签。
然后通过参数将首选标题传递给需要标题的目标片段。
第一个片段代码-起点

findNavController().navigate(
            R.id.action_FirstFragment_to_descriptionFragment,
            bundleOf(Pair("toolbar_title", "My Details Fragment Title"))
        )

如您所见,我在导航到目标片段时在参数包中成对发送
所以在你的目标片段中从 onCreate 方法中的参数获取标题,就像这样

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        toolbarTitle = requireArguments().getString("toolbar_title", "")
    }

然后使用它在 onCreateView 方法中像这样更改 Main Activity 的标题
requireActivity().toolbar.title = toolbarTitle

使用导航中的任一标签更新标题 xml 或排除标签并使用 requiresActivity().title 设置 支持混合使用带有和不带有动态标题的屏幕的两种方式。使用 Compose UI 工具栏和选项卡为我工作。

    val titleLiveData = MutableLiveData<String>()
    findNavController().addOnDestinationChangedListener { _, destination, _ ->
        destination.label?.let {
            titleLiveData.value = destination.label.toString()
        }
    }

    (requireActivity() as AppCompatActivity).setSupportActionBar(object: Toolbar(requireContext()) {
        override fun setTitle(title: CharSequence?) {
            titleLiveData.value = title.toString()
        }
    })

    titleLiveData.observe(viewLifecycleOwner, { 
        // Update your title
    })

无论如何,我尝试过的那些答案中有几个没有用,所以我决定在 Kotlin 中使用接口

以旧的 Java 方式来做

创建界面如下图。

interface OnTitleChangeListener {
   fun onTitleChange(app_title: String)
}

然后让我的activity实现这个接口如下图

class HomeActivity : AppCompatActivity(), OnTitleChangeListener {
    override fun onTitleChange(app_title: String) {
    title = app_title
    }
}

我的片段在 activity 上如何,我这个

override fun onAttach(context: Context) {
    super.onAttach(context)
    this.currentContext = context
    (context as HomeActivity).onTitleChange("New Title For The app")
}

我花了几个小时尝试做一件非常简单的事情,例如在从主片段上的特定项目导航时更改详细片段的栏标题。我爸爸总是对我说:K.I.S.S。保持简单,儿子。 setTitle() 崩溃了,接口很繁琐,在代码行方面提出了一个(非常)简单的解决方案,用于最新的片段导航实现。 在主片段中解析 NavController,获取 NavGraph,找到目标节点,设置标题,最后但并非最不重要的导航到它:

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

private 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);

}

简单的解决方法:

布局

androidx.coordinatorlayout.widget.CoordinatorLayout
  com.google.android.material.appbar.AppBarLayout
    com.google.android.material.appbar.MaterialToolbar
  
  androidx.constraintlayout.widget.ConstraintLayout
    com.google.android.material.bottomnavigation.BottomNavigationView
    fragment
      androidx.navigation.fragment.NavHostFragment 
    

Activity

    binding = DataBindingUtil.setContentView(this, layoutRes)
    setSupportActionBar(binding.toolbar)
    
    val controller = findNavController(R.id.nav_host)
    val configuration = AppBarConfiguration(
        setOf(
            R.id.navigation_home,
            R.id.navigation_dashboard,
            R.id.navigation_notifications
        )
    )
    setupActionBarWithNavController(controller, configuration)
    navView.setupWithNavController(controller)

如果您想使用 Activity 和 Fragment 之间的低内聚代码以编程方式更改工具栏的标题,这可能会有所帮助。


class DetailsFragment : Fragment() {

    interface Callbacks {
        fun updateTitle(title: String)
    }

    private var listener: Callbacks? = null

    override fun onAttach(context: Context) {
        super.onAttach(context)
        // keep activity as interface only
        if (context is Callbacks) {
            listener = context
        }
    }

    override fun onDetach() {
        // forget about activity
        listener = null
        super.onDetach()
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View =
        inflater.inflate(R.layout.fragment_details, container, false)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        listener?.updateTitle("Dynamic generated title")
    }

}

class MainActivity : AppCompatActivity(), DetailsFragment.Callbacks {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun updateTitle(title: String) {
        supportActionBar?.title = title
    }

}

2021 年 9 月 4 日

在我的例子中使用 Kotlin 这是解决方案。在 MainActivity

中使用此代码
 val navController = this.findNavController(R.id.myNavHostFragment)
     navController.addOnDestinationChangedListener { controller, destination, arguments ->
                destination.label = when (destination.id) {
                    R.id.homeFragment -> resources.getString(R.string.app_name)
                    R.id.roomFloorTilesFragment -> resources.getString(R.string.room_floor_tiles)
                    R.id.roomWallTilesFragment -> resources.getString(R.string.room_wall_tiles)
                    R.id.ceilingRateFragment -> resources.getString(R.string.ceiling_rate)
                    R.id.areaConverterFragment -> resources.getString(R.string.area_converter)
                    R.id.unitConverterFragment -> resources.getString(R.string.unit_converter)
                    R.id.lenterFragment -> resources.getString(R.string.lenter_rate)
                    R.id.plotSizeFragment -> resources.getString(R.string.plot_size)
                    else -> resources.getString(R.string.app_name)
                }
            }
            NavigationUI.setupActionBarWithNavController(...)
            NavigationUI.setupWithNavController(...)