53

I'm using the Google DrawerLayout.

When an item gets clicked, the drawer is smoothly closed and an Activity will be launched. Turning these activities into Fragments is not an option. Because of this, launching an activity and then closing the drawer is also not an option. Closing the drawer and launching the activity at the same time will make the closing animation stutter.

Given that I want to smoothly close it first, and then launch the activity, I have a problem with the latency between when a user clicks on the drawer item, and when they see the activity they wanted to go to.

This is what the click listener for each item looks like.

final View.OnClickListener mainItemClickListener = new View.OnClickListener() {
    @Override
    public void onClick(final View v) {
        mViewToLaunch = v;
        mDrawerLayout.closeDrawers();
    }
};

My activity is also the DrawerListener, its onDrawerClosed method looks like:

@Override
public synchronized void onDrawerClosed(final View view) {
    if (mViewToLaunch != null) {
        onDrawerItemSelection(mViewToLaunch);
        mViewToLaunch = null;
    }
}

onDrawerItemSelection just launches one of the five activities.

I do nothing on onPause of the DrawerActivity.

I am instrumenting this and it takes on average from 500-650ms from the moment onClick is called, to the moment onDrawerClosed ends.

There is a noticeable lag, once the drawer closes, before the corresponding activity is launched.

I realize a couple of things are happening:

  • The closing animation takes place, which is a couple of milliseconds right there (let's say 300).

  • Then there's probably some latency between the drawer visually closing and its listener getting fired. I'm trying to figure out exactly how much of this is happening by looking at DrawerLayout source but haven't figured it out yet.

  • Then there's the amount of time it takes for the launched activity to perform its startup lifecycle methods up to, and including, onResume. I have not instrumented this yet but I estimate about 200-300ms.

This seems like a problem where going down the wrong path would be quite costly so I want to make sure I fully understand it.

One solution is just to skip the closing animation but I was hoping to keep it around.

How can I decrease my transition time as much as possible?

yarian
  • 5,922
  • 3
  • 34
  • 48
  • *How can I decrease my transition time as much as possible?* - you could use the `onDrawerSlide()` like this https://gist.github.com/luksprog/6316295 , I don't know how much would that save you. Also, what are you doing in the `scheduleLaunchAndCloseDrawer(v);` and in the `onPause()` of the drawer activity? – user Aug 23 '13 at 07:05
  • inScheduleLaunchAndCloseDrawer I am just storing a reference to the view. I later match on its id to determine which Activity to launch. I do nothing onPause. I've tried doing it in onDrawerSlide but it would also stutter. I tried doing it past a certain threshold of 80%. – yarian Aug 23 '13 at 09:33
  • I have something I want to try out tomorrow, which is posting a runnable to launch the activity at some predetermined delay, say 350-400ms. This might still stutter, essentially the goal would be to reduce the latency between the drawer closing and the listener getting fired to zero. I'll update question when I try it. – yarian Aug 23 '13 at 09:35
  • That might be a solution but posting runnables at arbitrary time intervals doesn't sound such a good idea. You might as well try using `Handler.postAtFrontOfQueue(Runnable)` to post a `Runnable` starting the activity in the `onDrawerClosed()` callback. – user Aug 23 '13 at 10:12
  • I agree that it sounds hacky. I'll try both. My suspicion is that a big part of the latency is between me visually perceiving the drawer as closed an Android calling `onDrawerClosed()`, which I believe `postAtFrontOfQueue` would not help fix. But I'll give both a shot and report back. – yarian Aug 23 '13 at 20:25
  • I should mention that if I do the posting at an interval it would be done on a Handler owned by the DrawerActivity, that way a double click wouldn't result in two things getting fired and so on. – yarian Aug 23 '13 at 20:26
  • @Luksprog Ended up doing the posting thing. The problem with postAtFrontOfQueue is that it depends on `onDrawerClosed` getting called, which was the big source of the latency. This solution would have worked great if `onDrawerClosed` was getting called right away, but there were lots of things in the event loop, but that wasn't the big problem in this case. See answer for more details. – yarian Aug 27 '13 at 00:35
  • please find my answer for this problem [Open next activity only after navigation drawer completes it closing animation][1] [1]: http://stackoverflow.com/a/29049221/2626901 – Vishal Makasana Mar 14 '15 at 13:14

7 Answers7

42

According the docs,

Avoid performing expensive operations such as layout during animation as it can cause stuttering; try to perform expensive operations during the STATE_IDLE state.

Instead of using a Handler and hard-coding the time delay, you can override the onDrawerStateChanged method of ActionBarDrawerToggle (which implements DrawerLayout.DrawerListener), so that you can perform the expensive operations when the drawer is fully closed.

Inside MainActivity,

private class SmoothActionBarDrawerToggle extends ActionBarDrawerToggle {

    private Runnable runnable;

    public SmoothActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, Toolbar toolbar, int openDrawerContentDescRes, int closeDrawerContentDescRes) {
        super(activity, drawerLayout, toolbar, openDrawerContentDescRes, closeDrawerContentDescRes);
    }

    @Override
    public void onDrawerOpened(View drawerView) {
        super.onDrawerOpened(drawerView);
        invalidateOptionsMenu();
    }
    @Override
    public void onDrawerClosed(View view) {
        super.onDrawerClosed(view);
        invalidateOptionsMenu();
    }
    @Override
    public void onDrawerStateChanged(int newState) {
        super.onDrawerStateChanged(newState);
        if (runnable != null && newState == DrawerLayout.STATE_IDLE) {
            runnable.run();
            runnable = null;
        }
    }

    public void runWhenIdle(Runnable runnable) {
        this.runnable = runnable;
    }
}

Set the DrawerListener in onCreate:

mDrawerToggle = new SmoothActionBarDrawerToggle(this, mDrawerLayout, mToolbar, R.string.open, R.string.close);
mDrawerLayout.setDrawerListener(mDrawerToggle);

Finally,

private void selectItem(int position) {
    switch (position) {
        case DRAWER_ITEM_SETTINGS: {
            mDrawerToggle.runWhenIdle(new Runnable() {
                @Override
                public void run() {
                    Intent intent = new Intent(MainActivity.this, SettingsActivity.class);
                    startActivity(intent);
                }
            });
            mDrawerLayout.closeDrawers();
            break;
        }
        case DRAWER_ITEM_HELP: {
            mDrawerToggle.runWhenIdle(new Runnable() {
                @Override
                public void run() {
                    Intent intent = new Intent(MainActivity.this, HelpActivity.class);
                    startActivity(intent);
                }
            });
            mDrawerLayout.closeDrawers();
            break;
        }
    }
}
TheGreatOne
  • 535
  • 6
  • 6
  • 5
    I believe this is the better answer. It refers to the docs and implements a method based off of the recommendation made in the documentation. +1 – dsrees Jul 29 '15 at 17:48
  • 2
    But when using this, you should Run the first fragment immediately to work on First Activity launch – Sheychan Nov 09 '15 at 08:36
  • 1
    which function calls selectItem() ? is it overriden by a superclass? – Libathos Feb 13 '17 at 11:36
  • @libathos mDrawerToggle.runWhenIdle(...) and mDrawerLayout.closeDrawers() are the points and no matter where to use it. – Chan Chun Him Mar 27 '17 at 02:42
  • @Zhang NS, you call runnable.run(), using Runnable as a simple interface for function, maybe use something else, or run the runnable in a Handler.post as it should go. – alekshandru May 30 '18 at 11:30
27

I was facing same issue with DrawerLayout.

I have research for that and then find one nice solution for it.

What i am doing is.....

If you refer Android Sample app for the DrawerLayout then check the code for selectItem(position);

In this function based on the position selection fragment is called. I have modify it with below code as per my need and works fine with no animation close stutter.

private void selectItem(final int position) {
    //Toast.makeText(getApplicationContext(), "Clicked", Toast.LENGTH_SHORT).show();
    mDrawerLayout.closeDrawer(drawerMain);
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            Fragment fragment = new TimelineFragment(UserTimeLineActivity.this);
            Bundle args = new Bundle();
            args.putInt(TimelineFragment.ARG_PLANET_NUMBER, position);
            fragment.setArguments(args);

            FragmentManager fragmentManager = getSupportFragmentManager();
            fragmentManager.beginTransaction().replace(R.id.content_frame, fragment).commit();

            // update selected item and title, then close the drawer
            mCategoryDrawerList.setItemChecked(position, true);

            setTitle("TimeLine: " + mCategolyTitles[position]);
        }
    }, 200);


    // update the main content by replacing fragments


}

Here i am first closing the DrawerLayout. which takes approx 250 miliseconds. and then my handler will call the fragment. Which works smooth and as per the requirement.

Hope it will also helpful to you.

Enjoy Coding... :)

Shreyash Mahajan
  • 23,386
  • 35
  • 116
  • 188
20

So I seem to have solved the problem with a reasonable solution.

The largest source of perceivable latency was the delay between when the drawer was visually closed, and when onDrawerClosed was called. I solved this by posting a Runnable to a private Handler that launches the intended activity at some specified delay. This delay is chosen to correspond with the drawer closing.

I tried to do the launching onDrawerSlide after 80% progress, but this has two problems. The first was that it stuttered. The second was that if you increased the percentage to 90% or 95% the likelihood that it wouldn't get called at all due to the nature of the animation increased--and you then had to fall back to onDrawerClosed, which defeats the purpose.

This solution has the possibility to stutter, specially on older phones, but the likelihood can be reduced to 0 simply by increasing the delay high enough. I thought 250ms was a reasonable balance between stutter and latency.

The relevant portions of the code look like this:

public class DrawerActivity extends SherlockFragmentActivity {
    private final Handler mDrawerHandler = new Handler();

    private void scheduleLaunchAndCloseDrawer(final View v) {
        // Clears any previously posted runnables, for double clicks
        mDrawerHandler.removeCallbacksAndMessages(null); 

        mDrawerHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                onDrawerItemSelection(v);
            }
        }, 250);
        // The millisecond delay is arbitrary and was arrived at through trial and error

        mDrawerLayout.closeDrawer();
    }
}
yarian
  • 5,922
  • 3
  • 34
  • 48
  • is your issue solve with this? My answer is working well as per your requirement. – Shreyash Mahajan Aug 29 '13 at 03:53
  • 2
    What do you mean? I posted this answer before yours. It is very similar to yours but doesn't create a Handler each time, which allows you to cancel previous scheduled launches. – yarian Aug 29 '13 at 07:00
  • You have created seperate class to handle it, while i have in its own class by creating handler only. You are right in your way and i am also in my way. Enjoy Coding... – Shreyash Mahajan Aug 29 '13 at 07:13
  • 2
    Be careful with committing a fragment transaction in onDrawerItemSelection(v), since it will cause an IllegalStateException when the app is being stopped just after the user chooses a drawer item, but just before the drawer has been closed. – Divisible by Zero Mar 25 '14 at 10:51
  • @CiskeBoekelo You're quite right. This solution is specifically for launching activities. If I was using the drawer to toggle between Fragments, I'd probably do the fragment transaction immediately and then hide the drawer. – yarian Mar 25 '14 at 21:11
10

Google IOsched 2015 runs extremely smoothly (except from settings) the reason for that is how they've implemented the drawer and how they launch stuff.

First of they use handler to launch delayed:

        // launch the target Activity after a short delay, to allow the close animation to play
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                goToNavDrawerItem(itemId);
            }
        }, NAVDRAWER_LAUNCH_DELAY);

with delay being:

private static final int NAVDRAWER_LAUNCH_DELAY = 250;

Another thing they do is to remove animations from the activities that are launched with following code inside the activities onCreate():

overridePendingTransition(0, 0);

To view the source go to git.

Warpzit
  • 27,966
  • 19
  • 103
  • 155
4

I'm using approach like below. Works smoothly.

public class MainActivity extends BaseActivity implements NavigationView.OnNavigationItemSelectedListener {

    private DrawerLayout drawerLayout;
    private MenuItem menuItemWaiting;

    /* other stuff here ... */

    private void setupDrawerLayout() {

        /* other stuff here ... */

        drawerLayout.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
            @Override
            public void onDrawerClosed(View drawerView) {
                super.onDrawerClosed(drawerView);
                if(menuItemWaiting != null) {
                    onNavigationItemSelected(menuItemWaiting);
                }
            }
        });

    }

    @Override
    public boolean onNavigationItemSelected(MenuItem menuItem) {

        menuItemWaiting = null;
        if(drawerLayout.isDrawerOpen(GravityCompat.START)) {
            menuItemWaiting = menuItem;
            drawerLayout.closeDrawers();
            return false;
        };

        switch(menuItem.getItemId()) {
            case R.id.drawer_action:
                startActivity(new Intent(this, SecondActivity.class));

            /* other stuff here ... */

        }
        return true;
    }
}

The same with ActionBarDrawerToggle:

drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close){
    @Override
    public void onDrawerClosed(View drawerView) {
        super.onDrawerClosed(drawerView);
        if(menuItemWaiting != null) {
            onNavigationItemSelected(menuItemWaiting);
        }
    }
};
drawerLayout.setDrawerListener(drawerToggle);
marioosh
  • 27,328
  • 49
  • 143
  • 192
2

A better approach would be to use the onDrawerSlide(View, float) method and start the Activity once the slideOffset is 0. See below

public void onDrawerSlide(View drawerView, float slideOffset) {
    if (slideOffset <= 0 && mPendingDrawerIntent != null) {
        startActivity(mPendingDrawerIntent);
        mPendingDrawerIntent = null;
    }
}

Just set the mPendingDrawerIntent in the Drawer's ListView.OnItemClickListener onItemClick method.

oracleicom
  • 918
  • 4
  • 11
  • 19
0

This answer is for guys who uses RxJava and RxBinding. Idea is to prevent the activity launch until drawer closes. NavigationView is used for displaying the menu.

public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener{

  private DrawerLayout drawer;

  private CompositeDisposable compositeDisposable;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // setup views and listeners (NavigationView.OnNavigationItemSelectedListener)

    compositeDisposable = new CompositeDisposable();
    compositeDisposable.add(observeDrawerClose());

  }

  // uncomment if second activitiy comes back to this one again
  /*
  @Override
  protected void onPause() {
      super.onPause();
      compositeDisposable.clear();
  }

  @Override
  protected void onResume() {
     super.onResume();
     compositeDisposable.add(observeDrawerClose());
  }*/

  @Override
  protected void onDestroy() {
    super.onDestroy();
    compositeDisposable.clear();
  }

  @Override
  public boolean onNavigationItemSelected(MenuItem item) {
    // Handle navigation view item clicks here.
    int id = item.getItemId();

    navSubject.onNext(id);

    drawer.closeDrawer(GravityCompat.START);
    return true;
  }

  private Disposable observeDrawerClose() {
    return RxDrawerLayout.drawerOpen(drawer, GravityCompat.START)
        .skipInitialValue() // this is important otherwise caused to zip with previous drawer event
        .filter(open -> !open)
        .zipWith(navSubject, new BiFunction<Boolean, Integer, Integer>() {
          @Override
          public Integer apply(Boolean aBoolean, Integer u) throws Exception {
            return u;
          }
        }).subscribe(id -> {
          if (id == R.id.nav_home) {
            // Handle the home action
          } else {

          }
        });
  }
}
Ruwanka De Silva
  • 3,555
  • 6
  • 35
  • 51