Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Canvas - zooming in, shifting, and scaling on Android

I'm currently implementing a draw function on a webview image (the elephant below). I don't have a problem drawing on it but the zoom function does some funky stuff (2nd image below) on the drawing. When I zoom in, the drawing doesn't zoom but rather shifts over. Drawing at zoomed also doesn't work. My code is below:

Normal

enter image description here

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.save();
    canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
    canvas.drawPath(drawPath, drawPaint);
    canvas.scale(mScaleFactor, mScaleFactor);
    canvas.restore();
    clipBounds = canvas.getClipBounds();
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    mScaleDetector.onTouchEvent(event);
    float touchX = event.getX() / mScaleFactor + clipBounds.left;
    float touchY = event.getY() / mScaleFactor + clipBounds.top;
    if (Deal.on)
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                drawPath.moveTo(touchX, touchY);
                break;
            case MotionEvent.ACTION_MOVE:
                drawPath.lineTo(touchX, touchY);
                break;
            case MotionEvent.ACTION_UP:
                drawCanvas.drawPath(drawPath, drawPaint);
                drawPath.reset();
                break;
            default:
                return false;
    }
    else {
        super.onTouchEvent(event);
    }
    invalidate();
    return true;
}

public class ScaleGestureListener extends SimpleOnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        mScaleFactor *= detector.getScaleFactor();

        // Don't let the object get too small or too large.
        mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));

        invalidate();

        return true;
    }
}

Edit: I managed to get the canvas to redraw with zooming using the GestureDetector's scale factor. Additionally, I have a switch that toggles from drawing to zooming/webview controls. A problem that I run into is that double tap on WebView doesn't trigger the onScale gesture. Which means the canvas doesn't get redrawn and shifts on the zoom in.

I need to implement a feature that detects how much the scale factor is affected by the double tap zoom in. If anyone can suggest a solution to that. Here's the update code:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    clipBounds = canvas.getClipBounds();
    canvas.save();
    drawPaint.setStrokeWidth(8/mScaleFactor);
    canvas.scale(mScaleFactor, mScaleFactor, 0, 0);
    canvas.drawPath(drawPath, drawPaint);
    canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
    canvas.restore();
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    pointerCount = event.getPointerCount();
    mScaleDetector.onTouchEvent(event);
    float touchX = (event.getX() + clipBounds.left) / mScaleFactor;
    float touchY = (event.getY() + clipBounds.top) / mScaleFactor;
    if (Deal.on){
        if (event.getPointerCount() > 1){
            return false;
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                drawPath.moveTo(touchX, touchY);
                break;
            case MotionEvent.ACTION_MOVE:
                drawPath.lineTo(touchX, touchY);
                break;
            case MotionEvent.ACTION_UP:
                drawCanvas.drawPath(drawPath, drawPaint);
                drawPath.reset();
                break;
            default:
                return false;
        }
    }
    else {
        super.onTouchEvent(event);
    }
    invalidate();
    return true;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // Try for a width based on our minimum
   int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
   int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

   //int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(MeasureSpec.getSize(w), heightMeasureSpec, 0);
   setMeasuredDimension(w, h);
}

public class ScaleGestureListener extends SimpleOnScaleGestureListener {

    @Override
    public boolean onScale(ScaleGestureDetector detector) {

        // Don't let the object get too small or too large.
        if(Deal.on == false) {
            mScaleFactor *= detector.getScaleFactor();
            mScaleFactor = Math.max(1f, Math.min(mScaleFactor, 20.0f));
        }

        invalidate();

        return true;
    }
}
like image 692
ono Avatar asked Oct 18 '13 20:10

ono


2 Answers

I have implemented this behaviour, but in a slightly different way. I used a matrix to handle all the zooming and scrolling (and rotation, in my case). It makes for neat code and works like clockwork. I don't know what is causing your funky behaviour, though.

Store a Matrix and another path as class members:

Matrix drawMatrix = new Matrix();
Path transformedPath = new Path();

Replace your onScale:

@Override
public boolean onScale(ScaleGestureDetector detector) {
    Matrix transformationMatrix = new Matrix();

    //Zoom focus is where the fingers are centered, 
    transformationMatrix.postTranslate(-detector.getFocusX(), -detector.getFocusY());

    transformationMatrix.postScale(detector.getScaleFactor(), detector.getScaleFactor());

/* Using getFocuShift allows for scrolling with two pointers down. Remove it to skip this functionality */
    transformationMatrix.postTranslate(detector.getFocusX() + detector.getFocusShiftX(), detector.getFocusY() + detector.getFocusShiftY());

    drawMatrix.postConcat(transformationMatrix);
    invalidate();
    return true;
}

in onDraw; skip saving the canvas, instead:

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

    canvas.drawBitmap(canvasBitmap, drawMatrix, canvasPaint);
    transformedPath.rewind();
    transformedMatrix.addPath(drawPath);
    transformedPath.transform(drawMatrix, null);
    canvas.drawPath(transformedPath, drawPaint);
}

Happy coding!

like image 101
Tore Rudberg Avatar answered Sep 20 '22 13:09

Tore Rudberg


A good way to accomplish all the different effects you are trying to do in a very simple manner is making use of the canvas method

canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint);

This method allow you, to take the whole source bitmap or just a small sample of it (Rect src), and project it as you wish (Rect dst) doing the scaling / translation of matrix calculations automatically for you as shown in the example below:

This example just scales / zooms the whole image...

     Canvas canvas = null;
     Bitmap elephantBmp = null;
     Rect src = new Rect();
     Rect postRect = new Rect();
     Paint paintIfAny = new Paint();

     //To scale the whole image, first take the whole source and then postRect with a bigger size... 
     src.top = src.left = 0;
     src.right = elephantBmp.getWidth();
     src.bottom = elephantBmp.getHeight();
     //Here you have the chance to translate / scale the image(in this case i will not translate it but just scale it...)
     postRect.top = postRect.left = 0;
     postRect.right = (int)(elephantBmp.getWidth() * 1.1F);
     postRect.bottom = (int)(elephantBmp.getHeight() * 1.1F);

     canvas.drawBitmap(elephantBmp, src, postRect, paintIfAny);

And this example takes just a sample of the image and shows it "inner zoom effect"

     Canvas canvas = null;
     Bitmap elephantBmp = null;
     Rect src = new Rect();
     Rect postRect = new Rect();
     Paint paintIfAny = new Paint();

    //To shift the whole image, first take the part of the original source image you want to show 
     src.top = topPosition;//Top coordinate of piece of image to show
     src.left = leftPosition;//Left coordinate of piece of image to show
     src.right = rightPosition;//Right coordinate of piece of image to show
     src.bottom = bottomPosition;//Bottom coordinate of piece of image to show
     //Here you have the chance to show it as big as you want...
     postRect.top = postRect.left = 0;
     postRect.right = (int)(src.width() * 1.1F);
     postRect.bottom = (int)(src.height() * 1.1F);

     canvas.drawBitmap(elephantBmp, src, postRect, paintIfAny);

By making use of these src/dst objects you can do pretty much any effect you want on android, is a simple and really powerful tool

Hope this helps.

Regards!

like image 42
Martin Cazares Avatar answered Sep 20 '22 13:09

Martin Cazares