Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I place limits on my canvas's translation matrix?

So I'm extending a custom SurfaceView and am attempting to make it have pinch-zoom and scrolling capability.

How I scroll:

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2,
            float distanceX, float distanceY) {

        // subtract scrolled amount
        matrix.postTranslate(-distanceX, -distanceY);

        rebound();

        invalidate();

        // handled
        return true;
    }

How I zoom:

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        if (detector.isInProgress()) {
            // get scale 
            float factor = detector.getScaleFactor();

            // Don't let the object get too small or too large.
            if (factor * scale > 1f) {
                factor = 1f / scale;
            } else if (factor * scale < minScale) {
                factor = minScale / scale;
            }

            // store local scale
            scale *= factor;

            // do the scale
            matrix.preScale(factor, factor, detector.getFocusX(), detector.getFocusY());

            rebound();

            invalidate();
        }

        return true;
    }

(for reference I use this code for onDraw:)

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.save();
    canvas.setMatrix(matrix);
    // [...] stuff drawn with matrix settings
    canvas.restore();
    // [...] stuff drawn without matrix settings, as an overlay
}

Currently both of these methods are functioning well. The zooming is stopped at a minimum (between 0f and 1f) and maximum (currently always 1f) scale value correctly.

Global fields used:

  • drawW, drawH = float, the size in pixels of the canvas data at scale = 1.
  • parentW, parentH = float, the size in pixels of the visible view.
  • matrix = android.graphics.Matrix.

The problem is the "rebound()" (maybe needs a better name, heh) method I'm attempting to implement that would automatically force the contents to stay in view.

I've tried various methods to try to calculate where the bounds should be (within rectangle (0, 0, parentW, parentH)) and translate the matrix "back" when it goes too far.

Here's what I currently have, which definitely doesn't work, instead pushing it farther in the right a lot. I feel like the problem is my math, not my idea. Can someone please come up with simpler or cleaner code that translates the matrix to the edge if its too far and/or fix the problems with my attempt at this implementation? The if checks used to make it appear in the top left

public void rebound() {
    // bounds
    RectF currentBounds = new RectF(0, 0, drawW, drawH);
    matrix.mapRect(currentBounds);
    RectF parentBounds = new RectF(0, 0, parentW, parentH/2);

    PointF diff = new PointF(0, 0);

    if (currentBounds.left > parentBounds.left) {
        diff.x += (parentBounds.left - currentBounds.left);
    }
    if (currentBounds.top > parentBounds.top) {
        diff.y += (parentBounds.top - currentBounds.top);
    }
    if (currentBounds.width() > parentBounds.width()) {
        if (currentBounds.right < parentBounds.right) {
            diff.x += (parentBounds.right - currentBounds.right);
        }
        if (currentBounds.bottom < parentBounds.bottom) {
            diff.y += (parentBounds.bottom - currentBounds.bottom);
        }
    }

    matrix.postTranslate(diff.x, diff.y);
}

A previous version that I wrote before the matrix was a field (I just used canvas.scale() then canvas.translate() in onDraw) that worked:

public void rebound() {
    // bounds
    int boundTop = 0;
    int boundLeft = 0;
    int boundRight = (int)(-scale * drawW + parentW);
    int boundBottom = (int)(-scale * drawH + parentH);

    if (boundLeft >= boundRight) {
        mScrollX = Math.min(boundLeft, Math.max(boundRight, mScrollX));
    } else {
        mScrollX = 0;
    }
    if (boundTop >= boundBottom) {
        mScrollY = Math.min(boundTop, Math.max(boundBottom, mScrollY));
    } else {
        mScrollY = 0;
    }
}

I'm using the new way so that I can correctly scale centered on detector.getFocusX(), detector.getFocusY().

UPDATE: I changed the method to what it is now. It works only somewhat, still bounding the y direction way off-center and is wrong after changing zoom levels. I also made it "preScale" and "postTranslate" so that (as I understand it) it should always be applying the scale then the translate and not mixing them.

FINAL UPDATE: It works now. Here's a working rebound method with comments:

public void rebound() {
    // make a rectangle representing what our current canvas looks like
    RectF currentBounds = new RectF(0, 0, drawW, drawH);
    matrix.mapRect(currentBounds);
    // make a rectangle representing the scroll bounds
    RectF areaBounds = new RectF((float) getLeft(),
                                   (float) getTop(),
                                   (float) parentW + (float) getLeft(),
                                   (float) parentH + (float) getTop());

    // the difference between the current rectangle and the rectangle we want
    PointF diff = new PointF(0f, 0f);

    // x-direction
    if (currentBounds.width() > areaBounds.width()) {
        // allow scrolling only if the amount of content is too wide at this scale
        if (currentBounds.left > areaBounds.left) {
            // stop from scrolling too far left
            diff.x = (areaBounds.left - currentBounds.left);
        }
        if (currentBounds.right < areaBounds.right) {
            // stop from scrolling too far right
            diff.x = (areaBounds.right - currentBounds.right);
        }
    } else {
        // negate any scrolling
        diff.x = (areaBounds.left - currentBounds.left);
    }

    // y-direction
    if (currentBounds.height() > areaBounds.height()) {
        // allow scrolling only if the amount of content is too tall at this scale
        if (currentBounds.top > areaBounds.top) {
            // stop from scrolling too far above
            diff.y = (areaBounds.top - currentBounds.top);
        }
        if (currentBounds.bottom < areaBounds.bottom) {
            // stop from scrolling too far below
            diff.y = (areaBounds.bottom - currentBounds.bottom);
        }
    } else {
        // negate any scrolling
        diff.y = (areaBounds.top - currentBounds.top);
    }

    // translate
    matrix.postTranslate(diff.x, diff.y);
}

It negates any scrolling that I don't want by translating it back to the bounds. It completely negates scrolling if the content is too small, forcing the content to be in the top-left.

like image 920
Ribose Avatar asked Jul 21 '11 08:07

Ribose


1 Answers

I implemented something exactly like this to roll my own pinch to zoom. I suspect the problem with your y-axis being off centre may be down to the view not being the full screen size, or you might not be changing the co-ordinates to match the scale.

My implementation calculates a scale factor, applies it with: canvas.scale( pinchZoomScale, pinchZoomScale ); Then calculates the physical size of the screen in pixels, converts it to metres, and finally applies the scaling factor so all objects drawn are offset correctly.

This implementation relies on always knowing what should be at the centre of the screen and locking on to it, whatever the zoom level.

like image 100
ScouseChris Avatar answered Nov 03 '22 14:11

ScouseChris