Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does Canvas determine its clip bounds?

I've been doing some work with Android's Canvas, specifically trying to determine how its getClipBounds results are determined. I understand Canvas internally keeps a transform Matrix which is updated as I call translate, scale, etc. but trying to replicate that Matrix's results has baffled me.

@Override
public void onDraw(Canvas canvas) {
    Rect clipBounds;
    RectF viewport;

    canvas.save();
    canvas.concat(translationMatrix);
    //viewportMatrix.preConcat(canvas.getMatrix());
    viewportMatrix.set(canvas.getMatrix());

    clipBounds = canvas.getClipBounds();
    viewport = GetViewport();

    Log.d("clipBounds", clipBounds.toString() + " (" + clipBounds.width() + ", " + clipBounds.height() + ")");
    Log.d("viewport", viewport.toString() + " (" + viewport.width() + ", " + viewport.height() + ")");

    //drawing is done here

    canvas.restore();
}

//viewport code modeled after http://stackoverflow.com/a/17142856/2245528
private RectF GetViewport() {
    RectF viewport = new RectF();

    viewportMatrix.mapRect(viewport, originalViewport);

    return viewport;
}

private void Translate(float x, float y) {
    translationMatrix.postTranslate(x, y);

    invalidate();
}

private void Scale(float scaleFactor, PointF focusPoint) {
    if (focusPoint == null) {
        translationMatrix.postScale(scaleFactor, scaleFactor);
    }
    //keep the focus point in focus if possible
    else {
        translationMatrix.postScale(scaleFactor, scaleFactor, focusPoint.x, focusPoint.y);
    }

    invalidate();
}

private final Matrix translationMatrix = new Matrix();
private final RectF originalViewport = new RectF();
private final Matrix viewportMatrix = new Matrix();

originalViewport is set to 0, 0, canvas.getWidth(), canvas.getHeight(). Translate and Scale are called from gesture event handlers, which are working correctly.

The part that confuses me is viewportMatrix. It doesn't seem to matter whether I do

viewportMatrix.set(canvas.getMatrix());

or

viewportMatrix.preConcat(canvas.getMatrix());

or even a one-time call to

viewportMatrix.set(canvas.getMatrix());

at the beginning followed by side-by-side Translate/Scale calls to the two matrixes. I've even tried completely ignoring the Canvas's built-in Matrix and rewriting GetViewport as

//viewport code modeled after http://stackoverflow.com/a/17142856/2245528
private RectF GetViewport() {
    RectF viewport = new RectF();

    translationMatrix.mapRect(viewport, originalViewport);

    return viewport;
}

I can never seem to match getClipBounds(), and the discrepancies are fairly serious:

with viewportMatrix.set(canvas.getMatrix):

clipBounds: Rect(-97, -97 - 602, 452) (699, 549)
viewport: RectF(97.04178, 97.06036, 797.04175, 647.06036) (700.0, 550.0)

with viewportMatrix.preConcat(canvas.getMatrix):

clipBounds: Rect(-97, -96 - 602, 453) (699, 549)
viewport: RectF(2708.9663, 2722.2754, 3408.9663, 3272.2754) (700.0, 550.0)

with translationMatrix.mapRect:

clipBounds: Rect(-96, -96 - 603, 453) (699, 549)
viewport: RectF(96.73213, 96.85794, 796.7321, 646.8579) (700.0, 550.0)

with a one-shot call to viewportMatrix.preConcat(canvas.getMatrix()) followed by side-by-side Translate/Scale calls:

clipBounds: Rect(-96, -97 - 603, 452) (699, 549)
viewport: RectF(96.57738, 97.78168, 796.5774, 647.7817) (700.0, 550.0)

with a one-shot call to viewportMatrix.set(canvas.getMatrix()) followed by side-by-side Translate/Scale calls:

clipBounds: Rect(-96, -96 - 603, 453) (699, 549)
viewport: RectF(96.40051, 96.88153, 796.4005, 646.88153) (700.0, 550.0)

I can't even check the Canvas source, as all the Matrix code vanishes into private native calls whose code isn't shown.

Why are my GetViewport calls so grossly off, and what's going on behind-the-scenes with getClipBounds?

like image 400
cf stands with Monica Avatar asked Apr 17 '16 23:04

cf stands with Monica


1 Answers

I read through this answer on GameDev SE, which uses matrix inversion to swap between screen and world coordinate systems:

To go from screen to world space simply use Vector2.Transform. This is commonly used to get the location of the mouse in the world for object picking.

Vector2.Transform(mouseLocation, Matrix.Invert(Camera.TransformMatrix));

To go from world to screen space simply do the opposite.

Vector2.Transform(mouseLocation, Camera.TransformMatrix);

Inspired by this approach, I tried inverting the transformation matrix like this:

private RectF GetViewport() {
    RectF viewport = new RectF();
    Matrix viewportMatrix = new Matrix();

    translationMatrix.invert(viewportMatrix);

    viewportMatrix.mapRect(viewport, originalViewport);

    return viewport;
}

This viewport correctly matches the results returned by getClipBounds.

This matrix tutorial explains what I'd noticed from the results of transform calls, but hadn't applied to my matrix:

In a computer program the camera doesn’t move at all and in actuality, the world is just moving in the opposite direction and orientation of how you would want the camera to move in reality.

In order to understand this correctly, we must think in terms of two different things:

  1. The Camera Transformation Matrix: The transformation that places the camera in the correct position and orientation in world space (this is the transformation that you would apply to a 3D model of the camera if you wanted to represent it in the scene).
  2. The View Matrix: This matrix will transform vertices from world-space to view-space. This matrix is the inverse of the camera’s transformation matrix described above.
like image 95
cf stands with Monica Avatar answered Nov 17 '22 09:11

cf stands with Monica