Search code examples
androiddpisprite-sheet

Android Sprite Sheet Scaling


I've been doing a lot of reading on supporting multiple Android devices. I am under the impression that if I do not create a drawable for a specific dpi folder, then Android automatically takes the closest one and scales it. I have a sprite sheet I am using for XXHDPI. It works great on my Galaxy S4, but when I run the app on an emulator using the Nexus 7, which has a HDPI screen, it seems the sprite sheet isn't scaled perfectly. The width is a little bit off so instead of seeing the entire frames of the sprite animation, they are getting cut off in the wrong spots, so I see the left half of the current frame and the right half of the previous frame. The sprite sheet I made in Flash, so it was easy to go back and scale the vectors to produce a second sheet for the HDPI folder. Doing it manually like this fixed the problem. But I would rather not have 5 copies of the sheet as that would increase the file size. So what exactly is the source of this problem? Is there a better way to scale the image at runtime, is the blitting off because the emulator isn't pixel perfect, or is the Android scaling method just not that precise?

src = new Rect(frame * width, 0, (frame + 1) * width, height);
dst = new Rect(x, y, x + width, y + height);
canvas.drawBitmap(bmp, src, dst, null);

In the constructor of the object I have this:

width = bmp.getWidth() / 5;
height = bmp.getHeight();

I set the width of each frame to (1/5) the width of the bitmap because there are 5 frames.

EDIT

I have determined the problem is that when Android scales the sprite sheet, the size, of course, is cast to an integer. For example, the width of my sheet for XXHDPI is 575. It is divisible by 5 so it works well in XXHDPI, but when scaled to HDPI (which is half the size of XXHDPI) it is saved as 287, which is NOT divisible by 5. So the first frame is fine, the next is off by a pixel, the next by 2, and so on. This propagates and makes the later frames look really bad.


Solution

  • To avoid android scaling your source image, put it in the assets folder. This will mean you can't load the resource by it's ID though, so you'll need to do that manually with a method something like:

    public static Bitmap getBitmapFromAssets(Context context, String filePath) {
        AssetManager assetManager = context.getAssets();
    
        InputStream inStream;
        Bitmap bitmap = null;
        try {
            inStream = assetManager.open(filePath);
            bitmap = BitmapFactory.decodeStream(inStream);
        } catch (IOException e) {
            // handle exception
        }
    
        return bitmap;
    }
    

    Note: Depending on your IDE, you may or may not have an assets folder already created for you.

    You can then do your own scaling, AFTER obtaining a bitmap of the required sprite sheet region if you want. You can do your own screen size detection with:

    int screenSize = context.getResources().getConfiguration().screenLayout;
        screenSize &= Configuration.SCREENLAYOUT_SIZE_MASK;
    
        switch (screenSize) {
        case Configuration.SCREENLAYOUT_SIZE_SMALL:
            // scale for small
            break;
        case Configuration.SCREENLAYOUT_SIZE_NORMAL:
            // scale for normal
            break;
        case Configuration.SCREENLAYOUT_SIZE_LARGE:
            // scale for large
            break;
        case 4: Configuration.SCREENLAYOUT_SIZE_XLARGE // If your min sdk is >= 9
            // scale for xlarge
            break;