Android Recipe #3, sliding menu, layers and filters

This third recipe is a new take on the popular sliding menu. You can find sliding menus in many applications including Path, Evernote, Falcon Pro, Google+, etc. Very often the sliding menu is just that, a simple sliding menu. Some applications go a little farther and add a dimming effect: when you open the menu, the menu is slowly revealed by fading out a black layer. All the implementations I’ve seen, the SlidingMenu library for instance, simply draw a translucent rectangle on top of the menu. This is not only expensive because it causes overdraw but it’s also fairly limited.

Today’s demo uses a different effect. Instead of simply dimming the layer, I’m turning it from black and white to full colors. The demo’s menu is a simple image but the effect with arbitrarily complex view hierarchies. To follow along you can download the source code and a binary to run on your device. Note that I have only tested this demo on Nexus 7. You will need the SlidingMenu library to build the demo yourself.

Sliding menu

If you cannot (or don’t want to) run the demo application you can see what the effect looks like in motion in this video. I captured this video in the Android emulator and the frame rate of my capture program was capped at 30fps so it doesn’t look as nice as it does on a device:

To implement this effect you must first enable hardware layers on your views. There are several reasons to use hardware layers and one of them is performance. I won’t go into much details here so I recommend you watch the Android Accelerated Rendering presentation Chet Haase and I gave a Google I/O in 2011.

When a hardware layer is set on a View, that View is rendered into an OpenGL texture every time it updates. Once the View is captured as a texture animations such as moving, rotating or fading the View become extremely cheap. The SlidingMenu library does not use hardware layers unfortunately. Our first step is to fix this. To do so we will use a listener called CanvasTransformer invoked throughout the sliding animations. The intent is to let you customize the animation by changing the canvas’ transform but we’re going to do something a little different.

menu.setBehindCanvasTransformer(new CanvasTransformer() {
    @Override
    public void transformCanvas(Canvas canvas, float percentOpen) {
        boolean layer = percentOpen > 0.0f && percentOpen < 1.0f;
        int layerType = layer ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE;

        if (layerType != menu.getContent().getLayerType()) {
            menu.getContent().setLayerType(layerType, null);
            menu.getMenu().setLayerType(layerType, null);
        }
    }
});

This piece of code simply enables hardware layers when percentOpen is in the ]0..1[ range. When the menu is fully open or fully closed, the layer is discarded by setting LAYER_TYPE_NONE. Notice the second parameter of setLayerType(), set to null here. This parameter is of type Paint and is the key to today's demo. This paint, when set, is used to change the way the layer is drawn on screen. There are several ways the paint can influence the result and you can change:

  • The blending mode
  • The opacity
  • The color filter

A ColorFilter can be used to modify the color values of the source pixels. For instance you can use a LightingColorFilter to darken a layer. The color filter I used is called ColorMatrixColorFilter. You use it by supplying a 4x5 matrix that is in turn used to multiply each pixel (a pixel can be represented as a vector of 4 values.) Color matrices can be used to achieve many different results and the one we are interested in, that is turning our image into black and white, is built-in:

private ColorMatrix matrix = new ColorMatrix();
// ...
matrix.setSaturation(percentOpen);
ColorMatrixColorFilter filter = new ColorMatrixColorFilter(matrix);
paint.setColorFilter(filter);

This code resets the color matrix with a new saturation level and builds a new filter from that matrix. The result is applied to a paint. The last thing to do is to apply the paint itself to the layer:

menu.getMenu().setLayerPaint(paint)

This API is unfortunately only available as of API level 17 (Android 4.2.) To achieve this effect on previous versions of the platform you must use one of two techniques. On API levels 14 and 15, you must pass the paint to setLayerType() and invalidate the layer’s parent:

View backView = menu.getMenu();
backView.setLayerType(layerType, paint : null);
((View) backView.getParent()).postInvalidate(backView.getLeft(), backView.getTop(),
    backView.getRight(), backView.getBottom());

On API level 16 only you must use a rather disgusting reflection hack. The reason is that we changed how layers are handled internally by the framework to optimize animations. This gave us a great boost but it unfortunately made it almost impossible to update a layer’s paint without redrawing the content of the layer itself. Redrawing the layer increases overdraw and can impact performance. I won’t explain the details of the hack here; look at the source code instead.

The default dimming effect used by the SlidingMenu library can be implemented using color filters. The benefit of doing so is that the entire sliding menu, including the dim effect, requires only 2 draw calls: one for the bottom layer and one for the top layer. If you are using your own sliding menu, consider using layers!

29 Responses to “Android Recipe #3, sliding menu, layers and filters”

  1. Ivan says:

    It’s beautiful!

  2. Stephen says:

    Romain,

    Thanks for the great example, I really enjoy the effect. But, for those of us that need to maintain compatibility back to GB, we don’t have access to these API’s in our projects. Do you have any recommendations for performance in compatibility?

    Thank you!

  3. Joe says:

    Surely there’s not much use for this currently if it only works back to API 14?

  4. Romain Guy says:

    Joe: The share of ICS+ is large enough that you can add this feature in your application. Simply degrade gracefully on older versions and use a simpler effect or no effect at all.

    Stephen: You can use the drawing cache instead with View.setDrawingCacheEnabled.

  5. Ondrej says:

    Hi, thanks for those recipes, I like that you show not just how to do things, but also how to do them effectively. Just a quick notice – the video in the preview of this article on the main page of your blog points to the previous one.

  6. xBroak says:

    Thanks for replying Romain, I always feel a little wary of using new API features simply because of the huge fragmentation issue, i mean, 40%! of my users are on 2.3.3 to 2.3.7 that’s silly numbers, but as you’ve said i’d like to start using them and ‘degrading gracefully’ to older platforms, my newest app release i’ve tried to incorporate this more and embrace the newer OS to a greater deal than before.

    https://play.google.com/store/apps/details?id=com.broakenmedia.wordguess

  7. Stephen says:

    A follow up question,

    In your example, you set a hardware layer on the content view. In the case of my app, that content view is a ListView which, in your I/O talk, you specifically said to not put into a hardware layer when its scrolling. It’s very possible for my users to start scrolling the list and then swipe open the menu. In this case, would you recommend only setting the hardware layer on the menu view?

    Thanks again!

  8. Romain Guy says:

    Stephen, no you should still user layers in this case. What you are describing is an edge case. The list will keep scrolling but there might be a drop in framerate if the menu is opened at the same time. I wouldn’t worry too much about it though.

  9. Sam says:

    Sorry to saw I just implemented this line-for-line, and while the effect is cool, the performance is not. This introduced a HUGE stutter right when I open my sliding menu for the first time vs. the default fading of the slidingmenu lib. Subsequent opens are better. If I have time…I’ll try to figure out what’s going on.

  10. Sam says:

    Sorry to say I just implemented this line-for-line, and while the effect is cool, the performance is not. This introduced a HUGE stutter right when I open my sliding menu for the first time vs. the default fading of the slidingmenu lib. Subsequent opens are better. If I have time…I’ll try to figure out what’s going on.

  11. Tamar says:

    The link to downloading the source code is pointing to the Spotlight source code :(

  12. Tamar says:

    Thank you!

  13. Daniel Alvarado says:

    Hi Romain, would you mind peeking into this StackOverflow question regarding practical usage of the SlidingLibrary whenever you have some time?

  14. Josh says:

    Romain,
    Why would I want to disable the hardware layer when the menu is closed? Shouldn’t I just set android:hardwareAccelerated=”true” on the entire app and not worry about it?

  15. Romain Guy says:

    Josh, hardware layers are not used to *enable* hardware acceleration. They are used to capture a snapshot of a View inside an OpenGL texture. This texture can then be moved around much more efficiently than if the system had to execute every single individual drawing command. You must disable hardware layers when you don’t need them to save on memory and processing costs (updating a hardware layer is more expensive than drawing directly on screen.)

  16. Josh says:

    Thanks for the response Romain. I’m starting to get it. I’ve listened to your Google I/O in 2011 session 3 times now, but I’m still a little iffy on a couple of things:
    1) does android:hardwareAccelerated=”true” enable the **option** to use setLayerType(View.LAYER_TYPE_HARDWARE)
    2) if I have to manually set the layerType what does android:hardwareAccelerated=”true”?
    3) are there any general rules/resources for using setLayerType(View.LAYER_TYPE_HARDWARE) when animating and creating custom views?

    Happy New Year!

  17. Romain Guy says:

    Josh, layers only work when hardware acceleration is turned on (note that it’s on by default as of API level 14.) Like I said earlier, layers are only used to capture a snapshot of a View inside a texture (or bitmap.) This is all explained in the Google I/O 2011 session with an example of how it affects display lists :)

  18. Pipeline says:

    Great tutorial Romain.
    I have a question about the screencast, how have you recorded your mobile screen to have so much fluided?

    Thanks!

  19. Romain Guy says:

    It’s not a device, it’s the emulator running on my 5 years old MacPro.

  20. kc says:

    Like the architecture of android. Dislike Java. Nominal class based OO seems a bad fit for fully utilising graphics and multi-core processors. Need value types, properties, first class functions etc.

    Google has too many languages:
    – Java/Dalvik on Android
    – JS/V8 and Dart
    – Go on the server.

    Give developers a new language and runtime which runs in the browser/mobile/server.

  21. Hélio Padrão" says:

    Hi guys, cool, it worked like charm =)

    Is there a way to set the minimum percent of menu visible, when it’s closed?

    Thanks a lot,

    Hélio

  22. Rene says:

    (Android 4.03 on 7″ and 4.12 on SGS3)

    Hi Romain,

    Just downloaded your source and binary. Binary works fine on both my devices, but SlidingMenu has been updated with your HW layer tip. Unfortunately when I run your source version, the B/W to Color feature doesn’t work anymore. I am a Java/Android novice, could you please elaborate on that?

    Thanks!

  23. Rene says:

    Hi Romain,

    When I debug (Eclipse 3.7) SlidingMenu.java (latest version, breakpoint in “manageLayers” method) from the Library running your source, the output screen (7″ device, API 15) does show B/W saturation change on breakpoint halt. Things like this drive me up the wall! Actually both your manageLayers and the library’s get executed. I simply can’t find the mixup. What are the proper steps to follow??

  24. Michal says:

    Thank you for an excellent article! Just wanted to tell you (didn’t notice anyone writing it in previous posts), that Jeremy Feinstein introduced this solution in the last version of his library.

  25. Giancarlo Leonio says:

    Hi Romain Great tutorial! Thank you. I compiled a list of some top resources I found on creating sliding menus for android. I included your post. Check it out/ feel free to share. http://www.verious.com/board/Giancarlo-Leonio/creating-a-sliding-menu-for-android/ Hope other developers find this useful too. :)

  26. Andy says:

    Hey Romain, thanks for doing this series, it’s pretty awesome! Question:

    As someone else mentioned, there’s a pretty major stutter as you transition the views to/from hardware layer. I’ve noticed this previously on some of my own animations. Do you have any recommendations for dealing with this?

  27. Pierre LaFayette says:

    Now how do you deal with the clipping issues when you have a WebView in the content view and the SlidingMenu uses a hardware layer (occurs when drag closing the opened sliding menu)? My solution is to switch the WebView to a software layer. I imagine this would be resolved with the ChromeWebView eventually… I thought Nicolas Roard had fixed all the WebView clipping issues previously but I guess not.

  28. Jose L says:

    Bad news when switching back from layerType:hardware to layerType:none

    05-24 14:52:40.019: W/dalvikvm(21061): threadid=1: thread exiting with uncaught exception (group=0x40d25930)
    05-24 14:52:40.074: E/AndroidRuntime(21061): FATAL EXCEPTION: main
    05-24 14:52:40.074: E/AndroidRuntime(21061): java.lang.NullPointerException
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.GLES20RenderLayer.redrawLater(GLES20RenderLayer.java:114)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.View.getHardwareLayer(View.java:12406)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.View.getDisplayList(View.java:12619)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.View.getDisplayList(View.java:12694)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.View.draw(View.java:13428)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.ViewGroup.drawChild(ViewGroup.java:2928)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.ViewGroup.dispatchDraw(ViewGroup.java:2797)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.View.getDisplayList(View.java:12648)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.View.getDisplayList(View.java:12694)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:2910)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.View.getDisplayList(View.java:12588)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.View.getDisplayList(View.java:12694)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:2910)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.View.getDisplayList(View.java:12588)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.View.getDisplayList(View.java:12694)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:2910)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.View.getDisplayList(View.java:12588)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.View.getDisplayList(View.java:12694)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.HardwareRenderer$GlRenderer.draw(HardwareRenderer.java:1198)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.ViewRootImpl.draw(ViewRootImpl.java:2173)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:2045)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1854)
    05-24 14:52:40.074: E/AndroidRuntime(21061): at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:989)

  29. pavan says:

    Very nice tutorial you on option menu can also check this one at
    http://www.pavanh.com/2012/10/android-menu.html