Reputation: 1124
So I have implemented the navigation drawer and it works fine. I have action bar item that takes the uses to next activity. When entering the second activity by clicking that action bar icon, if the navigation drawer is open then it remains open even after the user returns back to first activity. I tried using
drawerLayout.closeDrawer(drawerListView);
after the intent is called but what happens is the second activity starts after the animation to close first activity is completed. This creates a bad user experience and even I dont like it.
So it there any way I could close the drawer after the second activity gets created? I mean from the second activtiy's onCreate or somewhere?
Upvotes: 3
Views: 6973
Reputation: 7804
You can use the new DrawerLayout.closeDrawer(int/View, bool)
methods in v24 of the support library to instantly close a drawer:
drawerLayout.closeDrawer(Gravity.LEFT, false);
Place in your onResume
if you want the drawer to animate closed on item click, but be closed when you go back to the activity from another one.
Upvotes: 7
Reputation: 6311
You can almost simulate closing the drawer after arriving at an activity, by passing a value in the intent to tell the new activity to open its drawer with no animation from onCreate()
and then animate it closed after the activity's layout is finished, however in my experiments the activity transition ruined the effect of the simulation.
An alternative is to avoid the drawer animation and just call startActivity()
without calling closeDrawer()
. The activity transition animation still provides a nice effect, and it occurs immediately, with no need to wait for the drawer close animation to finish settling first, no choppiness, no long perceptual delays.
However, you will need a way to close the drawer without animation when navigating back to the original activity with the back button.
(You can skip past this explanation if you just want to see the code.)
To make this work you need a way to close the drawer without any close animation when navigating back to the activity with the back button. (Not calling closeDrawer()
will leave the drawer open in that activity instance; a relatively wasteful workaround would be to just force the activity to recreate()
when navigating back, but it's possible to solve this without doing that.) You also need to make sure you only close the drawer if you're returning after navigating, and not after an orientation change, but that's easy.
Although calling closeDrawer()
from onCreate()
will make the drawer start out closed without any animation, the same is not true from onResume()
. Calling closeDrawer()
from onResume()
will close the drawer with an animation that is momentarily visible to the user. DrawerLayout
doesn't provide any method to close the drawer without that animation, but it's not difficult to add one.
Closing the drawer actually just slides it off the screen. So you can effectively skip the animation by moving the drawer directly to its "closed" position. The translation direction will vary according to the gravity (whether it's a left or right drawer), and the exact position depends on the size of the drawer once it's laid out with all its children.
However, simply moving it isn't quite enough, as DrawerLayout
keeps some internal state in extended LayoutParams
that it uses to know whether the drawer is open. If you just move the drawer off screen, it won't know that it's closed, and that will cause other problems. (For example, the drawer will reappear on the next orientation change.)
Since you're compiling the support library into your app, you can create a class in the android.support.v4.widget
package to gain access to its default (package-private) parts, or extend DrawerLayout
without copying over any of the other classes it needs. This will also reduce the burden of updating your code with future changes to the support library. (It's always best to insulate your code from implementation details as much as possible.) You can use moveDrawerToOffset()
to move the drawer, and set the LayoutParams
so it will know that the drawer is closed.
This is the code that'll skip the animation:
// move drawer directly to the closed position
moveDrawerToOffset(drawerView, 0.f);
/* EDIT: as of v23.2.1 this direct approach no longer works
because the LayoutParam fields have been made private...
// set internal state so DrawerLayout knows it's closed
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
lp.onScreen = 0.f;
lp.knownOpen = false;
invalidate();
/*/
// ...however, calling closeDrawer will set those LayoutParams
// and invalidate the view.
closeDrawer(drawerView);
/**/
Note: if you just call moveDrawerToOffset()
without changing the LayoutParams
, the drawer will move back to its open position on the next orientation change.
This approach adds a utility class to the support.v4 package to gain access to the package-private parts we need inside DrawerLayout.
Place this class into /src/android/support/v4/widget/:
package android.support.v4.widget;
import android.support.annotation.IntDef;
import android.support.v4.view.GravityCompat;
import android.view.Gravity;
import android.view.View;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class Support4Widget {
/** @hide */
@IntDef({Gravity.LEFT, Gravity.RIGHT, GravityCompat.START, GravityCompat.END})
@Retention(RetentionPolicy.SOURCE)
private @interface EdgeGravity {}
public static void setDrawerClosed(DrawerLayout drawerLayout, @EdgeGravity int gravity) {
final View drawerView = drawerLayout.findDrawerWithGravity(gravity);
if (drawerView == null) {
throw new IllegalArgumentException("No drawer view found with gravity " +
DrawerLayout.gravityToString(gravity));
}
// move drawer directly to the closed position
drawerLayout.moveDrawerToOffset(drawerView, 0.f);
/* EDIT: as of v23.2.1 this no longer works because the
LayoutParam fields have been made private, but
calling closeDrawer will achieve the same result.
// set internal state so DrawerLayout knows it's closed
final DrawerLayout.LayoutParams lp = (DrawerLayout.LayoutParams) drawerView.getLayoutParams();
lp.onScreen = 0.f;
lp.knownOpen = false;
drawerLayout.invalidate();
/*/
// Calling closeDrawer updates the internal state so DrawerLayout knows it's closed
// and invalidates the view for us.
drawerLayout.closeDrawer(drawerView);
/**/
}
}
Set a boolean in your activity when you navigate away, indicating the drawer should be closed:
public static final String CLOSE_NAV_DRAWER = "CLOSE_NAV_DRAWER";
private boolean mCloseNavDrawer;
@Override
public void onCreate(Bundle savedInstanceState) {
// ...
if (savedInstanceState != null) {
mCloseNavDrawer = savedInstanceState.getBoolean(CLOSE_NAV_DRAWER);
}
}
@Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
// ...
startActivity(intent);
mCloseNavDrawer = true;
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putBoolean(CLOSE_NAV_DRAWER, mCloseNavDrawer);
super.onSaveInstanceState(savedInstanceState);
}
...and use the setDrawerClosed()
method to shut the drawer in onResume()
with no animation:
@Overrid6e
protected void onResume() {
super.onResume();
if(mCloseNavDrawer && mDrawerLayout != null && mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
Support4Widget.setDrawerClosed(mDrawerLayout, GravityCompat.START);
mCloseNavDrawer = false;
}
}
This approach extends DrawerLayout to add a setDrawerClosed() method.
Place this class into /src/android/support/v4/widget/:
package android.support.v4.widget;
import android.content.Context;
import android.support.annotation.IntDef;
import android.support.v4.view.GravityCompat;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class CustomDrawerLayout extends DrawerLayout {
/** @hide */
@IntDef({Gravity.LEFT, Gravity.RIGHT, GravityCompat.START, GravityCompat.END})
@Retention(RetentionPolicy.SOURCE)
private @interface EdgeGravity {}
public CustomDrawerLayout(Context context) {
super(context);
}
public CustomDrawerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomDrawerLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setDrawerClosed(View drawerView) {
if (!isDrawerView(drawerView)) {
throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer");
}
// move drawer directly to the closed position
moveDrawerToOffset(drawerView, 0.f);
/* EDIT: as of v23.2.1 this no longer works because the
LayoutParam fields have been made private, but
calling closeDrawer will achieve the same result.
// set internal state so DrawerLayout knows it's closed
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
lp.onScreen = 0.f;
lp.knownOpen = false;
invalidate();
/*/
// Calling closeDrawer updates the internal state so DrawerLayout knows it's closed
// and invalidates the view for us.
closeDrawer(drawerView);
/**/
}
public void setDrawerClosed(@EdgeGravity int gravity) {
final View drawerView = findDrawerWithGravity(gravity);
if (drawerView == null) {
throw new IllegalArgumentException("No drawer view found with gravity " +
gravityToString(gravity));
}
// move drawer directly to the closed position
moveDrawerToOffset(drawerView, 0.f);
/* EDIT: as of v23.2.1 this no longer works because the
LayoutParam fields have been made private, but
calling closeDrawer will achieve the same result.
// set internal state so DrawerLayout knows it's closed
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
lp.onScreen = 0.f;
lp.knownOpen = false;
invalidate();
/*/
// Calling closeDrawer updates the internal state so DrawerLayout knows it's closed
// and invalidates the view for us.
closeDrawer(drawerView);
/**/
}
}
Use CustomDrawerLayout
instead of DrawerLayout
in your activity layouts:
<android.support.v4.widget.CustomDrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
>
...and set a boolean in your activity when you navigate away, indicating the drawer should be closed:
public static final String CLOSE_NAV_DRAWER = "CLOSE_NAV_DRAWER";
private boolean mCloseNavDrawer;
@Override
public void onCreate(Bundle savedInstanceState) {
// ...
if (savedInstanceState != null) {
mCloseNavDrawer = savedInstanceState.getBoolean(CLOSE_NAV_DRAWER);
}
}
@Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
// ...
startActivity(intent);
mCloseNavDrawer = true;
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putBoolean(CLOSE_NAV_DRAWER, mCloseNavDrawer);
super.onSaveInstanceState(savedInstanceState);
}
...and use the setDrawerClosed()
method to shut the drawer in onResume()
with no animation:
@Overrid6e
protected void onResume() {
super.onResume();
if(mCloseNavDrawer && mDrawerLayout != null && mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
mDrawerLayout.setDrawerClosed(GravityCompat.START);
mCloseNavDrawer = false;
}
}
Upvotes: 0
Reputation: 1255
After too long looking through the source of DrawerLayout.java I have found a way. Run this when the user returns to the first activity to close the drawer without running the animation:
View view = drawerLayout.getChildAt(drawerLayout.getChildCount() - 1);
ViewTreeObserver vto = view.getViewTreeObserver();
vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
final DrawerLayout.LayoutParams lp = new DrawerLayout.LayoutParams(view.getWidth(), view.getHeight());
lp.gravity = Gravity.LEFT;
view.setLayoutParams(lp);
view.setLeft(-view.getMeasuredWidth());
view.getViewTreeObserver().removeOnPreDrawListener(this);
return true;
}
});
Explanation
First find the view that corresponds to the navigation view, this will be the last child of drawerLayout. Set the left position to minus its width (for left side drawers).
As Lorne Laliberte mentioned, you also need to change LayoutParams.knownOpen to false for this to work, however there is no way to access this short of creating a local copy of appcompat-v4 and editing it - as this is a private field. This is where my trick comes in. In java a default boolean is set to false. Creating a new LayoutParams with the old width and height will result in one with knownOpen set to false. We can then set this to the view overwriting the old LayoutParams. This needs to be put inside an predraw listener in case the view hasn't been layed out yet, for example after the screen was rotated.
Please ask me if anyone has trouble getting this to work.
Upvotes: 5