Search code examples
androidandroid-canvasscaling

Android: Scaling circles on a canvas with pinch


I currently can place and drag circles around on a canvas, but I'm having trouble understanding how to allow the user to scale circles with a pinch gesture.

The circles are drawn with 'canvas.drawCircle(x,y,radius,paint)'. My initial idea was to listen for the pinch gesture with a custom 'SimpleOnScaleGestureListener', and the multiply the radius of the selected circle by a scale factor. This has produced some pretty unpredictable results, with circles being scaled to tiny or epic proportions from a quick tap with two fingers.

Here's my custom listener:

private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

            @Override
            public boolean onScale(ScaleGestureDetector detector){
                mScaleFactor *= detector.getScaleFactor()

                if (isCircleToolSelected) {
                    // Checks if gesture was made inside of an existing circle
                    // CircleDrawing is a private class that holds x,y,radius,paint values
                    CircleDrawing circle = getCircle(detector.getFocusX(), 
                                                     detector.getFocusY());

                    if (circle != null) {
                       touchedCircle.radius *= mScaleFactor;
                    }
                }
                return true;
            }
        }

This detects the gesture and knows whether the pinch was within a circle on my canvas, but the scaling that is applied is just totally out of control and unusable.

I tried only scaling when the difference between the current and previous scale factors is beyond a certain threshold value, but that just made the scaling less predictable and more choppy.

If anyone has implemented something similar, I would love some direction.


Solution

  • Answering my own question in case someone else gets themselves convinced that you have to scale everything using a ScaleFactor. For basic scaling of a circle, you just need to find the difference between the current and previous value of getCurrentSpan().

    This is the code that I wound up using:

    //Used elsewhere to detect if we've touched an existing circle
    CircleDrawing touchedCircle;
    
    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
    
            private float lastSpan;
            private float lastRadius;
    
            @Override
            public boolean onScaleBegin(ScaleGestureDetector detector) {
    
                //If the user has not already selected a circle
                if (null == touchedCircle) {
    
                    // Check if we started scale gesture within existing circle
                    touchedCircle = getCircle(detector.getFocusX(), detector.getFocusY());
                }
    
                //If a circle has been selected, save radius and span
                if (touchedCircle != null) {
                    lastRadius = touchedCircle.radius;
                    lastSpan = detector.getCurrentSpan();
                }
                return true;
            }
    
            @Override
            public boolean onScale(ScaleGestureDetector detector){
    
                //If the user has the shapes tool selected - handled elsewhere
                if (isShapesSelected) {
    
                    //If the user is touching a circle
                    if (touchedCircle != null) {
    
                        //Value used to calculate new radius
                        float currentSpan = detector.getCurrentSpan();
                        float diameterDiff = currentSpan - lastSpan;
    
                        //Keep track of scaling direction for min and max sizes
                        boolean scalingUp = diameterDiff > 0;
    
                        //Only scale within radius sizes between 150 and 500
                        //Likely will change these values to relate to size of canvas
                        if ((!scalingUp && (lastRadius >= 150)) || (scalingUp && (lastRadius < 500))) {
    
                            //Calculate and set new radius
                            touchedCircle.radius += (diameterDiff)/2;
    
                            //Save radius and span for next frame
                            lastRadius = touchedCircle.radius;
                            lastSpan = detector.getCurrentSpan();
                        }
                    }
                }
                return true;
            }
        }