Tags

, , , , , , , , , , , , , ,

One of the greatest ideas in CSS is using sprites — tightly packing all images into a single image and using parts of the image in various places. This reduces the size of images, the network requests, and improves performance — with some cryptic coding. How cool it would be if we can do such a thing in Android? Wait, is it even possible? A good candidate to investigate this would be Emoji. An emoji keyboard has a lot of small icons — over 200 of them — shown in a grid.

Texture Atlas

Since, it would be unnecessary to ship 200 * 4 images in an APK, it is always better to download these images later, based on the device resolution. So, let us assume that the images are stored somewhere on the disk — and not a part of res/drawable folder. A typical adapter in that case would be like:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ImageView view;
        if (convertView == null) {
            convertView = (ImageView) mInflater.inflate(R.layout.icon, parent, false);
        }
        view = (ImageView) convertView;
        
        // Note: This runs on UI thread. It's better to do this in background thread.
        // This is shown only for code completeness.
        Bitmap bitmap = BitmapFactory.decodeStream(input_stream); 
        view.setImageBitmap(bitmap);
        return convertView;
    }

As mentioned, since the decoding runs on UI thread, it’s better to run it in a background thread. By moving the decoding prior to setting the adapter, and using an LruCache, one can make the layout feel faster. This is more or less the usual way of handling bitmaps on Android applications. Let’s make this better!

Android’s Canvas has a nice method that allows drawing just a portion of larger bitmap on the screen. Given a bitmap, and a source rectangle, it takes the portion in the source rectangle, scales/translates it to the destination rectangle and draws it on the Canvas. So, if we pack all the icons in one single image, we can tell the adapter to choose a square based on the item shown. The adapter would change to:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        SpriteImageView view;
        if (convertView == null) {
            convertView = (SpriteImageView) mInflater.inflate(R.layout.icon, parent, false);
        }
        view = (SpriteImageView) convertView;

        // Consider the icons to be arranged in a 20x10 grid.
        // This would map the starting position of a particular item.
        final int left = (position % 10);
        final int top = (position / 10);

        // Let's say the icons are 64x64 on MDPI.
        // This finds the square based on the position.
        final int size = deviceDensity * 64;
        view.setCoordinates(left * size, top * size, size, size);
        return convertView;
    }

The Adapter knows how to map an item with the data — the Bitmap. This adapter uses a SpriteImageView, which looks more like:

public class SpriteImageView extends ImageView {
    // ... all other stuff;
    
    @Override
    public void onDraw(Canvas canvas) {
        // Get the destination rectangle.
        // Make any changes if needed -- like padding / scaling.
        getDrawingRect(mDestinationRect);

        // Draw the bitmap.
        canvas.drawBitmap(sBitmap, mSourceRect, mDestinationRect, null);
    }

    public void setCoordinates(int left, int top, int width, int height) {
        // It's not good to create objects. Better update.
        // I'm just being lazy 😉
        mSourceRect = new Rect(left, top, left + width, top + height);
        invalidate();
    }
}

On a performance note, it’s better to load the Bitmap once and share among all the SpriteImageView. Hence the sBitmap is a static variable. Also, it’s better to not to scale. As I mentioned earlier, if the images are downloaded, and not a part of APK, its better to use an image that corresponds to the device’s density. So, if the icons are of size 64x64dp, then for a XHDPI device, one large bitmap with 128x128px icons will be sent.

Finally we have a solution to use Sprites on Android. But does it help the app? As a matter of fact, starting with KitKat, Android generates an Atlas of images that will ever be used by a particular phone/tablet. These images are shared among apps, and hence help in redrawing Android resources in a much faster way. Romain Guy talks about in under “Shared assets texture“. However, such an Atlas is not generated for installed apps. So, an app’s resources cannot have this performance win. There is some proof that this is better. But really is it?

I ran a version with LruCache for Bitmaps and another with a 20×10 grid of images in a single Bitmap on HTC One and Nexus S. On HTC One, I could see a lot of time saved (oh wait! it’s a quadcore phone!). On Nexus S, individual images took a total of 1.3s to load into Cache. While a single large bitmap took 180ms. That’s 10x win just for loading images! Without any pngcrush-ing, the single large bitmap was just 300kb. While the 200 images occupied 1.3MB. Now even after zipping, that’s more than 3x win to download images from the server. LruCache evicts resources not being used. So, if we are currently showing rows 20-30, it’s highly likely that rows 1-10 have already been evicted out of memory. And if the user goes back to rows 1-10, they have to be re-loaded from the disk. That’s a performance problem — as we are unnecessarily decoding images again and again. The other way out is to have a large enough LruCache that will avoid evicting. In this case I had to have a 14MB LruCache to retain all images in memory. On the contrary, the large bitmap took 3MB in memory. Did we achieve another 4x win?

There’s one more thing! At the base, the drawing commands are mapped to OpenGL. OpenGL uses textures for bitmaps. Each bitmap is converted to a texture, and is drawn on the screen. So, every time a view requests drawing a bitmap, OpenGL will have to load the texture, map the coordinates to the drawing surface, and copy the pixels. But in this case, there is only one texture — the texture atlas — for all bitmaps that needs to be drawn! So, there is no cost for loading and binding a texture in OpenGL. That’s a huge benefit!

Alright! This is good for emoji. And definitely we cannot generate an Atlas for the device and use it, as we won’t be able to use the R.drawable functionality. So, where is the benefit for all other apps? Definitely all apps has its own set of icons to be drawn. Instead of using 10-20 Drawable resources, we can pack them in one single file. Does your app use a lot of profile images, list of pre-defined options with icons, or a navigation menu with icons? There you go! And if you want SpriteImageView to be useable from XML, a small change can do it.

    @Override
    public void setImageDrawable(Drawable drawable) {
        mBitmap = new WeakReference<Bitmap>(((BitmapDrawable) drawable).getBitmap());
    }

The mBitmap stores a WeakReference to the loaded bitmap. The setCoordinates can be called from the constructor, with values from custom XML attributes. And everything works like a charm!

    <com.appname.SpriteImageView custom:left="100dp"
                                 custom:top="100dp"
                                 custom:width="100dp"
                                 custom:width="100dp"
                                 android:drawable="@drawable/some_laaaaarge_bitmap"/>

That would draw an icon at (2,2), if each icon was of size 100dp x 100dp. OpenGL is a fan of “power-of-two” textures. May be that’s a good thing to consider too! Happy spriting!

P.S.: Thanks Smashing magazine and Freepik for the icons.

Advertisement