Как установить заголовок на панели приложения с помощью компонента Navigation Architecture

Я опробовал компонент архитектуры навигации и теперь испытываю трудности с настройкой заголовка. Как установить заголовок программно, а также как он работает?

Чтобы очистить мой вопрос, давайте рассмотрим пример, где я установил простое приложение с MainActivity хост хост-контроллера навигации, MainFragment имеет кнопку и при нажатии на кнопку она идет к DetailFragment,

Тот же код из другого вопроса о нескольких панелях приложений при переполнении стека.

Основная деятельность

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, это действительно идет, и название изменяется на "Деталь". Но при нажатии кнопки "Назад" заголовок меняется на "фрагмент_основы". Поэтому я добавил эту строку кода в 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 название изменится на "Привет". Но тут возникает вторая проблема: когда я закрываю приложение и запускаю снова, название меняется на "MainActivity", хотя вместо этого должно отображаться "Hello", знаете?

Хорошо, тогда добавление setTitle("Hello") в MainFrgment тоже не работает. Например, начинается действие и заголовок "Привет", перейдите к DetailsFragment и нажмите кнопку назад еще раз, заголовок возвращается к "фрагмент_основе".

Единственное решение состоит в том, чтобы оба setTitle("Hello") вместе с Navigation.findNavController(view).getCurrentDestination().setLabel("Hello") в MainFragment,

Итак, как правильно отображать заголовки для фрагментов, используя компонент навигации?

20 ответов

Решение

Это на самом деле из-за:

android:label="fragment_main"

Который вы установили в XML.

Итак, как правильно показать название для Fragmentс помощью компонента навигации?

setTitle() работает на данный момент. Но, потому что вы установили ярлык для тех, Fragments, это может показать метку снова при воссоздании Activity, Решение, вероятно, будет удаление android:label а потом делай свои вещи с кодом:

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

Или же:

((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle();

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

Проверьте это тоже: динамический заголовок ActionBar из фрагмента с помощью AndroidX Navigation

Поскольку другие все еще участвуют в ответе на этот вопрос, позвольте мне ответить на мой собственный вопрос, поскольку API изменились с тех пор.

Сначала удалите android:label из фрагментов, которые вы хотите изменить, изнутри navigation.xml (он же навигационный график).

Теперь вы можете изменить название с помощью Fragment позвонив

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

Но предпочтительный способ использования - это API NavController.addOnDestinationChangedListener изнутри MainActivity, Пример:

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

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

Вы можете использовать этот код в своем фрагменте, если вы не укажете панель своего приложения (панель приложений по умолчанию)

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

Не забудьте удалить android:label атрибут в вашем навигационном графике

Счастливый код ^-^

По опыту, 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 в ваших навигационных графиках.

Вы можете использовать xml-файл графа навигации и установить метку фрагмента в качестве аргумента. Затем в родительском фрагменте вы можете передать аргумент с помощью SafeArgs и указать значение по умолчанию, чтобы заголовок не был пустым или пустым.

<!--set the fragment's title to characters name passed as an argument-->
<fragment
    android:id="@+id/characterDetailFragment2"
    android:name="ui.character.CharacterDetailFragment"
    android:label="{character_name}"
    tools:layout="@layout/fragment_character_detail">
    <argument
        android:name="character_name"
        android:defaultValue="Name"
        app:argType="string" />
</fragment>

В исходном фрагменте:

 String text = "text to pass";
 CharactersListFragmentDirections.CharacterDetailAction action = CharactersListFragmentDirections.characterDetailAction();
 action.setCharacterName(text);
 Navigation.findNavController(v).navigate(action);

CharactersListFragmentDirections и CharacterDetailAction - эти классы автоматически создаются из идентификаторов в вашем файле nav_graph.xml. В классах типа 'ваш идентификатор действия + действие' вы можете найти автоматически сгенерированные методы установки, которые вы можете использовать для передачи аргументов

<fragment
        android:id="@+id/charactersListFragment"
        android:name=".character.CharactersListFragment"
        android:label="List of Characters"
        tools:layout="@layout/fragment_characters_list">
        <action
            android:id="@+id/characterDetailAction"
            app:destination="@id/characterDetailFragment2"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />
    </fragment>

В вашем месте назначения (файл макета xml находится вверху этого ответа) вы вызываете автоматически сгенерированный класс {идентификатор вашего получающего фрагмента}+Args

CharacterDetailFragmentArgs.fromBundle(requireArguments()).getCharacterName();

См. Официальное руководство https://developer.android.com/guide/navigation/navigation-pass-data.

NavigationUI.setupActionBarWithNavController(this, navController)

Не забудьте указать android:label для ваших фрагментов в графах навигации.

Чтобы вернуться назад:

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

Я потратил пару часов, пытаясь сделать очень простую вещь, например, изменить заголовок панели детального фрагмента при переходе от конкретного элемента к главному фрагменту. Мой папа всегда говорил мне: ПОЦЕЛУЙ БЕСПЛАТНО, сын. У 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);

}

Android: метка = "{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>

API мог измениться с тех пор, как был задан вопрос, но теперь вы можете указать ссылку на строку в ресурсах вашего приложения в метке графа навигации.

Это очень полезно, если вам нужен статический заголовок для ваших фрагментов.

Удалить детальную метку фрагмента в 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, например
requireActivity().toolbar.title = toolbarTitle

Я бы предложил включить AppBar на каждом экране. Чтобы избежать дублирования кода, создайте помощник, который создает AppBar, принимая заголовок в качестве параметра. Затем вызовите помощника в каждом классе экрана.

Обновите заголовок с помощью метки в 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 в моем случае - это решение. используйте этот код в 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(...)

Другой подход может быть таким:

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

В любом случае некоторые из тех ответов, которые я пробовал, не сработали, поэтому я решил сделать это старым способом Java в Kotlin, используя интерфейс

Создан интерфейс, как показано ниже.

      interface OnTitleChangeListener {
   fun onTitleChange(app_title: String)
}

Затем я предпринял действия по реализации этого интерфейса, как показано ниже.

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

Как на моем фрагменте по активности атташе я это

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

Это просто, вам нужно настроить (или "присоединиться") к панели действий с помощью NavController.

setupActionBarWithNavController(navController)

:)

Мое собственное простое решение для библиотеки последних свиданий. Перед вызовом навигации сделайте следующее:

      findNavController().findDestination(R.id.yourNavFragmentId)?.label = "Your title"

затем перейдите к вашему фрагменту

      findNavController().navigate(R.id.yourNavFragmentId)

Простое решение:

Макет

      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 
    

Деятельность

          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
    }

}