Tuesday, March 27, 2012

So different Canvas

Recently I've faced with one interesting feature of Canvas implementation for Android 2.x and newer versions of Android (3.0 and newer). Sometimes you need to perform your view rendering by yourself by overriding protected void onDraw(android.graphics.Canvas canvas) method:
    @Override
    protected void onDraw(Canvas canvas) {
       ...
    }
You have some image(drawable) and want to impose it on your view. And not just impose, but depending on some preconditions - apply for your image some transformations (like rotation, translation etc).
For example, you need to draw your original image at the top of your view and this image upturned at the bottom of your view. Here's how we did it for older versions of Android (2.3 and older):
int left = someConstantValueX;
int top = someConstantValueY;
int right = someConstantValueX + yourDrawableWidth;
int bottom = someConstantValueY + yourDrawableHeight;

// set bounds of your drawable
yourDrawable.setBounds(left, top, right, bottom);
// draw
yourDrawable.draw(canvas);

...

// you need to perform drawable rotation by rotating canvas

// saving initial matrix
canvas.save();

// translate (move) you canvas to right bottom point of new (required) image
canvas.translate(yourDrawableWidth, yourDrawableHeight + offsetY);

// rotate canvas. In our example we are rotating canvas on 180 degrees counterclockwise
canvas.rotate(-180f);

// set bounds of your drawable
yourDrawable.setBounds(left, -bottom, right, top);
// draw
yourDrawable.draw(canvas);

// restoring matrix
canvas.restore();
Pay attention to this moment:
yourDrawable.setBounds(left, -bottom, right, top);
Since we are working with canvas after performing some transformation (rotation) we need to provide image bounds considering this changes, so because we want to draw our image upturned we rotated canvas on 180 degrees and setting top and bottom of image in reverse order (translate of canvas was performed to place it into required position with respect to rotation). Special attention must be devoted to fact, that second(top) param in setBounds method must be greater than fourth(bottom). Otherwise, yourDrawable will not be drawn. This condition must be performed taking into account transformations of your canvas.
Sometimes all these calculations can be very tiring. Fortunately, in Android 3.0 (and newer) this operation can be simplified:
int left = someConstantValueX;
int top = someConstantValueY;
int right = someConstantValueX + yourDrawableWidth;
int bottom = someConstantValueY + yourDrawableHeight;

yourDrawable.setBounds(left, top, right, bottom);
yourDrawable.draw(canvas);

...

canvas.save();

// only offset is provided
canvas.translate(0, offsetY);

yourDrawable.setBounds(left, top, right, -bottom);
yourDrawable.draw(canvas);

// restoring matrix
canvas.restore();
There is no need to perform rotation and adjust position of your canvas (with respect to rotation). You just pass negative value of your bottom point as fourth param to setBounds.
Much easier, isn't that so?
Important remark: hardware acceleration must be enabled.
The key point here - hardware acceleration. Looks like new rendering pipeline which provided by hardware acceleration not just faster than its predecessor, but also smarter than it. It's hard to say about implementation details (probably, there is no answer in java layer, truth is somewhere deeper, in skia) - it's topic for another investigation. Of course, last example will not work correctly on older versions of Android (2.3 and older) or if hardware acceleration is disabled.