Can we hardware accelerate an off-screen canvas? - android

I'm creating an off-screen bitmap+canvas, drawing a bunch of smaller bitmaps into it, then drawing it into a view. The isHardwareAccelerated() method returns false for my canvas:
mBitmap = new Bitmap(500, 500, Bitmap.Config.RGB_565);
mCanvas = new Canvas(mBitmap);
mCanvas.isHardwareAccelerated(); // false
I can see that the canvas given to me in my View's onDraw() method is hardware accelerated though:
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.isHardwareAccelerated(); // true
// My current approach, but maybe better to draw directly
// to the view's canvas if it's hardware accelerated?
canvas.drawBitmap(mBitmap, ...);
}
I'm wondering if we can turn this on for the off-screen canvas?
I'm also wondering if I should just draw directly to the view's hardware-accelerated canvas instead of bothering with the off-screen one, if it's not going to be accelerated. I thought it would be faster drawing everything to the offscreen one first.
I have to reorganize my code to test this out, just wondering if I'm missing something obvious for the off-screen one.
Thanks
------- Update -----------------------------------
I refactored my draw code to just draw directly to the view's canvas in onDraw(), instead of using the off-screen canvas. Performance is way better.
It would still be nice to enable hardware acceleration for the off-screen canvas though, there are definitely tons of use-cases for that.

Related

Achieve setShadowLayer effect during drawCircle (or drawRoundRect) without turning off hardware acceleration

Currently, I have an app which support Android 2.3 and above.
In my custom view drawing operation, I need to drop shadow while drawing circle.
ballPaint.setShadowLayer(shadowSize, shadowSize, shadowSize, historyChartBallShadow);
canvas.drawCircle(px, py, this.ballSize, ballPaint);
I also understand that, with hardware acceleration turned on, I will not such shadow effect
setShadowLayer Android API differences
However, I realize once hardware acceleration is turned off through view.setLayerType(View.LAYER_TYPE_SOFTWARE, null), my entire custom view drawing operation become very slow.
I was wondering, is there any other way to achieve similar shadow effect. (A "blurred" black circle shadow)
without turning off hardware acceleration?
p/s Even when I want to use BlurMaskFilter from Android draw with blur, I realize it doesn't support hardware acceleration too.
I had found a way to achieve such. First, we construct the ball + shadow off-screen image. Note, by using off-screen bitmap drawing technique, no GPU will be involved. The key is, don't use the Canvas from onDraw to perform drawCircle.
Instead, construct our very own Canvas, backed by an off-screen bitmap.
private Bitmap generateBallBitmap() {
final int ballShadowSize = Utils.dpToPixel(BALL_SHADOW_SIZE);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(historyChartBallColor);
paint.setShadowLayer(ballShadowSize, ballShadowSize, ballShadowSize, historyChartBallShadow);
Bitmap bitmap = Bitmap.createBitmap((int)((ballSize + ballShadowSize)*2f), (int)((ballSize + ballShadowSize)*2f), Bitmap.Config.ARGB_8888);
bitmap.eraseColor(Color.TRANSPARENT);
Canvas canvas = new Canvas(bitmap);
canvas.drawCircle(ballSize, ballSize, ballSize, paint);
return bitmap;
}
In custom view onDraw function, just draw the off-screen bitmap directly.
if (null == this.ballBitmap) {
this.ballBitmap = generateBallBitmap();
}
canvas.drawBitmap(this.ballBitmap, px - this.ballSize, py, null);
For the entire process, I merely depend on the default value of layer type, without calling setLayerType explicitly.
The outcome is fast, yet shadow effect is visible too.

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.

Android Canvas and Hardware Acceleration?

Hi all I was wondering if it was possible to draw to an offscreen Canvas / Bitmap and take advantage of hardware acceleration or do I have to draw inside the onDraw() method of the View
For example I draw to an offscreen Bitmap by doing the following:
Bitmap.Config config = Bitmap.Config.ARGB_8888;
Bitmap buffer = Bitmap.createBitmap(200, 200, config);
Canvas canvas = new Canvas(buffer);
Paint paint = new Paint();
paint.setColor(Color.RED);
canvas.drawLine(0, 0, 100, 100, paint);
However canvas.isHardwareAccelerated() returns false and drawing is sluggish compared to:
protected void onDraw(Canvas canvas) {
Paint paint = new Paint();
paint.setColor(Color.RED);
canvas.drawLine(0, 0, 100, 100, paint);
}
where canvas.isHardwareAccelerated() returns true. Is there a way to draw to a Bitmap while taking advantage of hardware acceleration? Or do I have to draw directly to the screen in the onDraw method?
Thank you for your help :) I know in Java I can draw to a BufferedImage offscreen and it'll be hardware accelerated but maybe its not the same on a phone...
Because in first case , you create the canvas, it is not backed up by a hardware layer.
Also, as of now, you cannot enable HWA on just any Canvas, it must belong to an HWA View.
A view on the other hand, has access to system's hardware capabilities. You can use that by calling setLayerType( LAYER_TYPE_HARDWARE) as described in docs:
Indicates that the view has a hardware layer. A hardware layer is
backed by a hardware specific texture (generally Frame Buffer Objects
or FBO on OpenGL hardware) and causes the view to be rendered using
Android's hardware rendering pipeline, but only if hardware
acceleration is turned on for the view hierarchy. When hardware
acceleration is turned off, hardware layers behave exactly as software
layers.
Also, The performance concern applies more to the rendering part, and less to drawing. All draw operations are recorded as a Picture object. Its the rendering operation where HWA plays an important role.

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