2

I'm trying to get my app to display a circle that the user can rotate to a certain degree by touching and dragging the circle. Say the maximum the circle can be rotated is 60 degrees, either way.

I've extended the View class to create my custom View. I've drawn a circle on my canvas with the drawCircle() method. To rotate it, I know I'd have to do a canvas.rotate(), but I'm not sure what to fix the center as. How do I measure this angle? What do I rotate about?

public class DrawingView extends View {

    Paint paint;
    Canvas canvas;
    int originalX = 100;
    int originalY = 50;
    int radius = 75;
    // ... some code ...
    // paint, canvas initialised in constructors

    @Override
    protected void onDraw(Canvas canvas){
        canvas.drawCircle(originalX, originalY, radius, paint);
        // some more code ...
    }

   @Override
   public boolean onTouchEvent(MotionEvent event){
       int x = (int)event.getX();         
       int y = (int)event.getY();
       case MotionEvent.ACTION_DOWN:
           // Detect if touch in my circle
       break;             
       case MotionEvent.ACTION_MOVE: 
          double tx = x - originalX;
          double ty = y - originalY;                         
          double tlength = Math.sqrt(tx*tx + ty*ty);
          double angle = Math.acos(radius/tlength);
          double degrees = (angle * 180)/Math.PI;
          System.out.println("Degrees " + (angle*180)/Math.PI);
       break;
   }
}

The code I've displayed doesn't seem to give me a correct value. Like, if I touch and drag all the way to the end of the screen, it gives me a value around 70 degrees. I don't think I'm calculating the angle right, but I have no idea how to either.


I followed the selected answer's way of finding the angle. I then used canvas.rotate() before drawing the circle.

user7999116
  • 199
  • 1
  • 12
  • 1
    The problem is that tx and ty don't necessarily fall on the circle and may be inside or outside of it, so your Math.acos() calculation is off. I think [this](https://stackoverflow.com/a/1211243/6287910) is what you need. (But don't read the comments.) – Cheticamp Mar 28 '19 at 01:25
  • This seems to work too. But the math's a little complicated for me. Thanks though! – user7999116 Mar 28 '19 at 02:18

1 Answers1

1

I'm not sure I have your idea of how this should work down right. But the geometry there is certainly weird. tlength is the radius from the circle center to the fingerpress and you're doing an acos on it. But, that's wrong. acos is which values gives you the cos of that value. In theory that'll be a cosine wave of different angles. Which might be interesting but doesn't sound like what you want. It sounds like you want to call distance from center of 0 to be rotate by -60 degrees and radius rotate by 0 degrees and 2*radius as rotate +60 degrees. So just do that. You linearly interpolate the distance (60° * (distance/radius)) - 60° or whatever.

Though really it seems from a UI standpoint you'd be better off just letting the user change the actual angle on the circle by moving their finger to that angle. Then throw those values in an atan2() and limit it if it's beyond the permitted bounds.


There's many ways to do this. You can calculate the angle based on finger presses. So you can use the known points like center point and current polar coord. So if you have a current angle at distance of r the point at the other end is cos(x)*r, sin(y)*r if you have x and y of the where you want that end point then the distance from the center is sqrt(dx*dx + dy*dy) and the angle it's at is atan2(dy, dx). So you can go from polar to Cartesian with a bit of trig. If you want to call the difference between the center and the radius an angle you can linearly interpolate that.


Here's a quick rendering of a view that does it like that. There are other UI choices that could be done instead like not doing the actual angle but doing the change of the angle, so you're moving the circle like it's a knob or something.

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class RotateCircleView extends View {
    static final double TAU = Math.PI * 2;
    Paint paint;
    double angle = 0;
    double radius = 0;
    double ix = Double.NaN, iy = Double.NaN;

    public RotateCircleView(Context context) {
        super(context);
        init();
    }
    public RotateCircleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    public RotateCircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    public void init() {
        paint = new Paint();
        paint.setAlpha(100);
        paint.setStrokeWidth(20);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        ix = w / 2.0;
        iy = h / 2.0;
        radius = Math.min(ix, iy) / 2.0;
        invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (Double.isNaN(ix)) return;
        super.onDraw(canvas);
        canvas.save();
        canvas.rotate((float) ((TAU * -angle) / 360.0), (float) ix, (float) iy);
        canvas.drawCircle((float) ix, (float) iy, (float) radius, paint);
        double limit = TAU / 6.0;
        if (angle > limit) angle = limit;
        if (angle < -limit) angle = -limit;
        double tx = ix + Math.cos(angle) * radius;
        double ty = iy + Math.sin(angle) * radius;
        canvas.drawLine((float) ix, (float) iy, (float) tx, (float) ty, paint);
        canvas.restore();
    }

    public boolean onTouchEvent(MotionEvent event) {
        double mx = event.getX();
        double my = event.getY();
        double dx = mx - ix;
        double dy = my - iy;
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                //radius = Math.sqrt(dx * dx + dy * dy);
                angle = Math.atan2(dy, dx);
                invalidate();
                break;
        }
        return true;
    }
}
Tatarize
  • 10,238
  • 4
  • 58
  • 64
  • How do I get the user to decide the angle, that is, how do I rotate to the point they put their finger on? And how do I check if this angle falls in my range [-60, 60]? – user7999116 Mar 28 '19 at 01:47
  • There's multiple ways to do the UI there. The math depends a bit which way you're going to do it. If you want the angle changed based on the distance from the center of the circle that's a different set of math. If you want the angle to be whatever their fingerpress is at you do atan2(dy, dx) and say that that is the angle. – Tatarize Mar 28 '19 at 01:56
  • If a,b is the center of my circle and x,y the co-ordinates of the finger press, is dy = y-b, dx = x-a? – user7999116 Mar 28 '19 at 02:00
  • 1
    public static double angleR(double a, double b, double x, double y) { return Math.atan2(y - a, x - b); } – Tatarize Mar 28 '19 at 02:04
  • The main thing to grasp is that when you have two points, one for the center of the circle and another one, you can solve for the angle between them, the distance between them, and if you have a set radius etc. you can mess with the numbers. The angle between those points is Math.atan2(y-a,x-b) the distance is sqrt(dx * dx, dy * dy). If you have the angle and distance it's x = cos(angle)*distance and y = sin(angle)*distance so you can jump between the systems pretty easily. Even doing things like finding the new position on the radius at the angle found by applying delta-angle. – Tatarize Mar 28 '19 at 02:38