Skip to content
Advertisement

Android NavigationDrawer and Toolbar in all fragments, plus TabLayout into one

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:

Appbar and TabView

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 this and this) because I want the tabs to be docked directly under the ToolBar and, in the future, handle devices with big screens with the fragments displayed simultaneously. Unfortunately there are issues with this approach, that I would like to solve:

  1. I’m unable to apply the NavigationUI to this method, because I don’t know how to call NavigationUI.setupActionBarWithNavController from the Fragments correctly. I must call it from the Fragments because the Toolbar is there, in fact I’m missing both the AppBar title and the hamburger icon:

    First fragment (Home)

  2. 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?

    Second fragment (Gallery) with tabs

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:

ToolBar and TabLayout when TabLayout is not inside AppBarLayout

Answer

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:

Elevation problem

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:

Enabling app:elevation in code

The drop shadow is kept when navigation out to the other fragments that doesn’t have the TabLayout:

app:elevation is restored

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!).

Advertisement