Как установить заголовок на панели приложения с помощью компонента 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()
работает на данный момент. Но, потому что вы установили ярлык для тех, Fragment
s, это может показать метку снова при воссоздании 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
}
}