I wrote an example code to generically show my issue.
Basically, inside the onDraw of a View, I want to create a scaled up bitmap, draw it on canvas, recyle that bitmap and free its memory, and do it again and again, in a loop.
The problem is that memory grows and grows at each step of the loop, and remains full even after that onDraw has finished, even after waiting some time.
As far as my logic goes, canvas already contains a bitmap sized like the screen, so it should not grow in memory size whatever I draw into it, and I suppose each of the scaled up bitmaps remains in memory even though i called recycle on them and even though their reference is destroyed.
Where am I wrong?
Thanks!
#Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
int i=0;
while(i<10) {
matrix = new Matrix();
bitmapToDraw = Bitmap.createScaledBitmap(bitmap, canvas.getWidth(), canvas.getHeight(), false);
canvas.drawBitmap(bitmapToDraw, matrix, imagePaintF);
bitmapToDraw.recycle();
bitmapToDraw = null;
i++;
}
super.onDraw(canvas);
}
Just by repeating the loop 10 times, memory grows to -and always remains at- almost 200MB.
If I do it more and more, it goes outOfMemory. While by recycling bitmaps, I should be able to do it without a limit.
Related
I am writing an app to draw some data (map of subway data), and the map can be moved/scaled when user touch the screen.
Now I am using View to draw the map, the map is being drawed in Canvas when onDraw(Canvas canvas) called.
I caculate the translate x/y and scale when onTouchEvent called and then call invalidate() to refresh the map.
The onDraw method looks like this:
#Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.translate(nowPosX, nowPosY);
canvas.scale(mScaleFactor, mScaleFactor);
canvas.drawColor(Color.WHITE);
for (Subways.Line line : subwayData.getLineList()) {
for (Subways.Line.Stations:line.getStationList) {
canvas.drawLine();
canvas.drawText();
canvas.drawCircle();
}
}
canvas.restore();
super.onDraw(canvas);
}
But I have a problem now. Every time I move/scale the map, I need to redraw everything and the data is so many and spend lot of time. It will slow when I move/scale the map.
I tried to draw in Bitmap first and move/scale the bitmap everytime onDraw called. But if bitmap is small, it will looks fuzzy when I scale. If bitmap is big, it may cause OutOfMemoryError.
Is there any way to solve my problem?
On some older Android versions you can improve performance by drawing all the bitmaps at once and all the text at once. Other possibility is to create a Path for each text you have, cache them and use the cache for drawing the Paths everytime a redraw is needed.
You can prepare the map with lines, circles and text drawn on it into many Bitmaps before showing the map to the screen.
Then modify the onDraw() method to make it only draw certain breakdown bitmaps in correct scale.
I am developing app which can give user ability to open picture in ImageView and then draw lines on it and also erase them. But i have one big problem: when i am loading a big image the drawing process becomes very slow. But when the image is small then drawing process is normal))).
This is an algorithm what i do to draw lines:
1. Open image from SD card (decode image size and rescale it if necessary).
2. Put Bitmap into ImageView by setImageBitmap(bitmap); method
3. Create transparent bitmap with width and height as the original bitmap has (to draw and erase lines on it).
4. Draw or erase lines on transparent bitmap (using Path class) in ImageView onDraw() method
This is my onDraw() method:
#Override
protected void onDraw(Canvas canvas) {
super.setImageMatrix(mMatrix);
super.onDraw(canvas);
if (!mFragmentImage.inDrawMode()) return;
canvas.save();
if (mFragmentImage.mShape!=null){
if (mFragmentImage.mShape.getType() == CustomShape.SHAPE_LINE) {
mOverlayCanvas.drawPath(mFragmentImage.mShape.getPath(), mFragmentImage.mShape.getPaint());
}
}
if (mOverlay!=null) canvas.drawBitmap(mOverlay, mMatrix, null);
canvas.restore();
}
Can anyone give some advices how to make drawing process faster.
I have created a custom ImageView and in its onDraw method I need to draw some bitmaps based on user interaction like touch. Everything is working fine however slowly as I start adding more and more bitmap the application really slows down.
This is what I do in my onDraw of the Custom ImageView
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.e(TAG, "onDraw called");
for (int i=0; i < bitmapList.size(); i++){
drawBitmap(canvas, bitmapList.get(i));
}
}
As you can see, I am redrawing all the bitmaps in the List everytime onDraw is called naturally when the number of bitmap exceeds say 4-5 the operation becomes very expensive and slows the application down.
Any solution to this proble as to how can this be optimized?
Can calling drawBitmap in a different thread make the operation less expensive?
Is there a way to keep a copy of the previous canvas and then simply restore it in onDraw rather than drawing all the bitmaps again?
The question essentially is refreshing the View with lots of dynamic images on it and its optimization.
You should create a Bitmap which size must be equal to the ImageView's image size and draw all the bitmaps from the bitmapList on this bitmap only once. On every onDraw() call you should draw only this bitmap. When the bitmapList changes, this additional bitmap must be recreated.
You could use a backbuffer image to draw all your images into whenever they change, and then draw only the backbuffer image to the screen in onDraw()
// create backbuffer and draw it as seldom as possible
final Bitmap bufferBitmap = Bitmap.createBitmap(320, 240, Bitmap.Config.ARGB_8888);
final Canvas bufferCanvas = new Canvas(bufferBitmap);
for (int i=0; i < bitmapList.size(); i++){
drawBitmap(bufferCanvas, bitmapList.get(i));
}
// draw the buffered image as often as needed
canvas.drawBitmap(bufferBitmap ...);
I have a custom view that fills my entire screen. (A piano keyboard)
When a user touches the key, it causes invalidate() to be called and the whole keyboard gets redrawn to show the new state with a touched key.
Currently the view is very simple, but I plan to add a bit more nice graphics. Since the whole keyboard is dynamically rendered this would make redrawing the entire keyboard more expensive.
So I thought, let's look into partial redrawing. Now I call invalidate(Rect dirty) with the correct dirty region. I set my onDraw(Canvas canvas) method to only draw the keys in the dirty region if I do indeed want a partial redraw. This results in those keys being drawn, but the rest of the keyboard is totally black/not drawn at all.
Am I wrong in expecting that calling invalidate(Rect dirty) would "cache" the current canvas, and only "allows" drawing in the dirty region?
Is there any way I can achieve what I want? (A way to "cache" the canvas and only redraw the dirty area?"
Current nice workaround is to manually cache the full canvas to a bitmap:
private void onDraw(Canvas canvas)
{
if (!initialDrawingIsPerformed)
{
this.cachedBitmap = Bitmap.createBitmap(getWidth(), getHeight(),
Config.ARGB_8888); //Change to lower bitmap config if possible.
Canvas cacheCanvas = new Canvas(this.cachedBitmap);
doInitialDrawing(cacheCanvas);
canvas.drawBitmap(this.cachedBitmap, 0, 0, new Paint());
initialDrawingIsPerformed = true;
}
else
{
canvas.drawBitmap(this.cachedBitmap, 0, 0, new Paint());
doPartialRedraws(canvas);
}
}
Ofcourse, you need to store the info about what to redraw yourself and preferably not use a new Paint everytime, but that are details.
Also note: Bitmaps are quite heavy on the memory usage of your app. I had crashes when I cached a View that was used with a scroller and that was like 5 times the height of the device, since it used > 10MB memory!
To complement Peterdk's answer, you could save your operations in a Picture instead of a Bitmap.
A Bitmap will save all pixels, like
he said it could take a lot of
memory.
A Picture will save the
calls, like drawRect, drawLine, etc.
It depends of what is really heavy in your application : a lot of draw operations, a few draw operations but controlled by heavy calculations, a lot of blank/unused space (prefer Picture) etc...
I'm used to handle graphics with old-school libraries (allegro, GD, pygame), where if I want to copy a part of a bitmap into another... I just use blit.
I'm trying to figure out how to do that in android, and I got very confused.
So... we have these Canvas that are write-only, and Bitmaps that are read-only? It seems too stupid to be real, there must be something I'm missing, but I really can't figure it out.
edit: to be more precise... if bitmaps are read only, and canvas are write only, I can't blit A into B, and then B into C?
The code to copy one bitmap into another is like this:
Rect src = new Rect(0, 0, 50, 50);
Rect dst = new Rect(50, 50, 200, 200);
canvas.drawBitmap(originalBitmap, src, dst, null);
That specifies that you want to copy the top left corner (50x50) of a bitmap, and then stretch that into a 150x150 Bitmap and write it 50px offset from the top left corner of your canvas.
You can trigger drawing via invalidate() but I recommend using a SurfaceView if you're doing animation. The problem with invalidate is that it only draws once the thread goes idle, so you can't use it in a loop - it would only draw the last frame. Here are some links to other questions I've answered about graphics, they might be of use to explain what I mean.
How to draw a rectangle (empty or filled, and a few other options)
How to create a custom SurfaceView for animation
Links to the code for an app with randomly bouncing balls on the screen, also including touch control
Some more info about SurfaceView versus Invalidate()
Some difficulties with manually rotating things
In response to the comments, here is more information:
If you get the Canvas from a SurfaceHolder.lockCanvas() then I don't think you can copy the residual data that was in it into a Bitmap. But that's not what that control is for - you only use than when you've sorted everything out and you're ready to draw.
What you want to do is create a canvas that draws into a bitmap using
Canvas canvas = new Canvas(yourBitmap)
You can then do whatever transformations and drawing ops you want. yourBitmap will contain all the newest information. Then you use the surface holder like so:
Canvas someOtherCanvas = surfaceHolder.lockCanvas()
someOtherCanvas.drawBitmap(yourBitmap, ....)
That way you've always got yourBitmap which has whatever information in it you're trying to preserve.
In android you draw to the canvas, and when you want it to update you call invalidate which will the redraw this canvas to the screen. So I'm guessing you have overridden the onDraw method of your view so just add invalidate();
#Override
public void onDraw(Canvas canvas) {
// Draw a bitmap to the canvas at 0,0
canvas.drawBitmap(mBitmap, 0, 0, null);
// Add in your drawing functions here
super.onDraw(canvas);
// Call invalidate to draw to screen
invalidate();
}
The above code simply redraws the bitmap constantly, of course you want to add in extra thing to draw and consider using a timing function that calls invalidate so that it is not constantly running. I'd advice having a look at the lunarlander sources.