Spritesheet programmatically cutting: best practices - android

I have a big spritesheet (3808x1632) composed by 42 frames.
I would present an animation with these frames and I use a thread to load a bitmap array with all the frames, with a splash screen waiting for its end.
I'm not using a SurfaceView (and a draw function of a canvas), I just load frame by frame in an ImageView in my main layout.
My approach is similar to Loading a large number of images from a spritesheet
The completion actually takes almost 15 seconds, not acceptable.
I use this kind of function:
for (int i=0; i<TotalFramesTeapotBG; i++) {
xStartTeapotBG = (i % framesInRowsTeapotBG) * frameWidthTeapotBG;
yStartTeapotBG = (i / framesInRowsTeapotBG) * frameHeightTeapotBG;
mVectorTeapotBG.add(Bitmap.createBitmap(framesBitmapTeapotBG, xStartTeapotBG, yStartTeapotBG, frameWidthTeapotBG, frameHeightTeapotBG));
}
framesBitmapTeapotBG is the big spritesheet.
Looking more deeply, I've read in the logcat that the createBitmap function takes a lot of time, maybe because the spritesheet is too big.
I found somewhere that I could make a window on the big spritesheet, using the rect function and canvas, creating small bitmaps to be loaded in the array, but it was not really clear. I'm talking about that post: cut the portion of bitmap
My question is: how can I speed the spritesheet cut?
Edit:
I'm trying to use this approach but I cannot see the final animation:
for (int i=0; i<TotalFramesTeapotBG; i++) {
xStartTeapotBG = (i % framesInRowsTeapotBG) * frameWidthTeapotBG;
yStartTeapotBG = (i / framesInRowsTeapotBG) * frameHeightTeapotBG;
Bitmap bmFrame = Bitmap.createBitmap(frameWidthTeapotBG, frameHeightTeapotBG, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bmFrame);
Rect src = new Rect(xStartTeapotBG, yStartTeapotBG, frameWidthTeapotBG, frameHeightTeapotBG);
Rect dst = new Rect(0, 0, frameWidthTeapotBG, frameHeightTeapotBG);
c.drawBitmap(framesBitmapTeapotBG, src, dst, null);
mVectorTeapotBG.add(bmFrame);
}
Probably, the Bitmap bmFrame is not correctly managed.

The short answer is better memory management.
The sprite sheet you're loading is huge, and then you're making a copy of it into a bunch of little bitmaps. Supposing the sprite sheet can't be any smaller, I'd suggest taking one of two approaches:
Use individual bitmaps. This will reduce the memory copies as well as the number of times Dalvik will have to grow the heap. However, these benefits may be limited by the need to load many images off the filesystem instead of just one. This would be the case in a normal computer, but Android systems may get different results since they're run off flash memory.
Blit directly from your sprite sheet. When drawing, just draw straight from sprite sheet using something like Canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint). This will reduce your file loads to one large allocation that probably only needs to happen once in the lifetime of your activity.
I think the second option is probably the better of the two since it will be easier on the memory system and be less work for the GC.

Thanks to stevehb for the suggestion, I finally got it:
for (int i = 0; i < TotalFramesTeapotBG; i++) {
xStartTeapotBG = (i % framesInRowsTeapotBG) * frameWidthTeapotBG;
yStartTeapotBG = (i / framesInRowsTeapotBG) * frameHeightTeapotBG;
Bitmap bmFrame = Bitmap.createBitmap(frameWidthTeapotBG, frameHeightTeapotBG, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bmFrame);
Rect src = new Rect(xStartTeapotBG, yStartTeapotBG, xStartTeapotBG+frameWidthTeapotBG, yStartTeapotBG+frameHeightTeapotBG);
Rect dst = new Rect(0, 0, frameWidthTeapotBG, frameHeightTeapotBG);
c.drawBitmap(framesBitmapTeapotBG, src, dst, null);
mVectorTeapotBG.add(bmFrame);
}
The computation time falls incredibly! :)

Use a LevelListDrawable. Cut the sprites into individual frames and drop them in your drawable resource directory. Either programmatically or through an xml based level-list drawable create your drawable. Then use ImageView.setImageLevel() to pick your frame.

I use a method of slicing based on rows and columns. However your sprite sheet is rather huge. You have to think you are putting that whole sheet into memory. 3808x1632x4 is the size of the image in memory.
Anyway, what I do is I take an image (lets say a 128x128) and then tell it there are 4 columns and 2 rows in the Sprite(bitmap, 4, 2) constructor. Then you can slice and dice based on that. bitmap.getWidth() / 4 etc... pretty simple stuff. However if you want to do some real stuff use OpenGL and use textures.
Oh I also forgot to mention there are some onDraw stuff that needs to happen. Basically you keep an index counter and slice a rectangle from the bitmap and draw that from a source rectangle to a destination rectangle on the canvas.

Related

Android: Working with bitmaps without OutOfMemoryError

first the important lines of code:
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), getDrawableForLvl(drawable));
int []pixels = new int[bitmap.getWidth()*bitmap.getHeight()];
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
//..
pixels = null; // please gc remove this huge data
System.gc();
So I'm working on an Android app (game) with multiple levels. Every level is an image that I load as bitmap on level start. Then I have to analyse every pixel (line 3). For that I create an array (line 2). The images are 1280x800, so the array has a size over one million ints. No problems here and since it's a local variable it should be destroyed on method return. But it's java, so it's not -.- Depending on the device the garbage collector is running fast enough in time or not. So when a user starts and closes levels very fast it produces a java.lang.OutOfMemoryError in line 2. I guess because the old array(s) wasn't/weren't removed yet and now I have multiple ones filling the memory.
I could make the array a static member. So it's created once and is always available. Then it's not possible to have multiple instances of it. But I think that's a bad coding style, because it's also available (4 MB) when not needed.
I don't need all pixels at the same time. I'm splitting the images in more than a hundred rectangles, so I could use a way smaller array and fill it one after another with pixels. But I have problems to understand the methods parameters can someone help me here?
There is also a method to get just one pixel at position x,y, but I guess a million function calls is pretty slow.
Has someone a better idea? There is no way to force an object out of memory in java, is there?
Update1:
As vzoha suggested to get only the first quarter:
int []pixels = new int[bitmap.getWidth()/2*bitmap.getHeight()/2];
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth()/2, bitmap.getHeight()/2);
gives an ArrayIndexOutOfBound. I think the function call is just getting the pixels of the first quarter, but still expects the full size array and the other fields will be left untouched.
Update2: I guess I can do it row by row (half row by half row for the first quarter):
int []pixels = new int[bitmap.getWidth()/2*bitmap.getHeight()/2];
for(int row = 0; row < bitmap.getHeight()/2; ++row)
bitmap.getPixels(pixels, bitmap.getWidth()/2, bitmap.getWidth(), 0, row, bitmap.getWidth()/2, 1);
But when I do that for 20x10 parts it's not much better than getting each pixel by itself. Well it is much better but still the method should be capable to do that with one call, shouldn't it? I just don't get this "stride" parameter: "The number of entries in pixels[] to skip between rows (must be >= bitmap's width). Can be negative." How can it be negativ when >= width?
The size in pixels doesn't directly translate to how much memory the image will take up in memory. Bitmaps in Android (before using ART) are notoriously difficult to use heavily while avoiding OOM exceptions, enough so, that there's a page dedicated to how to use them efficiently. The problem is normally that there is actually enough memory available, but it has become fragmented and there isn't a single contiguous block the size you need available.
My first suggestion (for a quick win) would be to decode the bitmap with a specific config, you should be able to occupy only 1/4 of the amount of memory you were previously using by switching to use ALPHA_8 for your bitmap config. This will use 1 byte per pixel instead of 4 (the default is ARGB_8888)
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ALPHA_8
bitmap = BitmapFactory.decodeResource(getResources(), getDrawableForLvl(drawable), options);
My next suggestion would be to scale you bitmap to start with and place the appropriate one in your hdpi,xhdpi,xxhdpi folders.
In fact it is pretty simple to get just the pixels of a specific area. But the documentation is wrong about the stride parameter. It says:
public void getPixels (int[] pixels, int offset, int stride, int x, int y, int width, int height)
stride: The number of entries in pixels[] to skip between rows (must be >= bitmap's width). Can be negative.
But the truth is it can (and must in most cases) be less than the bitmap's width, it just has to be bigger or equal to the passed width (the second to last parameter of getPixels), meaning the width of the area from which I want the pixels.
So to get the pixels of the first quarter of a bitmap:
int []pixels = new int[bitmap.getWidth()>>1 * bitmap.getHeight()>>1]; // quarter of the bitmap size
bitmap.getPixels(pixels, 0, bitmap.getWidth()>>1, 0, 0, bitmap.getWidth()>>1, bitmap.getHeight()>>1);
Or to get a specific rect with x,y (upper left corner) and width, height:
int []pixels = new int[width*height];
bitmap.getPixels(pixels, 0, width, x, y, width, height);
So pretty simple. It was just the wrong documentation that put a twist in my brain.

How to swap regions in bitmap

ex.:
I have a bitmap size 500x500. And on this bitmap I have coordinates to 2 regions.
one region is at X=10, Y=10, size 10x10
second region is at X=400, Y=400, size 10x10
What would be the best way to swap those two regions in the bitmap.
You can do it trough Canvas.
Something like:
Bitmap swapped = Bitmap.createBitmap(origin.getWidth(), origin.getHeight(), origin.getConfig());
Canvas drawer = new Canvas(swapped);
drawer.drawBitmap(origin, new Rect(0,0,100,100), new Rect(100,100,100,100), paint);
drawer.drawBitmap(origin, new Rect(100,100,100,100), new Rect(0,0,100,100), paint);
At that point your 'swapped' Bitmap will have the origin pieces drawed in different regions.
For more see the Canvas documentation:
http://developer.android.com/reference/android/graphics/Canvas.html#drawBitmap(android.graphics.Bitmap, android.graphics.Matrix, android.graphics.Paint)
Hm, a simple "cruel" approach can do the work:
Load the bitmap in an 2-dimensional array and swap your cells.
It will take roughly around: 500x500x4 bytes which is a bit less than a 1 mega-byte of memory, which is nothing for android phones nowdays since app have at least 8/16 mgb of ram at their use.(on weaker phones)
Also the operations will be quite fast, even if you do a bunch of processing of the bit maps, such as resizing and so on.....
If you wont best performance you can use native code, there are some libraries for processing bitmaps that are quite memory and cpu efficient.
The best way would be the same as switching any type of data:
make a temporary bitmap to hold area1 data and put there the data.
put area2 data into area1.
put the temporary bitmap data into area2 , and recycle the temporary bitmap.
Here's a sample code that I've written. It's not tested, but should work:
Bitmap origin=...;
Rect r1=...,r2=... ; //assumption: both rectangles are of the same size
//copy from region1 to temp bitmap
Bitmap temp= Bitmap.createBitmap(origin,r1.left,r1.top,r1.width(),r1.height());
//copy from region2 into region1
Canvas canvas=new Canvas(origin);
canvas.drawBitmap(origin, r2, r1, new Paint());
//copy from temp bitmap to region2
canvas.drawBitmap(temp, new Rect(0,0,r2.width(),r2.height()), r2, paint);
temp.recycle();
An alternative way (which might be better in terms of speed and/or memory) would be to use int array instead of a new bitmap object, but I think this method is easy to understand.
Here's the alternative:
Bitmap origin=...;
Rect r1=...,r2=... ; //assumption: both rectangles are of the same size
//copy from region1 to temp pixels
int[] pixels=new int[r1.width()*r1.height()];
origin.getPixels ( pixels, 0, origin.getWidth(), r1.left, r1.top, r1.width(), r1.height());
//copy from region2 into region1
Canvas canvas=new Canvas(origin);
canvas.drawBitmap(origin, r2, r1, new Paint());
//copy from temp pixels to region2
origin.setPixels (pixels, 0, origin.getWidth(), r2.left, r2.top, r2.width(), r2.height());
I hope I didn't make any mistakes here, since I haven't tested it.

Background image taking too long to draw (Canvas) Jerky Sprites......?

Hey all I'm at a crossroads with my app that I've been working on.
It's a game and an 'arcade / action' one at that, but I've coded it using Surfaceview rather than Open GL (it just turned out that way as the game changed drastically from it's original design).
I find myself plagued with performance issues and not even in the game, but just in the first activity which is an animated menu (full screen background with about 8 sprites floating across the screen).
Even with this small amount of sprites, I can't get perfectly smooth movement. They move smoothly for a while and then it goes 'choppy' or 'jerky' for a split second.
I noticed that (from what I can tell) the background (a pre-scaled image) is taking about 7 to 8 ms to draw. Is this reasonable? I've experimented with different ways of drawing such as:
canvas.drawBitmap(scaledBackground, 0, 0, null);
the above code produces roughly the same results as:
canvas.drawBitmap(scaledBackground, null, screen, null);
However, if I change my holder to:
getHolder().setFormat(PixelFormat.RGBA_8888);
The the drawing of the bitmap shoots up to about 13 MS (I am assuming because it then has to convert to RGB_8888 format.
The strange thing is that the rendering and logic move at a very steady 30fps, it doesn't drop any frames and there is no Garbage Collection happening during run-time.
I've tried pretty much everything I can think of to get my sprites moving smoothly
I recently incorporated interpolation into my gameloop:
float interpolation = (float)(System.nanoTime() + skipTicks - nextGameTick)
/ (float)(skipTicks);
I then pass this into my draw() method:
onDraw(interpolate)
I have had some success with this and it has really helped smooth things out, but I'm still not happy with the results.
Can any one give me any final tips on maybe reducing the time taken to draw my bitmaps or any other tips on what may be causing this or do you think it's simply a case of Surfaceview not being up to the task and therefore, should I scrap the app as it were and start again with Open GL?
This is my main game loop:
int TICKS_PER_SECOND = 30;
int SKIP_TICKS = 1000 / TICKS_PER_SECOND;
int MAX_FRAMESKIP = 10;
long next_game_tick = GetTickCount();
int loops;
bool game_is_running = true;
while( game_is_running ) {
loops = 0;
while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP) {
update_game();
next_game_tick += SKIP_TICKS;
loops++;
}
interpolation = float( GetTickCount() + SKIP_TICKS - next_game_tick )
/ float( SKIP_TICKS );
display_game( interpolation );
}
Thanks
You shouldn't use Canvas to draw fast sprites, especially if you're drawing a fullscreen image. Takes way too long, I tell you from experience. I believe Canvas is not hardware accelerated, which is the main reason you'll never get good performance out of it. Even simple sprites start to move slow when there are ~15 on screen. Switch to OpenGL, make an orthographic projection and for every Sprite make a textured quad. Believe me, I did it, and it's worth the effort.
EDIT: Actually, instead of a SurfaceView, the OpenGL way is to use a GLSurfaceView. You create your own class, derive from it, implement surfaceCreated, surfaceDestroyed and surfaceChanged, then you derive from Renderer too and connect both. Renderer handles an onDraw() function, which is what will render, GLSurfaceView manages how you will render (bit depth, render modes, etc.)

Android: Canvas made out of multiple images won't save

Right my title isn't the best in the world. I've got a big code that's supposed to make on big bitmap out of multiple bitmaps. I've isolated the problem to this part of the code
bity = Bitmap.createBitmap(specialWidth,specialHeight,Bitmap.Config.ALPHA_8);
Canvas canvas = new Canvas(bity);
float left=0.0f;
for (int i = 0; i < imagesArrayz.length; i++){
float top=0.0f;
canvas.drawBitmap(imagesArrayz[i], left, top, null);
left+=imagesArrayz[i].getWidth();
}
To explain: "bity" is a globally defined Bitmap object and it's unassigned untill this point; imagesArrayz is an array of 5 Bitmaps that has already ben assigned and has ben assigned correctly (i tested it to see if each image is in the array)
After this i just have a function that saves the global variable bity to a file. THE PROBLEM is that instaid of saving my nicely drawn canvas it saves an empty jpg file of 0kb. Please help!
I answered my own question... Replace ALPHA_8 with ARGB_8888 and it all magically works.
Someone shoot me please...

How to copy a smaller bitmap into a larger one?

Hopefully this should be an easy question. I'm trying to copy a series of small bitmaps into a larger one, arranging them side by side without any gaps or overlap in their pixels. For example, if I have 3 square bitmaps, I'd like to copy them into one long and thin rectangle. I know how to do the opposite, namely creating a small bitmap out of a larger one, but not this way around. What's the right command?
(If anyone's curious, I want to do this to be able to reuse some code I wrote for handling animation with a single bitmap.)
Thanks!
Create a canvas for the large bitmap, then use that to draw your small bitmaps. I'm pretty new to android, but I'm guessing that it's something like this:
Bitmap makeBigBitmap(Bitmap srcBmps[]) {
Bitmap wideBmp;
Canvas wideBmpCanvas;
Rect src, dest;
// assume all of the src bitmaps are the same height & width
wideBmp = Bitmap.createBitmap(srcBmps[0].getWidth() * srcBmps.length,
srcBmps[0].getHeight(), srcBitmaps[0].getConfig());
wideBmpCanvas = new Canvas(wideBmp);
for (int i = 0; i < srcBmps.length; i++) {
src = new Rect(0, 0, srcBmps[i].getWidth(), srcBmps[i].getHeight());
dest = new Rect(src);
dest.offset(i * srcBmps[i].getWidth(), 0);
wideBmpCanvas.drawBitmap(srcBmps[i], src, dest, null);
}
return wideBmp;
}

Categories

Resources