The story of creating a new library
At Supercharge we build highly polished digital products, mainly for mobile screens. Recently we’ve created a new library called ShimmerLayout, which allows you to create shimmer effects on your layout in a memory efficient way-even when working with big layouts.
Shimmer effect was created by Facebook to indicate a loading status. They also open-sourced a library called Shimmer both for Android and iOS so that every developer could use it for free. As I see it, this kind of animation is now being widely used-not only by Facebook, but by LinkedIn and Udemy as well.
We’ve been working on a project where this kind of animation was needed to handle big layouts. Not wanting to reinvent the wheel, we started to use the Shimmer library. It was easy to use and gave us a great number of custom attributes, so we could use it as we wanted.
However, it turned out that it was eating up the memory. Not only did it make the application slow for the loading period, but it was also the second-highest reason for crashes since it caused OutOfMemoryError. After a little search, we discovered that the library had been neglected and there were many reports of issues similar to ours.
Studying the library revealed it could not be used for big layouts, as it required a great amount of memory during the animation. The reason for this is just simple math. The layout which I will be discussing throughout the article can be found at the sample application part of the repository. This layout has a width of 1080 pixels and a height of 777 pixels on a HTC M8 device . To achieve the desired effect the Shimmer library used 3 bitmaps with Bitmap.Config.ARGB_8888 configuration.
This means that when you create a bitmap each pixel will store 4 bytes of data about its color. Each channel (RGB and the alpha) uses 1 byte. So the formula is 1080 x 777 x 4 x 3 which is around 9.6 megabytes. On the dashboard we used 6 instances of this animation, which means a great number of additional megabytes of RAM consumption.
I conducted a measurement in the Android Studio with the memory monitor comparing the memory used in the example before the start of the animation and during the animation. You can see the significant difference.
Then create a new one…
The only solution was to create a new class to achieve the same effect. I was quite familiar with animating using ObjectAnimator and ValueAnimator, so I started to experiment with them.
My approach was that on each View I would animate an ImageView from the beginning of the View to the end. To create the shimmer line which changes its colour from transient to light grey and then back to transient, I made a shape resource with gradient in it
When the first prototype was ready, I quickly realised that it was a dead end for a number of reasons:
- The number of Views I had to work with was doubled
- It was working fine on one View, but if I have a complex layout then I had to make them work together
- Besides rectangular Views, I had Views with an oval shape.
- There was not a general solution, so for every distinct layout I had to create the logic.
The Solution — PorterDuff compositing
Tinting is one of the great features that was brought to us by Lollipop. However, this tinting effect was previously available under the ColorFilter name. To apply colours on top of drawables, ColorFilter uses PorterDuff compositing. PorterDuff compositing describes on a pixel basis how the result image will look if we want to combine 2 images. To give you a deeper understanding of this method I will use the tinting example for the explanation. To help you imagine this, I’ve created the image below:
Let’s call the first image (the rectangle with a red circle inside it) image A. The second image-the blue rectangle is Image B, and the third one is the result image. In the context of PorterDuff compositing, Image A is a destination image onto which we want to apply a different colour, called source (Image B).
To get the result image, PorterDuff first calculates the alpha of each pixel, which determines the visibility. 1 alpha means totally visible, 0 means not visible at all and any value in between means partially visible. When we know the visible parts, then we can calculate the colour. This calculation determines the different PorterDuff.Modes.
For the tinting example we will use the PorterDuff.Mode.SRC_IN. So let’s do some math. The alpha of the pixels is calculated by multiplying the source alpha and the destination alpha. The source image has pixels for which the alpha value is 1. However, the destination has pixels for which the alpha value is 0 (outside of the circle). And 0 x 1 equals zero. That is why the area outside of the circle is empty-it has an alpha value of 0. As for the color, we have to multiply the color of the source by the alpha of the destination. In the circle area it means the color of the source image will override the color of the destination image. As for the pixels outside, since their alpha value is zero, so we can multiply them by any kind of color value, and the end result will always be 0.
If you think a little bit, you quickly realise that this is exactly what we need for the Shimmer animation. The layout would be the destination, and the darker line the source.
With this knowledge we just picked up we can start the implementation. To reduce memory consumption, instead of creating 3 bitmaps with the size of the example layout, I only created 2 of them, and the width of the second one is only half of that of the original. The reason for this half-sized bitmap is that I discovered that in most of the cases the shimmer line occupies less then half of the screen. So why should we use a full-width bitmap if we can use just the half of it?
The animation itself is quite simple. With the help of ValueAnimator I am increasing the starting position offset of the shimmer line on the X axis. The magic happens at the
drawMask(Canvas renderMask) method.
maskOffsetX + localMaskBitmap.getWidth(),
super.dispatchDraw(renderCanvas); renderCanvas.drawBitmap(localMaskBitmap, maskOffsetX, 0, maskPaint);
The renderCanvas variable here represents the destination bitmap and the localMaskBitmap is the source bitmap. By calling the dispatchDraw(…), we instruct the system to draw the layout of the canvas. However, the clipRect(…) method mentioned above limits the drawing to only the specified rectangular area. The pixels contained in this are the only ones we want to use for PorterDuff compositing. So it basically saves drawing resources and time. The actual compositing happens at the
drawBitmap(...) method where we provide the source bitmap and the maskPaint (instance of the Paint class) object containing the PorterDuff.Mode we use. So, when this method is called, the system will run the calculations described earlier, but now with different images, and the result is the original layout with the shimmer line. Of course, the position of the line depends on the animation.
Since we’ve been using this library on the project mentioned above, I can say that we no longer have issues regarding the shimmer effect or the OutOfMemoryError.
If you compare this benchmark with the one at the beginning of the article, you can see that it consumes far less memory.