...a hack!

Edit: Thanks to Lorne Laliberte for a link to some other good methods of controlling the navigation drawer animation duration. Be sure to check it out to explore the topic more.

The past couple of days at work, I have been working on writing UI tests around our implementation of the Android navigation drawer. These tests open and close the navigation drawer a couple dozen times during the tests in order to test different functionalities navigating through the app.

The native navigation drawer implementation through the Android SDK takes 600 milliseconds to open and another 600 milliseconds to close. Take this 1.2 seconds, multiply by a couple dozen to complete one full test class and you end up taking up a bit of time running this one single test by simply opening and closing the drawer. This is not including the time it takes to complete the other test files in the suite queued up for testing along with the queue of tests the rest of our team wants to run on the devices so simply put, we needed to find a way to make the native navigation drawer open and close instantly without animation for testing purposes. Luckily, Gradle makes it easy to use flavors for production/testing so the hacky mess stays in testing only while the production code remains untouched.

First attempt:

=> DrawerLayout.java provided interface

The Android DrawerLayout class is the interface used to open and close the navigation drawer programmatically through your app. In many other Android SDK classes, you are able to set an animation duration before calling the appropriate action function that will then perform the animation so naturally, the first place to try and set a custom animation duration for the navigation drawer would be the DrawerLayout javadoc.

Unfortunetly, looking through the doc, there does not exist a way to set an animation speed. No accessible function exists that we are able to call through an instance of DrawerLayout or even an extended instance. So, I had to find another way.

Second attempt:

=> Look at Android source code to find where animation was being set in hopes of modifying it.

DrawerLayout.java is where openDrawer() and closeDrawer() exists in order to open and close the navigation drawer so I figured, if that was where the drawer is opening and closing, I wanted to figure out how the animation was being executed in those functions.

Looking at DrawerLayout.java source code, the openDrawer() and closeDrawer() begin the process of opening and closing the drawer through the mLeftDragger and mRightDragger fields depending on if you are attempting to open/close a left or right navigation drawer

public void closeDrawer(View drawerView) {
    if (!isDrawerView(drawerView)) {
        throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer");
    }
    if (mFirstLayout) {
        final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
        lp.onScreen = 0.f;
        lp.knownOpen = false;
    } else {
        if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) {
            mLeftDragger.smoothSlideViewTo(drawerView, -drawerView.getWidth(), drawerView.getTop());
        } else {
            mRightDragger.smoothSlideViewTo(drawerView, getWidth(), drawerView.getTop());
        }
    }
    invalidate();
}

Checking out the source, it says: mLeftDragger.smoothSlideViewTo()...having the function be named smoothSlideTo sounds like we are getting a little closer...

mLeftDragger is an instance of ViewDragHelper.java and is what's responsible for performing the navigation drawer animation. Looking at ViewDragHelper.java source code, this is where the start of the animation takes place.

public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
    mCapturedView = child;
    mActivePointerId = INVALID_POINTER;
    
    return forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
}

This does not tell us too much yet. It just tells us to go to forceSettleCapturedViewAt() to see what it does.

"..But wait. What about overriding this function?" Sure, if I was able to create my own class that extends ViewDragHelper I could override this function that calls my own function that behaves the same as what ViewDragHelper is doing, but ViewDragHelper does not provide a public constructor so it does not allow the use of extending it. This was another reason this whole process became such a hack in the first place. I tried where I could to extend and override functions/fields where I could but in the end, something was always private that stopped me in my tracks.

Moving on, here is forceSettingCapturedViewAt() from ViewDragHelper:

private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
    final int startLeft = mCapturedView.getLeft();
    final int startTop = mCapturedView.getTop();
    final int dx = finalLeft - startLeft;
    final int dy = finalTop - startTop;
    if (dx == 0 && dy == 0) {
        // Nothing to do. Send callbacks, be done.
        mScroller.abortAnimation();
        setDragState(STATE_IDLE);
        return false;
    }
    final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
    mScroller.startScroll(startLeft, startTop, dx, dy, duration);
    setDragState(STATE_SETTLING);
    return true;
}

Ohhhhh man we're getting closer... final int duration = computeSettingDuration(mCapturedView, dx, dy, xvel, yvel); is what needs to be changed. I MUST make that duration variable = 0 somehow.

Ok, so here is is computeSettingDuration() and where the duration for the animation is actually calculated. This should find us our answer.

private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) {
    xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);
    yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);
    final int absDx = Math.abs(dx);
    final int absDy = Math.abs(dy);
    final int absXVel = Math.abs(xvel);
    final int absYVel = Math.abs(yvel);
    final int addedVel = absXVel + absYVel;
    final int addedDistance = absDx + absDy;
    final float xweight = xvel != 0 ? (float) absXVel / addedVel :
                (float) absDx / addedDistance;
    final float yweight = yvel != 0 ? (float) absYVel / addedVel :
                (float) absDy / addedDistance;
    int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child));
    int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child));
    return (int) (xduration * xweight + yduration * yweight);
}

If I could just make xduration or xweight, and yduration or yweight equal to 0, then we would have our 0 length duration.

Well, xweight and yweight had lots of values that need to be changed in order for those values to become 0. But as for xduration and yduration, those values are computed in the computerAxisDuration() function so lets check that one out:

private int computeAxisDuration(int delta, int velocity, int motionRange) {
    if (delta == 0) {
        return 0;
    }
    final int width = mParentView.getWidth();
    final int halfWidth = width / 2;
    final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);
    final float distance = halfWidth + halfWidth *
                distanceInfluenceForSnapDuration(distanceRatio);
    int duration;
    velocity = Math.abs(velocity);
    if (velocity > 0) {
        duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
    } else {
        final float range = (float) Math.abs(delta) / motionRange;
        duration = (int) ((range + 1) * BASE_SETTLE_DURATION);
    }
    return Math.min(duration, MAX_SETTLE_DURATION);
}

...Math.min(duration, MAX_SETTLE_DURATION)...after all of that code reading it comes to this one moment. If I could make this one single constant MAX_SETTLE_DURATION equal to 0, then I would have my 0 length animation duration (I have said that phrase many times now, right?).

Well, MAX_SETTLE_DURATION is indeed a private static final constant of ViewDragHelper so that indeed means I must enter the dark world of reflection.

Reflection FTW?

Interestingly enough, reflection on a constant does not seem to work (or at least it did not succeed for myself) on Android with the two methods below. I am not sure if it is because Android somehow blocks the use of reflection on constants or my situation is a special case or perhaps the Java Gods prevented me from using reflection, I don't know.

Note: I am not a fan of reflection like most Java programmers out there for many reasons. The use of reflection in this case was 1. because this code is used for testing purposes only so hacking away a solution to allow tests to run better is not as bad as hacking away a solution on production code users will be using everyday and 2. this was a journey of mine so please bear with me and my experimentation below.

  • Simple method of setting a new value for a constant:
Field field = ViewDrager.class.getDeclaredField("MAX_SETTLE_DURATION");
field.setAccessible(true);
field.set(null, 0); # Setting value from original 600 value to 0. 

The outcome of this was interesting. While debugging the app after running it on my device, MAX_SETTLE_DURATION was modified to 0 according to the debugger but while running the app, the drawer still had the same animation. I am still not sure why this is, but my guess at the time was because MAX_SETTLE_DURATION was final and Android somehow was using the old value of 600 instead of the updated 0 as I was not exactly modifying it.

  • So I tried a more involved approach modifying the final as well as setting it:
Field field = ViewDragHelper.class.getDeclaredField("MAX_SETTLE_DURATION");
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, 0); 

This attempt resulted in a java.lang.NoSuchFieldException: modifiers exception which was interesting indeed as I thought Android used the Java reflection classes but I suppose not completely.

Reflection seemed like it was not going to end up working.

Third attempt:

=> Importing source code into project.

I am not sure of any other way to go about solving this issue but to in fact take the Google source code for DrawerLayout and ViewDragHelper and inserting them into the project.

Because ViewDragHelper is not able to be extended, it has to be copied over in order to modify MAX_SETTLE_DURATION once and for all. DrawLayout can be extended, but I went with the approach of instead extending ViewGroup and making it my very own DrawerLayout instead of extending it.

In the end, the project had the following files added. Don't worry if you encounter issues along the way, do not understand a file, etc. I created a gist with code for an example solution to all of these files.

public class CustomDurationViewDragHelper {
    private static final int MAX_SETTLE_DURATION = 0;
    
    // ViewDragHelper Android source code inserted here
}

Notice the magnificant MAX_SETTLE_DURATION = 0 here. All this work for that one single line of code.

And as the DrawerLayout:

public class CustomDurationDrawerLayout extends ViewGroup {
    private CustomDurationViewDragHelper mLeftDragger;
    private CustomDurationViewDragHelper mRightDragger;
    
    // DrawerLayout Android source code inserted here
}

Modify the mLeftDragger field at the top of the file and then fix all the parts inside the file where mLeftDragger is associated with ViewDragHelper and switch it to our custom CustomDurationViewDragHelper.

Also, the following three files must also be added as they depend on DrawerLayout but now we am using my own CustomDurationDrawerLayout so they need to also be imported and have some fields modified to match the custom DrawerLayout:

public class CustomDurationActionBarDrawerToggle implements CustomDurationDrawerLayout.DrawerListener {
    // ActionBarDrawerToggle Android source code inserted here
}

and:

public class ActionBarDrawerToggleHoneycomb {
    // ActionBarDrawerToggleHoneycomb Android source code inserted here
}

and:

public class ActionBarDrawerToggleJellybeanMR2 {
	// ActionBarDrawerToggleJellybeanMR2 Android source code inserted here
}

Notice for the honeycomb and jellybean files I didn't even name it different. I simply imported it and did not edit a single part of the file.

Lastly, you will need to edit your xml layout files anywhere you are using the navigation drawer to use the CustomDurationDrawerLayout file:

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

	<!-- remaining layout code -->

</com.levibostian.widget.CustomDurationDrawerLayout>

After all of this work, it does indeed work! My project tests run without navigation drawer animations resulting in a fast test suite just like I was hoping for. It uses some dirty Java in it to hack the solution which I am not a fan of along with many of the readers out there, but sometimes it is all you can do.

Through it all, I myself became very curious as to why Google made it such a challenge to change the animation speed of the navigation drawer. Do they really not want someone changing it that bad? I am not sure why this cannot be changed while other views provide such an easy means. My guess is because the navigation drawer has a very distinct design to it and Google wants to enforce everyone uses it the same way for their users but that is about my only guess. Maybe they feel if they provide a number of ways to modify the navigation drawer it will turn into something that is not a navigation drawer. Have any ideas yourself? Please share!

If anyone has found an easier solution to this issue, found issues with this solution, please share!

Resources:
Example code solution gist