How can you create a clipped NavigationDrawer below the AppBar?

1.7k Views Asked by At

How can you create a clipped NavigationDrawer below the AppBar?

The latest Android Studio (3.5.3) generates a full-height NavigationDrawer, and my question is what needs to be changed to instead get a clipped NavigationDrawer?

( Please do not label this question as a "duplicate" by linking to ANCIENT (e.g. 2015) questions with long list of ancient answers. Now it is 2020 and I am hoping that there will now exist an easy method of implementing a clipped NavigationDrawer. Hopefully there is now a simple solution that plays nice with androidx and jetpack navigation and Kotlin methods such as setupActionBarWithNavController. When I above mentioned code generated with Android Studio I am now talking about Android Studio 3.5.3, i.e. the currently latest version, and its project template "Navigation Drawer Activity" with Kotlin and the minimum API Level 19 i.e. Android 4.4. When developers today want to find a way to do this, and search with google and stackoverflow, then we do not want to find and scroll through long pages with lots of old/outdated pages answers. Since this question is now asked in february 2020, it will be clear for everyone that all potentially coming answers below will also be later than that. )

It is strange that it seems so difficult to find documentation about HOW to implement a clipped drawer with Android. Here the two types ("full-height" and "clipped") of NavigationDrawers are mentioned:

https://material.io/components/navigation-drawer/#standard-drawer

Quote:

"A standard navigation drawer can use one of these elevation positions:

 At the same elevation as a top app bar (full-height)

 At a lower elevation than a top app bar (clipped)"

At the above webpage there is also a link to an android specific page:

https://material.io/develop/android/components/navigation-view/

However, that page does currently not mention anything about how to create a clipped NavigationDrawer. Also, that android page does not seem very updated since it currently links to the old support v4 library about DrawerLayout.

When I instead look at the new androidx page about DrawerLayout I can still not find anything about "clipped" drawer. (since "clipped" is the term used in google's material design then google should also use that same word to be searchable in the documentation pages).

Here are some pages where it should be possible to find something about "clip" but currently, unfortunately not:

https://developer.android.com/jetpack/androidx/releases/drawerlayout

https://developer.android.com/guide/navigation/navigation-ui#add_a_navigation_drawer

To illustrate what I am looking for (independently from the above material design page which might change) I provide some pictures below.

The first screenshot below is the result (with two modifications, see below) after having generated an Android application with Android Studio 3.5.3 (currently the latest) and the "Navigation Drawer Activity" with Kotlin and the minimum API Level 19 (Android 4.4).

The two changes I have done (in "activity_main.xml") was that I removed the app:headerLayout from NavigationView and replaced android:layout_height="match_parent" with android:layout_height="wrap_content". enter image description here

Then I have edited some screenshots with GIMP to illustrate what I really would want, as in the picture below. The "hamburger icon" should be possible to close the NavigationDrawer i.e. use it for toggling.

screenshot

Below are some of the relevant files generated with the "full-height" NavigationDrawer, and my question is what changes do I have to implement to get the "clipped" NavigationDrawer as in the aboved edited picture?


MainActivity.kt

package com.myapplication

import android.os.Bundle
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import androidx.drawerlayout.widget.DrawerLayout
import com.google.android.material.navigation.NavigationView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import android.view.Menu

class MainActivity : AppCompatActivity() {

    private lateinit var appBarConfiguration: AppBarConfiguration

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val toolbar: Toolbar = findViewById(R.id.toolbar)
        setSupportActionBar(toolbar)

        val fab: FloatingActionButton = findViewById(R.id.fab)
        fab.setOnClickListener { view ->
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                .setAction("Action", null).show()
        }
        val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
        val navView: NavigationView = findViewById(R.id.nav_view)
        val navController = findNavController(R.id.nav_host_fragment)
        // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        appBarConfiguration = AppBarConfiguration(
            setOf(
                R.id.nav_home, R.id.nav_gallery, R.id.nav_slideshow,
                R.id.nav_tools, R.id.nav_share, R.id.nav_send
            ), drawerLayout
        )
        setupActionBarWithNavController(navController, appBarConfiguration)
        navView.setupWithNavController(navController)
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu; this adds items to the action bar if it is present.
        menuInflater.inflate(R.menu.main, menu)
        return true
    }

    override fun onSupportNavigateUp(): Boolean {
        val navController = findNavController(R.id.nav_host_fragment)
        return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<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">

    <include
        layout="@layout/app_bar_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <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:menu="@menu/activity_main_drawer" />

</androidx.drawerlayout.widget.DrawerLayout>

app_bar_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"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        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>

    <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_margin="@dimen/fab_margin"
        app:srcCompat="@android:drawable/ic_dialog_email" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

content_main.xml

<?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"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:showIn="@layout/app_bar_main">

    <fragment
        android:id="@+id/nav_host_fragment"
        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" />
</androidx.constraintlayout.widget.ConstraintLayout>
2

There are 2 best solutions below

8
On BEST ANSWER

This turned out to be a little trickier than I thought i'd be but this error message helped get me there:

DrawerLayout must be measured with MeasureSpec.EXACTLY

Here is the Kotlin solution:

  1. Create a new class that extends the DrawerLayout class. Inside the onMeasure method, create two new variables for widthMeasureSpec and heightMeasureSpec and pass them onto the super class:

    class CustomDrawerLayout @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
    ) : DrawerLayout(context, attrs, defStyleAttr) {
    
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec),MeasureSpec.EXACTLY)
        var newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),MeasureSpec.EXACTLY)
        super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec)
      }
    }
    
  2. In your activity_main.xml file, update the outermost tag to use your new CustomDrawerLayout. Change the CustomDrawerLayout and NavigationView layout_height to this:

    android:layout_height="wrap_content"
    
  3. Make sure that when you find the drawer_layout view, you are initializing it as an instance of the CustomDrawerLayout class:

    var drawerLayout : CustomDrawerLayout = findViewById(R.id.clipped_drawer_layout)
    
  4. To keep the action bar visible, you need to add this to the NavigationView component:

    android:layout_marginTop="?android:attr/actionBarSize"
    

The full activity_main.xml file would look like this:

<com.mullr.neurd.Miscellaneous.CustomDrawerLayout
android:id="@+id/clipped_drawer_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:openDrawer="start"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

    <include
        layout="@layout/app_bar_main"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="start"
        android:layout_marginTop="?android:attr/actionBarSize"
        app:headerLayout="@layout/nav_header_main"
        app:menu="@menu/activity_main_drawer" />

</com.mullr.neurd.Miscellaneous.CustomDrawerLayout>

Last, remove this line from your styles.xml (v21) file so that the status bar is not covered up:

<item name="android:statusBarColor">@android:color/transparent</item>

That should do it. enter image description here

8
On

New answer to keep the app bar on screen when the nav drawer is opened:

main_drawer.xml - New layout file that holds the custom drawerLayout, app_bar_main layout, and the navigation view

<com.example.myapp.Miscellaneous.CustomDrawerLayout 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/full_drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">

    <include
        layout="@layout/app_bar_main"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

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

    </com.google.android.material.navigation.NavigationView>

</com.example.myapp.Miscellaneous.CustomDrawerLayout>

app_bar_main.xml - Remove the Toolbar from this file

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

    <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_margin="@dimen/fab_margin"
        android:visibility="gone"
        app:srcCompat="@android:drawable/ic_dialog_email" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

activity_main.xml - Add the toolbar and the main_drawer layout here.

<androidx.appcompat.widget.LinearLayoutCompat 
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:fitsSystemWindows="true"
>

<androidx.appcompat.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="@color/primaryBlue"
    android:theme="@style/AppTheme"
    app:popupTheme="@style/AppTheme.PopupOverlay"
    app:titleTextColor="@color/design_default_color_background" />


<include
    layout="@layout/main_drawer"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

</androidx.appcompat.widget.LinearLayoutCompat>

MainActivity - Add this to make the menu toggle still work

toolbar.setNavigationOnClickListener {
            if(drawerLayout.isDrawerOpen(GravityCompat.START)){
                drawerLayout.closeDrawer(GravityCompat.START)
            }
            else{
                drawerLayout.openDrawer(GravityCompat.START)
            }
        }

End result should look something like this:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main)


        // NAVIGATION 
        val toolbar: Toolbar = findViewById(R.id.toolbar)
        setSupportActionBar(toolbar)

        var drawerLayout: CustomDrawerLayout = findViewById(R.id.full_drawer_layout)
        navView = findViewById(R.id.nav_view)

        // This needs to come after drawerLayout has been initialized
        toolbar.setNavigationOnClickListener {
            if(drawerLayout.isDrawerOpen(GravityCompat.START)){
                drawerLayout.closeDrawer(GravityCompat.START)
            }
            else{
                drawerLayout.openDrawer(GravityCompat.START)
            }
        }

        navController = findNavController(R.id.nav_host_fragment)
        navView.itemIconTintList = null
        toolbar.setupWithNavController(navcontroller,drawerLayout)
        // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        appBarConfiguration = AppBarConfiguration(
            setOf(
                R.id.nav_home, R.id.nav_favorites, R.id.nav_settings
            ), drawerLayout
        )
        setupActionBarWithNavController(navController, appBarConfiguration)
        navView.setupWithNavController(navController)
    }

To preserve the behavior of the back button (or "Up" button) on the toolbar, make sure you include this line in your activity's onCreate method (setUpWithNavController):

    toolbar.setupWithNavController(findNavController(R.id.nav_host_fragment),drawerLayout)

Last, remove this line from your styles.xml (v21) file so that the status bar is not covered up:

<item name="android:statusBarColor">@android:color/transparent</item>

enter image description here