I would like to create an Android app that uses a navigation drawer which loads different fragments, all of which include a toolbar/appbar and one that also has a TabView with ViewPager2, something like this:
So I started a new Java project with Android Studio and chose the Navigation Drawer Activity template that creates 3 different fragments. This is my code:
activity_main.xml (removed ToolBar from the template)
<androidx.drawerlayout.widget.DrawerLayout 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/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">
<fragment
android:id="@ id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
<com.google.android.material.navigation.NavigationView
android:id="@ id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/activity_main_drawer" />
</androidx.drawerlayout.widget.DrawerLayout>
MainActivity.java (commented setupActionBarWithNavController
because ToolBar is not here anymore)
package com.testui2;
import android.os.Bundle;
import android.view.Menu;
import com.google.android.material.navigation.NavigationView;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.appcompat.app.AppCompatActivity;
import com.testui2.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
private AppBarConfiguration mAppBarConfiguration;
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
DrawerLayout drawer = binding.drawerLayout;
NavigationView navigationView = binding.navView;
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
mAppBarConfiguration = new AppBarConfiguration.Builder(
R.id.nav_home, R.id.nav_gallery, R.id.nav_slideshow)
.setOpenableLayout(drawer)
.build();
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
//NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
NavigationUI.setupWithNavController(navigationView, navController);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onSupportNavigateUp() {
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
return NavigationUI.navigateUp(navController, mAppBarConfiguration)
|| super.onSupportNavigateUp();
}
}
fragment_home.xml (first fragment with ToolBar only)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".ui.home.HomeFragment">
<include layout="@layout/app_bar_main"
android:id="@ id/appbar" />
<TextView
android:id="@ id/text_home"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
app_bar_main.xml (moved ToolBar here to apply to other fragments, too)
<?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"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.TestUI2.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/Theme.TestUI2.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@ id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/fab_margin"
android:layout_marginBottom="16dp"
app:srcCompat="@android:drawable/ic_dialog_email" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
app_bar_main_tabs.xml (identical to the previous, but with TabLayout
for the second fragment that requires it)
<?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"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.TestUI2.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/Theme.TestUI2.PopupOverlay" />
<!-- This layout has the tabs -->
<com.google.android.material.tabs.TabLayout
android:id="@ id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TabLayout.Colored" />
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@ id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/fab_margin"
android:layout_marginBottom="16dp"
app:srcCompat="@android:drawable/ic_dialog_email" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
fragment_gallery.xml (second fragment that has ToolBar and TabLayout with ViewPager like the image at the top)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".ui.gallery.GalleryFragment">
<include layout="@layout/app_bar_main_tabs"
android:id="@ id/appbar" />
<androidx.viewpager2.widget.ViewPager2
android:id="@ id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.constraintlayout.widget.ConstraintLayout>
HomeFragment.java (code behind the first fragment, modified the template to have the ToolBar setup here)
package com.testui2.ui.home;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import com.testui2.MainActivity;
import com.testui2.databinding.FragmentHomeBinding;
public class HomeFragment extends Fragment {
private FragmentHomeBinding binding;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
HomeViewModel homeViewModel =
new ViewModelProvider(this).get(HomeViewModel.class);
binding = FragmentHomeBinding.inflate(inflater, container, false);
View root = binding.getRoot();
final TextView textView = binding.textHome;
homeViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);
return root;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
MainActivity currentActivity = (MainActivity) requireActivity();
currentActivity.setSupportActionBar(binding.appbar.toolbar);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
GalleryFragment.java (code behind the second fragment, with tabs and viewpager2)
package com.testui2.ui.gallery;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import com.testui2.MainActivity;
import com.testui2.databinding.FragmentGalleryBinding;
public class GalleryFragment extends Fragment {
private FragmentGalleryBinding binding;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
binding = FragmentGalleryBinding.inflate(inflater, container, false);
View root = binding.getRoot();
// Code to handle tabs
GalleryPagerAdapter galleryPagerAdapter = new GalleryPagerAdapter(requireActivity());
ViewPager2 viewPager = binding.viewPager;
viewPager.setAdapter(galleryPagerAdapter);
TabLayout tabs = binding.appbar.tabs;
new TabLayoutMediator(tabs, viewPager,
(tab, position) -> tab.setText("TAB " (position 1))
).attach();
return root;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
MainActivity currentActivity = (MainActivity) requireActivity();
currentActivity.setSupportActionBar(binding.appbar.toolbar);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
// Class to handle ViewPager2
private class GalleryPagerAdapter extends FragmentStateAdapter {
public GalleryPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
}
public GalleryPagerAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle) {
super(fragmentManager, lifecycle);
}
@NonNull
@Override
public Fragment createFragment(int position) { return GalleryPageFragment.newInstance(position); }
@Override
public int getItemCount() {
return 3;
}
}
}
GalleryPageFragment.java (the code that handles the pages on the ViewPager2
)
package com.testui2.ui.gallery;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.testui2.databinding.FragmentGalleryPageBinding;
public class GalleryPageFragment extends Fragment {
private FragmentGalleryPageBinding binding;
private static final String ARG_PARAM1 = "param1";
private int mParam1;
public GalleryPageFragment() {
// Required empty public constructor
}
public static GalleryPageFragment newInstance(int param1) {
GalleryPageFragment fragment = new GalleryPageFragment();
Bundle args = new Bundle();
args.putInt(ARG_PARAM1, param1);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getInt(ARG_PARAM1);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
binding = FragmentGalleryPageBinding.inflate(inflater, container, false);
View root = binding.getRoot();
binding.textGallery.setText(String.format("This is gallery page %d", mParam1 1));
return root;
}
}
Basically I took the template and modified it to move the ToolBar code into the fragment (using
The tab layout is displayed correctly on the second fragment (Gallery) and the PageViewer2 scrolls the tabs successfully. But if I click on the tab names, it doesn't switch the current tab. How can I do that?
Or, if you have other suggestions on how to handle a fixed ToolBar (meaning it is inside activity_main.xml
) more easily, but with one of the fragments that attaches the TabLayout to look the same than the first picture, I could of course change the code. I must have the Navigation drawer, too.
I tried in another project to stick with the default template (with the ToolBar in the activity_main.xml
) and, on the Gallery fragment, putting TabLayout and ViewPager on the same XML layout. But doing that, the tabs are not looking the same: an horizontal separator appears between the TabLayout and the ToolBar (because TabLayout is not inside the <com.google.android.material.appbar.AppBarLayout>
XML node) and there's no drop shadow below the TabLayout. Example below:
CodePudding user response:
After doing several tests, what I want to obtain is too much difficult with that approach. Starting from scratch (the Navigation Drawer Activity template) and solving the UI glitch is much more easy.
app_bar_main.xml (1 line changed from the template, as all the fragments already have the ToolBar)
<?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"
tools:context=".MainActivity">
<!-- added "app:elevation" line -->
<com.google.android.material.appbar.AppBarLayout
android:id="@ id/appbarlayout"
app:elevation="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.TestUI3.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/Theme.TestUI3.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_main" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@ id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/fab_margin"
android:layout_marginBottom="16dp"
app:srcCompat="@android:drawable/ic_dialog_email" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
fragment_gallery.xml (this fragment has the TabLayout
too, so it's added together with the ViewPager2
that displays the other "page" fragments)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
tools:context=".ui.gallery.GalleryFragment">
<!-- "android:elevation" should be the same than the previous
"app:elevation" on the AppBarLayout; the style is used
to copy the same colour of the ToolBar -->
<com.google.android.material.tabs.TabLayout
android:id="@ id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="8dp"
style="@style/Widget.MaterialComponents.TabLayout.Colored" >
</com.google.android.material.tabs.TabLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@ id/viewPager2"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Using this method, tabs are clickable correctly (problem #1 solved), but the style is not exactly the same:
Unfortunately, if I set app:elevation="0dp"
on the AppBarLayout as several answers here suggest, then when other fragments without the TabLayout
are displayed the drop shadow is missing!
So, at this point, the easier way to handle is disabling the elevation using code.
GalleryFragment.java (the code behind the fragment that has TabLayout
)
package com.testui3.ui.gallery;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.ViewModelProvider;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import com.testui3.R;
import com.testui3.databinding.FragmentGalleryBinding;
public class GalleryFragment extends Fragment {
private FragmentGalleryBinding binding;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
GalleryViewModel galleryViewModel =
new ViewModelProvider(this).get(GalleryViewModel.class);
binding = FragmentGalleryBinding.inflate(inflater, container, false);
View root = binding.getRoot();
// ADDED: disable elevation on toolbar when this fragment is displayed
((AppCompatActivity) getActivity()).findViewById(R.id.appbarlayout).setElevation(0);
// Code to handle tabs
GalleryPagerAdapter galleryPagerAdapter = new GalleryPagerAdapter(requireActivity());
ViewPager2 viewPager = binding.viewPager2;
viewPager.setAdapter(galleryPagerAdapter);
TabLayout tabs = binding.tabLayout;
new TabLayoutMediator(tabs, viewPager,
(tab, position) -> tab.setText("TAB " (position 1))
).attach();
return root;
}
@Override
public void onDestroyView() {
// ADDED: Restore previous elevation when fragment disappears
((AppCompatActivity) getActivity()).findViewById(R.id.appbarlayout).setElevation(8);
super.onDestroyView();
binding = null;
}
// Class to handle ViewPager2
private class GalleryPagerAdapter extends FragmentStateAdapter {
public GalleryPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
}
public GalleryPagerAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle) {
super(fragmentManager, lifecycle);
}
@NonNull
@Override
public Fragment createFragment(int position) { return GalleryPageFragment.newInstance(position); }
@Override
public int getItemCount() {
return 3;
}
}
}
This method seems to work well, and NavigationUI works, too:
The drop shadow is kept when navigation out to the other fragments that doesn't have the TabLayout
:
I still think doing this in code is not the "correct" solution, but at least it works and doesn't have the hassles of the question post method (too many layouts and includes!).