59

I am using the Navigation Drawer pattern from the support library: http://developer.android.com/training/implementing-navigation/nav-drawer.html

I was trying to set it as always opened on tablet (as a side menu)

enter image description here

Is that something possible with the current implementation, or do we have to create a new layout and a new structure with a Listview instead of reusing the same code?

Waza_Be
  • 39,545
  • 47
  • 176
  • 256

5 Answers5

47

Based on the idea of larger devices could have different layout files, I have created the follow project.

https://github.com/jiahaoliuliu/ABSherlockSlides

HighLights:

Since the drawer of a large device is always visible, there is not need to have an drawer. Instead, a LinearLayout with two elements with the same name will be enough.

<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="horizontal">
     <ListView
             android:id="@+id/listview_drawer"
             android:layout_width="@dimen/drawer_size"
             android:layout_height="match_parent"
             android:layout_gravity="start"
             android:choiceMode="singleChoice"
             android:divider="@android:color/transparent"
             android:dividerHeight="0dp"
             android:background="@color/drawer_background"/>
    <FrameLayout
            android:id="@+id/content_frame"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginLeft="@dimen/drawer_content_padding"
            />
</LinearLayout>

Because we don't have the drawer in the layout file, when the app try to find the element in the layout, it will return null. So, there is not need to have an extra boolean to see which layout is using.

DrawerLayout mDrawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout);

if (mDrawerLayout != null) {
    // Set a custom shadow that overlays the main content when the drawer opens
    mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
    // Enable ActionBar app icon to behave as action to toggle nav drawer
    getSupportActionBar().setHomeButtonEnabled(true);
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);

    // ActionBarDrawerToggle ties together the proper interactions
    // between the sliding drawer and the action bar app icon
    mDrawerToggle = new ActionBarDrawerToggle(
            this,
            mDrawerLayout,
            R.drawable.ic_drawer,
            R.string.drawer_open,
            R.string.drawer_close) {

        public void onDrawerClosed(View view) {
            super.onDrawerClosed(view);
        }

        public void onDrawerOpened(View drawerView) {
            // Set the title on the action when drawer open
            getSupportActionBar().setTitle(mDrawerTitle);
            super.onDrawerOpened(drawerView);
        }
    };

    mDrawerLayout.setDrawerListener(mDrawerToggle);
}

Here is the example to use it as boolean.

@Override
protected void onPostCreate(Bundle savedInstanceState) {
    super.onPostCreate(savedInstanceState);
    if (mDrawerLayout != null) {
        mDrawerToggle.syncState();
    }
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    if (mDrawerLayout != null) {
        // Pass any configuration change to the drawer toggles
        mDrawerToggle.onConfigurationChanged(newConfig);
    }
}
jiahao
  • 3,279
  • 2
  • 32
  • 34
  • 1
    How to know for sure it's a tablet device? Is using `res/layout-sw600dp-land` sufficient? – Alexander Farber Aug 10 '15 at 09:34
  • 1
    This is an old but interesting question. You might find answer here: http://stackoverflow.com/questions/11330363/how-to-detect-device-is-android-phone-or-android-tablet Note that just by creating this folder and placing the layout in it, it does not means this layout will be used. If you want, you can detect if it is tablet or not, and then force it to be in landscape mode programatically. Then this will work. Good luck! – jiahao Aug 10 '15 at 09:53
32

Building upon CommonsWare's answer you can do this with a couple of adjustments. The first is setting the following three lines:

drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN);
drawerLayout.setScrimColor(getResources().getColor(R.color.drawerNoShadow));
isDrawerLocked = true;

The drawerNoShadow color can just be a no-alpha color (like 0x00000000). That gets you an open drawer with no background overlay.

The second thing you need to do is adjust the padding_left value of your FrameLayout. For this purpose you can setup a dimension to control this (0dp by default) - in this example R.dimen.drawerContentPadding. You will also need an R.dimen.drawerSize value that will be the width of the DrawerLayout.

This allows you to check the paddingLeft value of the FrameLayout to call those lines.

FrameLayout frameLayout = (FrameLayout)findViewById(R.id.content_frame);
if(frameLayout.getPaddingLeft() == (int)getResources().getDimension(R.dimen.drawerSize) {
    drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN);
    drawerLayout.setScrimColor(getResources().getColor(R.color.drawerNoShadow));
    isDrawerLocked = true;
}

You can then wrap all the functionality you don't want to enable in an if(!isDrawerLocked) statement. This will include:

  • drawerLayout.setDrawerListener(drawerToggle);
  • getActionBar().setDisplayHomeAsUpEnabled(true);

Lastly you do need to setup alternate layouts for the views with a static drawer. An example is:

<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.DrawerLayout

    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- The navigation drawer -->
    <ListView
        android:id="@+id/left_drawer"
        android:layout_width="@dimen/drawerSize"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:choiceMode="singleChoice"
        android:divider="@android:color/transparent"
        android:dividerHeight="0dp"
        android:background="#111"/>

</android.support.v4.widget.DrawerLayout>
<FrameLayout
    android:id="@+id/content_frame"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="@dimen/drawerContentPadding"/>

The beauty here is you can then control all of the logic by setting up alternate dimen.xml files for the devices you want to target and the only thing you need to change is the value of drawerContentPadding and offer the modified layouts.

NOTE: I ended up using margin_left instead of padding_left since in the new layout it overlays the drawer. See a more in-depth blog post about the technique at http://derekrwoods.com/2013/09/creating-a-static-navigation-drawer-in-android/

methodin
  • 6,473
  • 1
  • 21
  • 25
  • Great example! Since you changed it to padding_left instead of margin, you can't detect which layout anymore with your example. However, if you set a tag on each of the layouts you can compare the tag to find which layout is in use. *** One more thing though - I find that after having the drawer locked in the out position, when I switch back to portrait so it would open/close the swipe action no longer opens the drawer. Any idea why? – Tony Maro Sep 23 '13 at 16:41
  • I also just realized that after swapping from the open drawer back to portrait mode, my onKeyDown event stops firing for the activity! I'm using the compatibility fragments, I wonder if there's a bug in there somewhere... – Tony Maro Sep 23 '13 at 16:55
  • 2
    I resolved most of my issues by moving this code into onPostCreate(), but I still get a crash if you try to swipe the drawer closed while it is locked open. – Tony Maro Sep 23 '13 at 17:10
  • Okay so to continue my private discussion - I fixed the back button issue and crashes as well. I had to put a dummy framelayout inside the DrawerLayout for tablet views to stop the crash. Then I had to set mDrawerLayout.setFocusableInTouchMode(false); to keep it from hogging the touch input. – Tony Maro Sep 23 '13 at 17:40
  • Let me see if I can dig this code up - I have it fully integrated in two apps. – methodin Sep 24 '13 at 16:59
  • I've added a more detailed explanation about the process I used at http://derekrwoods.com/2013/09/creating-a-static-navigation-drawer-in-android/ – methodin Sep 24 '13 at 22:53
  • Thanks! I wonder how many of the issues I ran into though were due to differences with the compatibility library. I expect the crashes on touch were which caused me to move the code into onPostCreate(). Wit this answer's help I was able to finish my app this past weekend and got it released. Worked like a charm. – Tony Maro Sep 25 '13 at 18:42
  • Glad to hear that. I imagine the Compat libraries would have varying differences - it might be worthwhile for you to put an article somewhere and link to it for anyone else working through this with the Compat library! – methodin Sep 25 '13 at 19:05
  • i am getting nullpointer exception while touching the content_frame. – madan V Mar 10 '14 at 06:16
  • @Tony Maro - "I find that after having the drawer locked in the out position, when I switch back to portrait so it would open/close the swipe action no longer opens the drawer. Any idea why?" How did you solve this issue? any suggestions pls – Akh Feb 11 '15 at 05:08
  • The drawer seems to be stuck in open positions when switched back to portrait mode. Fixed this issue by unlocking the drawer which was previously locked in the landscape mode. drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); – Akh Feb 11 '15 at 06:02
  • 1
    @methodin I'd really appreciated if you could add the full code to your blog. – eddy Mar 02 '15 at 03:17
  • @SamerZuhair Yeah I tried but there are too many parts missing that ,as beginner, I can't make it run :( – eddy Mar 02 '15 at 12:13
  • 1
    @eddy if you know how to run the code http://stackoverflow.com/a/28809296/2148631 – zaPlayer Mar 02 '15 at 12:16
  • Does this code work? How would I revert back to a draggabble navigation-drawer? And, what should be the trigger (in code) of when to use this? – android developer May 01 '15 at 16:51
  • In order to fix landscape/portrait drawer dragging issue, simply place `android:saveEnabled="false"` into `DrawerLayout` tag in both xml files. This disables the previous state of the view when rotation is changed, causing the drawer reset. – waqaslam May 26 '15 at 12:23
14

Try setDrawerLockMode() to lock the drawer open on large-screen devices.

As I noted in a comment, I don't think that DrawerLayout is designed for your scenario (though it's not a bad idea, IMHO). Either use a different layout that hosts the same ListView and content, or perhaps download and modify your own copy of DrawerLayout that, on large-screen devices, slides the content over when opened rather than overlaps it.

CommonsWare
  • 910,778
  • 176
  • 2,215
  • 2,253
  • 1
    The drawer is opened, but is in front of my actual layout. – Waza_Be Jun 19 '13 at 11:37
  • @Waza_Be: Oh, right. I don't think that `DrawerLayout` is designed for your scenario. Either use a different layout that hosts the same `ListView` and content, or perhaps download and modify your own copy of `DrawerLayout` that, on large-screen devices, slides the content over when opened rather than overlaps it. – CommonsWare Jun 19 '13 at 12:39
  • I accepted your answer, but would be usefull if you copy your comment inside the answer ;-) – Waza_Be Jun 19 '13 at 12:49
8

Simply provide an alternate layout file for tablets. This way, you can save all the default behaviours of NavigationView.


Step 1

Simply create an alternate layout file similar to this for tablet devices and place it in layout-w600dp-land resource directory.

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

    <!--
    NavigationView and the content is placed in a horizontal LinearLayout
    rather than as the direct children of DrawerLayout.
    This makes the NavigationView always visible.
    -->

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <android.support.design.widget.NavigationView
            android:id="@+id/nav"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:headerLayout="@layout/nav_header_main"
            app:menu="@menu/activity_main_drawer"/>
        <include
            layout="@layout/app_bar_main"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </LinearLayout>

</android.support.v4.widget.DrawerLayout>



Step 2

In this step we will be doing enough changes to make sure the opening and closing of drawer works only in non-tablet devices.

Step 2.1

Add the following content to a new value resource file in values directory and name it config_ui.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <bool name="isDrawerFixed">false</bool>
</resources>

That was for non-tablet devices. For tablet devices, create another one with the same name and place it in values-w600dp-land.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <bool name="isDrawerFixed">true</bool>
</resources>

Create a new field in the class of the activity the drawer belongs to as
private boolean isDrawerFixed;
and initialize it as
isDrawerFixed = getResources().getBoolean(R.bool.isDrawerFixed);.

Now we can check if the device is a tabled or a non-tablet as simple as if (isDrawerFixed){}.

Step 2.2

Wrap the code which sets up toggle button on the actionbar with an if statement like this.

if (!isDrawerFixed) {
    ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
    drawer.addDrawerListener(toggle);
    toggle.syncState();
}

Wrap the code which closes the drawer when an item is clicked with another if statement like this.

if (!isDrawerFixed) {
    drawer.closeDrawer(GravityCompat.START);
}




The final result will look somewhat like this.

output

Bertram Gilfoyle
  • 8,353
  • 5
  • 36
  • 61
  • 1
    This is a great answer. Much easier and sensible than the others provided. Thank you! I used a `RelativeLayout` rather than a `LinearLayout`, I couldn’t get the main/detail fragment to display using just a `LinearLayout`. Thanks again. Extra points for providing screenshots for proof and comparing against my device. – Sakiboy Aug 10 '20 at 23:34
7

Previous answers are good, but I faced some problems while implementing them in my project, so I want to share my solution. First of all, we need to define a custom drawer:

public class MyDrawerLayout extends DrawerLayout {
    private boolean m_disallowIntercept;

    public MyDrawerLayout (Context context) {
        super(context);
    }

    @Override
    public boolean onInterceptTouchEvent(final MotionEvent ev) {
        // as the drawer intercepts all touches when it is opened
        // we need this to let the content beneath the drawer to be touchable
        return !m_disallowIntercept && super.onInterceptTouchEvent(ev);
    }

    @Override
    public void setDrawerLockMode(int lockMode) {
        super.setDrawerLockMode(lockMode);
        // if the drawer is locked, then disallow interception
        m_disallowIntercept = (lockMode == LOCK_MODE_LOCKED_OPEN);
    }
}

Then we put it in a basic activity layout (without arbitrary layouts from previous answers) like this:

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

    <!--We must define left padding for content-->
    <FrameLayout
        android:id="@+id/content_frame"
        android:paddingStart="@dimen/content_padding"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <android.support.design.widget.NavigationView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        app:menu="@menu/menu_nav" />

</MyDrawerLayout>

Content padding here is 0dp in portrait orientation and about 300dp in landscape for NavigationView (figured out empirically). We define them in appropriate values folders:

values/dimens.xml -

<dimen name="content_padding">0dp</dimen>

values-land/dimens.xml -

<dimen name="content_padding">300dp</dimen>

Finally, we lock the drawer in the activity:

    if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
        mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN);
        mDrawerLayout.setScrimColor(0x00000000); // or Color.TRANSPARENT
        isDrawerLocked = true;
    } else if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
        mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
        mDrawerLayout.setScrimColor(0x99000000); // default shadow
        isDrawerLocked = false;
    }
mister_potato
  • 118
  • 3
  • 5
  • 1
    I think this is by far the best answer, as there isn't duplicate code involved and it leverages the NavigationView API making way for the Navigation Graphs. Nice job. – John Shelley Jan 07 '19 at 19:26