46

I'm developing an application which uses the navigation drawer pattern (With DrawerLayout).

Each click on a drawer's item, replaces the fragment in the main container.

However, I'm not sure when is the right time to do the fragment transaction? When the drawer starts closing? Or after it is closed?

In google's documentaion example, you can see that they are doing the transaction right after the item click, and then close the drawer.
As a result, the drawer seems laggy and not smooth, and it looks very bad (It happens in my application too).

In Gmail and Google Drive applications, on the other way, It seems like they are doing the transaction after the drawer closed (Am I Right?).
As a result, the drawer is not laggy and very smooth, BUT it takes about 1 second (the time it takes to the drawer get closed) at least, to see the next fragment.

It seems like there is no way the drawer will be smooth when immediately doing fragment transaction.

What do you think about that?

Thanks in advance!

dor506
  • 5,246
  • 9
  • 44
  • 79
  • 1
    I agree! I see jerky drawer close animation when I have my app start a new activity from within the drawer's `onItemClick()`. This is probably because a lot of activity takes place on the UI thread. So now I have to launch a new activity from within `DrawerLayout.DrawerListener` instead... which, like you mentioned, slows down the user experience. – Someone Somewhere Aug 30 '13 at 21:38
  • 2
    is it possible to change the velocity of the Drawer's closing animation? Velocity seems like an important property to expose... yet I can't find it in the documentation!) http://developer.android.com/reference/android/support/v4/widget/DrawerLayout.html – Someone Somewhere Aug 30 '13 at 21:47
  • @SomeoneSomewhere You can change the animation length, but it's not an elegant solution by any means: http://stackoverflow.com/questions/19460683/speed-up-navigation-drawer-animation-speed-on-closing – Makario Jan 21 '14 at 21:38
  • @SomeoneSomewhere I do dislike the jerky close animation, but I dislike making the user wait longer even more. – theblang May 08 '14 at 13:56
  • You can have a look at this solution : http://stackoverflow.com/a/32455989/879154 – Quentin G. Sep 08 '15 at 10:59
  • If animated closing is not so important in your case, the `DrawerLayout` can also be closed without animation: `closeDrawer(View drawerView, boolean animate)`. This method closes the drawer immediately. – Ahmed May 29 '21 at 09:24

7 Answers7

25

Yup, couldn't agree more, performing a fragment (with view) transaction results in a layout pass which causes janky animations on views being animated, citing DrawerLayout docs:

DrawerLayout.DrawerListener can be used to monitor the state and motion of drawer views. Avoid performing expensive operations such as layout during animation as it can cause stuttering; try to perform expensive operations during the STATE_IDLE state.

So please perform your fragment transactions after the drawer is closed or somebody patches the support library to somehow fix that :)

eveliotc
  • 12,863
  • 4
  • 38
  • 34
24

Another solution is to create a Handler and post a delayed Runnable after you close the drawer, as shown here: https://stackoverflow.com/a/18483633/769501. The benefit with this approach is that your fragments will be replaced much sooner than they would be if you waited for DrawerListener#onDrawerClosed(), but of course the arbitrary delay doesn't 100% guarantee the drawer animation will be finished in time.

That said, I use a 200ms delay and it works wonderfully.

private class DrawerItemClickListener implements OnItemClickListener {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
        drawerLayout.closeDrawer(drawerList);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                switchFragments(position); // your fragment transactions go here
            }
        }, 200);
    }
}
Community
  • 1
  • 1
Makario
  • 2,097
  • 21
  • 19
15

This is what I do to achieve an smooth transaction animation similar to Gmail app:

activity_drawer.xml

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

    <!-- The main content view -->
    <FrameLayout
        android:id="@+id/content_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- The navigation drawer -->
    <ListView 
    android:id="@+id/left_drawer"
        android:layout_width="280dp"
        android:layout_height="match_parent"
        android:layout_gravity="left"
        android:choiceMode="singleChoice" />

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

DrawerActivity.java

private Fragment mContentFragment;
private Fragment mNextContentFragment;
private boolean mChangeContentFragment = false;

private Handler mHandler = new Handler();

...

@Override
public void onCreate(Bundle savedInstanceState) {
    ...

    mDrawerLayout.setDrawerListener(new DrawerListener());

    mDrawerList.setOnItemClickListener(new DrawerItemClickListener());

    ...
}

....

private class DrawerItemClickListener implements ListView.OnItemClickListener {

    @Override
    public void onItemClick(AdapterView parent, View view, int position, long id) {
        getSupportFragmentManager().beginTransaction().remove(mContentFragment).commit();

        switch (position) {
            case 0:
                mNextContentFragment = new Fragment1();
                break;

            case 1:
                mNextContentFragment = new Fragment2();
                break;

            case 2:
                mNextContentFragment = new Fragment3();
                break;
        }

        mChangeContentFragment = true;

        mDrawerList.setItemChecked(position, true);

        mHandler.postDelayed(new Runnable() {

            @Override
            public void run() {
                mDrawerLayout.closeDrawer(mDrawerList);
            }           
        }, 150);
    }
}

private class DrawerListener implements android.support.v4.widget.DrawerLayout.DrawerListener {

    @Override
    public void onDrawerClosed(View view) {
        if (mChangeContentFragment) {
             getSupportFragmentManager().beginTransaction().setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN).replace(R.id.content_frame, mNextContentFragment).commit();

             mContentFragment = mNextContentFragment;           
             mNextContentFragment = null;

             mChangeContentFragment = false;
         }
     }
 }

Hope that helps you! :-)

saguinav
  • 497
  • 4
  • 5
  • I like this solution, but I don't really know how to get the back stack behavior working. I could add "addToBackStack" on the replace transaction, but that doesn't work as expected since the previous content was removed. I tried hiding rather than removing, but that doesn't work either. – pqvst Oct 13 '13 at 11:36
  • 1
    @pbergqvist what you might do to get the back stack behavior is: 1. Use another variable to save the "old content fragment" before remove it. 2. Override the onBackPressed method from your activity and replace there the content fragment by your "old content fragment". – saguinav Oct 14 '13 at 15:18
  • @pbergqvist something even better. If you put the "remove fragment" transaction, the mChangeContentFragment=true and the handler post all together inside a method called setContentFragment(Fragment fragment), you can use this method in you onBackPressed in order to not rewrite the same code twice. Does it work for you? – saguinav Oct 14 '13 at 15:25
  • I guess that would work, but only one step back. Otherwise I would have the maintain a stack myself. – pqvst Oct 14 '13 at 19:01
  • 2
    The back stack shouldn't need changed - none of Google's app switch fragments via the back button. That's confusing. – Matthew Reilly Oct 14 '13 at 23:35
5

I know this question is old but I ran into the same problem and figured I would post my solution as I think it is a better implementation than adding a hardcoded delay time. What I did was use the onDrawerClosed function to verify that the drawer IS closed before doing my task.

//on button click...
private void displayView(int position) {
    switch (position) {
    //if item 1 is selected, update a global variable `"int itemPosition"` to be 1
    case 1:
        itemPosition = 1;
        //();
        break;
    default:
        break;
    }

    // update selected item and title, then close the drawer
    mDrawerList.setItemChecked(position, true);
    mDrawerList.setSelection(position);
    mDrawerLayout.closeDrawer(mDrawerList); //close drawer
}

and then in onDrawerClosed, open the corresponding activity.

public void onDrawerClosed(View view) {
    getSupportActionBar().setTitle(mTitle);
    // calling onPrepareOptionsMenu() to show action bar icons
    supportInvalidateOptionsMenu();
    if (itemPosition == 1) {
        Intent intent = new Intent(BaseActivity.this, SecondActivity.class);
        startActivity(intent);
    }
}
Piyush
  • 18,895
  • 5
  • 32
  • 63
user1282637
  • 1,827
  • 5
  • 27
  • 56
2

Just write your code in a handler and put 200 ms delay.

 new Handler().postDelayed(new Runnable() {
  @Override
   public void run() {
       openSelectionDrawerItem(position);          
   }
 }, 200);
chank007
  • 240
  • 2
  • 8
1

Instead of delaying your item clicks which may make your app feel slow. I would just delay the closing of the mDrawerLayout. I would not use the DrawerLayout.OnDrawerListener onClose(...) either because those callbacks are so slow to be called.

new Handler().postDelayed(new Runnable() {
    @Override
    public void run() {
        mDrawerLayout.closeDrawer(GravityCompat.START);
    }
}, 200);
mco
  • 1,809
  • 15
  • 31
0

If you want it smooth and without any delay, leave the drawer open and close it afterwards when returning (in the onRestart() method).

@Override
protected void onRestart() {
    // TODO Auto-generated method stub
    super.onRestart();
    mDrawerLayout.closeDrawer(mDrawerList);     
}

The side effect is an (speedy) animation when returning, but this might be acceptable.

Nesreteip
  • 1
  • 1
  • Returning from what? I just want the drawer to close and my screen to go to another Activity. – IgorGanapolsky Jun 08 '15 at 19:28
  • @Igor the drawer will still be open after returning from another activity using the back button (or any other method that allows you to return to the activity without recreating it). – Lorne Laliberte Jul 10 '15 at 22:28