2

I am trying to create an endless zoomable rings, something like this -

enter image description here

Whenever a user pinch zooms, new small circle at the center is created and the outer circle grows bigger. I am using pinch zoom in relative layout to create the following effect, but not able to achieve it properly. I have created a repo for this, let me know if you can help https://github.com/rohankandwal/zoomable-growing-circles

Update:- Changed dispatchDraw method on the mentioned stackoverflow answer -

protected void dispatchDraw(Canvas canvas) {
    Paint myPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    myPaint.setStyle(Paint.Style.STROKE);
    int strokeWidth = 4;  // or whatever
    myPaint.setStrokeWidth(strokeWidth);
    myPaint.setColor(0xffff0000);   //color.RED
    float radius = (float) (0.5 * (width + height) * 2.5);
    for (int i = 1; i <= 51; i=i+10) {
      canvas.drawCircle(canvas.getWidth() / 2, canvas.getWidth() / 2, (radius) + mScaleFactor + i,
        myPaint);
    }
    canvas.save();
    super.dispatchDraw(canvas);
    canvas.restore();
  }

This code allows circle to grow -

Original image without pinch zoom

Image without zoom

Image when pinch zoomed

Image with zoom

As you can see, zoom is working, but don't know how to create new circles when zoomed at certain level.

Community
  • 1
  • 1
Rohan Kandwal
  • 9,112
  • 8
  • 74
  • 107

2 Answers2

3

If I understand correctly what you want, there is not much to change in your code

In the OnPinchListener :

public boolean onScale(ScaleGestureDetector detector) {
    // 1 : no zoom
    final float zoomLevel = detector.getCurrentSpan() / startingSpan;
    zoomableRadarLayout.scale(zoomLevel, startFocusX, startFocusY);
    return true;
}

and in ZoomableRadarLayout :

// current pinch
private float mScaleFactor = 1;
// previous pinches
private float mAccumulatedScaleFactor = 1;


protected void dispatchDraw(Canvas canvas) {
    ...

    float radius = (float) (0.5 * (width + height) * 2.5);

    // space between circles
    int step = 10;
    // radius of the outer circle
    int outerRadius = (int) (radius * mScaleFactor * mAccumulatedScaleFactor);
    // radius of the inner circle
    int innerRadius = 0;

    for (int i = outerRadius; i >= innerRadius; i = i - step) {
        canvas.drawCircle(canvas.getWidth() / 2, canvas.getWidth() / 2, i, myPaint);
    }

    ...

}

public void restore() {
    // add the ending pinch to the previous ones
    mAccumulatedScaleFactor = mAccumulatedScaleFactor * mScaleFactor;
    mScaleFactor = 1;
    this.invalidate();
}

By the way, if possible it is recommended to avoid creating drawing objects like Paint while drawing. e.g. put the creation in some init method.

bwt
  • 17,292
  • 1
  • 42
  • 60
  • This is just rough code as you might have seen so I haven't added coding standards or improvement. The code looks pretty close to what I want only issue that scaling doesn't work properly with it. Let me know if you can help with it. I am trying on my own too. – Rohan Kandwal Aug 03 '18 at 12:19
  • I mean scaling guesture, it doesn't scale properly – Rohan Kandwal Aug 03 '18 at 12:30
  • seems a bit better now, the dispatchDraw is event simpler – bwt Aug 03 '18 at 13:16
  • Almost perfect, now, only issue, whenever we are starting pinch zoom again, the circles are becoming small and then animating. – Rohan Kandwal Aug 03 '18 at 13:49
  • I'm not sure I understand, you mean you want that the circles stay large when the gesture if finished, so you can start a new pinch to zoom further ? – bwt Aug 03 '18 at 14:14
  • Yes, I mean, it should start from where we ended it, no reset it. – Rohan Kandwal Aug 04 '18 at 04:16
  • ok, I've added a field to accumulate the pinches, instead of resetting each time – bwt Aug 05 '18 at 14:21
1

You can use this approach: calculate radius of inner circle (minimal radius) and then draw circles and increasing radius, while circle can be visible on screen (max radius is not grater than distance from center of screen to one of its corner). Try this custom view with ScaleGestureDetector:

public class ZoomableRingView extends View {

    private Paint mPaint;
    private ScaleGestureDetector mScaleDetector;
    private float mDeltaRadius = 0;
    private final float mBaseRadius = 50;
    private float mPinchBaseRadius = mBaseRadius;
    private float mMinRadius = mBaseRadius;

    public ZoomableRingView(Context context) {
        super(context);
        init(context);
    }

    public ZoomableRingView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public ZoomableRingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public ZoomableRingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    @Override
    public void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        canvas.save();
        drawCircles(canvas);
        canvas.restore();
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        mScaleDetector.onTouchEvent(ev);
        return true;
    }

    private void init(Context context) {
        setWillNotDraw(false);

        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(5);

        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
    }

    private void drawCircles(Canvas canvas) {
        int canvasWidth = canvas.getWidth();
        int canvasHeight = canvas.getHeight();

        int centerX = canvasWidth / 2;
        int centerY = canvasHeight / 2;

        float maxRadius = (float) Math.sqrt(centerX * centerX + centerY * centerY);
        int nCircles = (int) Math.ceil(maxRadius / mBaseRadius) + 1;

        // calculate radius change
        mMinRadius = mPinchBaseRadius + mDeltaRadius / 2;

        // bring radius to [0..2 * mBaseRadius] interval
        while (mMinRadius < 1f) {
            mMinRadius += 2 * mBaseRadius;
        }
        while (mMinRadius > 2 * mBaseRadius) {
            mMinRadius -= 2 * mBaseRadius;
        }

        // draw circles from min to max
        float radius = mMinRadius;
        for (int ixCircle = 0; ixCircle < nCircles; ixCircle++) {
            canvas.drawCircle(centerX, centerY, radius, mPaint);
            radius += 2 * mBaseRadius;
        }
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        float startingSpan;

        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            mPinchBaseRadius = mMinRadius;
            startingSpan = detector.getCurrentSpan();
            return true;
        }

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            mDeltaRadius = detector.getCurrentSpan() - startingSpan;
            invalidate();
            return true;
        }
    }
}

Your activity.xml with that custom view can be like:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:id="@+id/layout_channel1"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">

    <{YOUR_PACKAGE_NAME}.ZoomableRingView
        android:id="@+id/zoomablering_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</RelativeLayout>

And you should get something like that:

Infinite ring zoom

Andrii Omelchenko
  • 13,183
  • 12
  • 43
  • 79