How to avoid spend long time in onDraw? - android

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.

Related

Android: drawing on large bitmap, 2 layers, best way?

im working on an app, that displays large(around 2000x2000px) bitmap in imageview. This image has to be that large since user can pinch to zoom it in order to see some details. App has to be able to draw circles on that image, and also to display image alone, without circles on it. I was using 2 layers but the problem is memory since 2k x 2k px is around 16mb of memory, and creating another bitmap(another 16mb), just to draw a few circles, is pointless in my opinion. Is there any way, that you can draw simple primitives on image, and also be able to display it without primitives(circles in my case)?
Maybe somehow to store only modified pixels or sth?
Thanks!
You don't need to make another 2000x2000 Bitmap to draw those circles on. Just 'prerender' a circle, and then choose where you draw it.
I'm working under the assumption that you're drawing your 'big' image on a Canvas, since you have zooming features etc.
If you're not, you'll need to override your SurfaceView's onDraw(Canvas canvas) method so that you can access the SurfaceView Canvas. I won't go into depth about that part since again I'm assuming you have it, but if not the implementation of that function would look like this:
//Overriding SurfaceView onDraw(Canvas canvas)
#Override
protected void onDraw(Canvas surfaceCanvas) {
if(canvas == null) return; //No Canvas? No point in drawing then.
surfaceCanvas.drawColor(Color.BLACK);
//Draw your 'big' image on the SurfaceView Canvas
insertYourBigImageDrawingFunctionHere(surfaceCanvas);
//Now draw your circles at their correct positions...
insertCircleDrawingFunctionHere(surfaceCanvas);
}
Now that you have access to the SurfaceView Canvas, you can choose precisely how things are drawn on it. Like circles for example...
I want to draw your attention to the multiple Canvas' being used below (surfaceCanvas vs. circleCanvas). I once thought that Canvas was a kind-of 'one Canvas for the whole app/activity' implementation, but it isn't. You are free to create Canvas' as you please. It is merely an instance of a tool to draw onto Bitmaps. This was a HUGE revelation for me, and gave me much more robust control over how Bitmaps are composed.
public void myCircleDrawingFunction(Canvas surfaceCanvas){
//Make a new Bitmap for your circle
Bitmap.Config conf = Bitmap.Config.ARGB_4444;
tinyCircleBMP = Bitmap.createBitmap(10,10, conf);
//Make a new canvas using that Bitmap as the source...
Canvas circleCanvas = new Canvas(cacheBmp);
//Now, perform your drawing on the `Canvas`...
Paint p = new Paint();
circleCanvas.drawCircle(5, 5, 5, p);
//Now the `Bitmap` has a circle on it, draw the `Bitmap` on the `SufaceView Canvas`
surfaceCanvas.drawBitmap(tinyCircleBMP, 10, 10, p);
//Replace the '10's in the above function with relevant coordinates.
}
Now obviously, your circles will zoom/pan differently to your 'big' image, since they are no longer being drawn at the same size/position of the 'big' image. You will need to consider how to translate the positions of each circle taking into account the current scale and position of the 'big' image.
For example, if your image is zoomed in to 200%, and a circle is supposed to appear 100px from the left of the big image, then you should multiply the pixel values to take into account the zoom, like this
(PsuedoCode):
drawCircleAtX = Bitmap.left * BitmapZoomFactor
If you are using the canvas API (if not I would suggest to)? if so you are just draw your image on the canvas and then the primitive shapes on top of the same canvas before display. This way you just keep a reference of the circles position in some basic data types and scale them as the user moves around and zooms, so you know where to draw them each frame.

Fast tap on TextView clearing random portion of my Custom View

I am using one 40 miliseconds timer and am plotting graph with this speed. (In each 40 milisecsnds of onDraw() call , it is plotting 5 points).
but on the layout same time when I am taping on one TextView, it is clearing random portion of Custom View.
So my question Is
Is it related with cache Drawing. ?
I tried with setDrawingcacheEnabled(true); but this also is not working.
Any idea ?
This is my onDraw()
#Override
public void onDraw(Canvas canvas)
{
super.onDraw(canvas);
//setDrawingCacheEnabled(true);
//canvas.drawBitmap(bBitMap,0,0,null);
drawStuff(canvas);
}
My cache memory Size is 256k and my screen resulotion is 800x480

Osmdroid custom marker drawables can't draw text or shapes

I'm currently using the latest osmdroid library (3.0.10) to show my custom markers on a map with an ItemizedOverlay. I've extended the Drawable class to implement my own drawing. However my drawables are not drawn correctly. I draw a bitmap image, a circle and a text on top of each other. The bitmap is drawn, but the circle and the text is not visible. When I use the same drawable in an imageview, everything is OK.
Here is the code of my drawable's onDraw method:
#Override
public void draw(Canvas canvas) {
canvas.save();
canvas.translate(getBounds().left, getBounds().top);
//this draws fine
canvas.drawBitmap(bitmap, new Rect(0,0,bitmap.getWidth(),bitmap.getHeight()), new Rect(0,0,mWidth,mWidth), mPaint);
//this is not
canvas.drawCircle(mHeight/2, mHeight/2, mHeight/2, mPaint);
//neither
canvas.drawText("X", mHeight/2, mHeight/2, mPaint2);
canvas.restore();
}
I've tried drawing the circle and the text on a bitmap, and drawing that on the provided canvas. It's working but that kills the whole point.
Any help is appreciated.
After a day of going trough the osmdroid sources, i managed to resolve my problems, but haven't found why are they there in the first place. Osmdroid uses an ISafeCanvas interface (and SafeTranslatedCanvas implementation) to wrap a canvas to deal with some high zoom level translate glitches. While I haven't found any straightforward errors in the code, I bypassed the mechanics when drawing my overlays by overriding the ItemizedOverlay's draw method and making my own ISafeCanvas implementation, which only returns the encapsulated canvas in it's getSafeCanvas method, and now everything looks fine.
#Override
protected void draw(Canvas canvas, MapView mv, boolean shadow) {
drawSafe(new MyCanvas(canvas),mv,shadow);
}
This is far from a best solution, the SafeCanvas is there for a reason, so I suggest a thorough testing if using this method.
Note: I used only Android 4.1.1, and don't know if this issue is present on other devices.
Please take a look at Issue 427. Essentially you want to use:
canvas.getUnsafeCanvas(new UnsafeCanvasHandler() {
// Draw circle and text here
// (but not drawBitmap)
});
Keep an eye on the ticket for a more permanent solution.

Android: How to get a custom view to redraw partially?

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...

How to blit() in android?

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.

Categories

Resources