1

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>
hata
  • 11,633
  • 6
  • 46
  • 69
user743436
  • 324
  • 2
  • 14
  • The word *clipped* just means clipped its top by the AppBar, doesn't mean a clipped bottom. – hata Feb 23 '21 at 03:21

2 Answers2

2

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

Code on the Rocks
  • 11,488
  • 3
  • 53
  • 61
  • Sorry but no it did not work. I have now tried it, but I also actually could see in your screenshot that it seems as you misunderstood something. In your screenshot, the header of your navigation drawer is still on top of your black app bar, hiding the "hamburger icon". I want the drawer content to be below the app bar, while the hamburger icon in the app bar is visible and can be toggled, as I illustrated with the second (GIMP edited) picture in my question. – user743436 Feb 05 '20 at 15:07
  • I updated my response, you have to add a few attributes to the XML file to make that happen. – Code on the Rocks Feb 05 '20 at 16:08
  • Nice but not perfect (in the way I had hoped). The desired toggle effect is working (kind of, but not in the expected way, since clicking anywhere will remove the drawer, including clicking the now disabled hamburger icon) The app bar options should be directly clickable (i.e. without the first click effectively becoming a toogle click for the drawer) I intend to accept the solution unless someone else provides an even better solution within a day or two. IMHO, a solution might be better in three ways: - Less code - App bar options not disabled - Visually better for Android 4.4 (API 19) – user743436 Feb 05 '20 at 17:30
  • Regarding this current statement in the above latest edit of the answer: 4. To keep the action bar visible, you need to add this to the CustomDrawerLayout: android:keepScreenOn="true" No, that attribute does not seem to be needed. I removed it and it still worked the same. Also I think it does not make any sense why it would make a difference when you look at the documentation about it: https://developer.android.com/training/scheduling/wakelock – user743436 Feb 05 '20 at 19:21
  • Ahh, you right. Anyways, I added a new solution that works well. It's not any less code but the app bar options still work and it looks decent. – Code on the Rocks Feb 05 '20 at 20:52
  • Your second solution was not acceptable because of the broken behaviour I mentioned there in the comments. But since nobody yet has provided a better solution I can accept your above first solution (if you edit it by adding the thing about @android:color/transparent from your second solution, to avoid hiding the status bar with e.g. the system clock). And also, please remove the thing about keepScreenOn. And the id's should match i.e. current xml android:id="@+id/clipped_drawer_layout" but in the code findViewById(R.id.drawer_layout) – user743436 Feb 07 '20 at 20:14
  • Updated it, let me know how that looks – Code on the Rocks Feb 08 '20 at 01:07
  • Okay you added styles.xml at the bottom, and that was good. You also removed android:keepScreenOn="true" which was also good. But the words just before keepScreenOn are still there. Therefore it currently looks awkward with "you need to add this to the CustomDrawerLayout: And this to the NavigationView component:" Please also let the id's be the same ("drawer_layout" vs "clipped_drawer_layout") in the code (R.id.drawer_layout) and xml ("@+id/clipped_drawer_layout") since it will otherwise cause unnecessary confusion for all future readers. – user743436 Feb 08 '20 at 10:07
0

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

Code on the Rocks
  • 11,488
  • 3
  • 53
  • 61
  • Now this seems to both behave and look as I wanted, regarding Android 4.4 (which did not look perfect with your previous solution, then with the top of the navigation content coming a bit into the bottom of the app bar). But now instead problems with my Android 7 tablet, with the app being too big vertically, hiding the system clock in the upper right corner. I tried to remove android:fitsSystemWindows="true" in LinearLayoutCompat but it then introduces space below the app bar. – user743436 Feb 06 '20 at 10:27
  • I updated my post again. You need to remove this line from your styles.xml (v21) file:@android:color/transparent. I don't have a tablet to test on but as you can see in my screenshot it no longer covers the status bar on Android. – Code on the Rocks Feb 06 '20 at 17:48
  • Yes it seems to work. However I have now discovered a serious problem which was probably introduced with the added code in MainActivity. I have added some navigation code but now when I navigate away from a top-level option (i.e. an option in the navigation drawer) then you automatically get a left arrow button, but now clicking that arrow does not work but instead toggles the drawer. (BTW the "left" arrow button I am talking about is actually called "Up" button, https://developer.android.com/guide/navigation/navigation-principles ) – user743436 Feb 06 '20 at 22:12
  • I added this line to the MainActivity file above: toolbar.setupWithNavController(navcontroller,drawerLayout). Setting the toolbar up with the navController preserves the up button functionality. Sorry I missed that, still learning about the new navigation components. Thanks for the link! – Code on the Rocks Feb 07 '20 at 00:19
  • Thank you. It is better now but still a problem. I have tested by creating some navigation subcreens for the "Tools" screen with a button at Tools which navigates to a "Tools_B" fragment and from there a button that navigates to fragment "Tool_C". When I have navigated away and then click back with the Up button to the root item "Tools" the arrow indeed becomes a hamburger icon again. However, when I then click it, it *should* open the drawer again, but instead navigates to "Home". – user743436 Feb 07 '20 at 15:32
  • This sounds like it is more of a navigation issue and not so much an issue with the drawer. Did I answer your original question? – Code on the Rocks Feb 07 '20 at 17:19
  • I am sorry so say but no. I wanted the drawer options to be displayed below the app bar but I think it is implicit that it is *not* acceptable to break the normal behaviour regarding what happens when clicking the hamburger icon and the up button. Clicking the hamburger icon should show the drawer (or remove it if is not shown i.e. toggle) but should *not* navigate away to one of the drawer options. – user743436 Feb 07 '20 at 18:25
  • Actually, when I earlier today wrote that "it is better now" I think I was wrong. I then focused too much on the part of the navigation with the up button and thus did not note that the behaviour seems broken (because of the latest "toolbar.setupWithNavController") when just trying to toggle between the top level drawer options. Now the hamburger icon does not close the drawer anymore when it is open i.e. can not toggle anymore. And when I click some of the options, and then click it again, it navigates to "Home" at the first click instead of opening the drawer. – user743436 Feb 07 '20 at 18:25