Android contains method on arcs not working properly

208 Views Asked by At

I have custom view control, which looks like this:

enter image description here

Inside my activity I want to be able to move this view around the screen by dragging it on green Arcs (left or right does not matter).

Also want to be able to detect if yellow arc at top is tapped, middle circle or bottom arc.

I'm having trouble to detect where the tap is in which area. This is the code that I'm using inside of my activity:

float dX, dY;
final MyCustomView myCustomView = (MyCustomView)findViewById(R.id.test);
final Boolean[] movable = {false};

myCustomView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent event) {

                switch (event.getActionMasked()) {

                    case MotionEvent.ACTION_DOWN:
                        movable[0] = false;

                        dX = view.getX() - event.getRawX();
                        dY = view.getY() - event.getRawY();

                        int x = (int) event.getX();
                        int y = (int) event.getY();

                        if (myCustomView.leftArcRegion.contains(x,y) || myCustomView.rightArcRegion.contains(x,y)){
                            movable[0] = true;
                        } else if (myCustomView.topArcRegion.contains(x,y)){
                            //todo: do something if top arc area is selected
                        } else if (myCustomView.midRoundedBitmapRegion.contains(x,y)){
                            //todo: do something if mid bitmap area is selected
                        } else if (myCustomView.bottomArcRegion.contains(x,y)){
                            //todo: do something if bottom arc area is selected
                        }
                    break;

                    case MotionEvent.ACTION_MOVE:
                        if (movable[0]) {
                            view.animate()
                                    .x(event.getRawX() + dX)
                                    .y(event.getRawY() + dY)
                                    .setDuration(0)
                                    .start();
                        }
                        break;

                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:


                        break;
                    default:
                        return false;
                }
                return true;
            }
        });

And these are public fields from my custom view control:

public Region topArcRegion;
private Path topArc;

//topArc is my top arc path
RectF rectFTop = new RectF();
topArc.computeBounds(rectFTop, true);
topArcRegion = new Region();
topArcRegion.setPath(topArc, new Region((int) rectFTop.left, (int) rectFTop.top, 
   (int) rectFTop.right, (int) rectFTop.bottom));

But it looks like it uses rectangular shapes for these regions, not arcs when checking with this "contains" method. And because of that I'm not getting expected results.

So, how can I detect where is initial tap (top arc, bottom arc, side arcs or middle bitmap) in order to apply my app logic?

1

There are 1 best solutions below

1
On BEST ANSWER

Since you're only looking to detect touches inside an arc segment, it should't be too complicated.

Each of your arc segments is defined as the space between two concentric circles and between start and end angles. So all you really want to do is do a little trig to determine the distance from the center of the circles to your touch point and the angle from the center to your touch point.

float x = touchevent.getX();
float y = touchevent.getY();

// Transform relative to arc centers
x -= circle1.x;
y -= circle1.y;

double dist = Math.sqrt(x*x + y*y);
double angle = Math.atan2(y,x) * 180 / Math.PI;

// Given an arc segment defined by circle1, circle2, angle1, angle2:
boolean touch = dist > circle1.radius && dist < circle2.radius &&
                angle > angle1 && angle < angle2;

You'll probably have to play around a bit depending on whether angle1 > angle2 or vice versa. If there's any chance that any of the angles cross the zero-degree angle, it gets a little trickier.


Meta: For clarity, I used sqrt() to compute distance, but you can optimize this code by skipping the sqrt() and compare distance² instead:

double dist2 = x*x + y*y;
if (dist2 > circle1.radius * circle1.radius &&
    dist2 < circle2.radius * circle2.radius &&
    ...

One more edit: computing trig functions can be expensive; certainly a lot more expensive than computing distance².

In the interest of optimization, you should check the distance against the circle radii before bothering with the trig:

boolean touch = dist > circle1.radius && dist < circle2.radius;
if (touch) {
    // This is only a *possible* touch, check the angles now
    double angle = Math.atan2(y,x) * 180 / Math.PI;
    touch = angle > angle1 && angle < angle2;
}