Home > database >  App that moves circle button to random location if user clicks near circle
App that moves circle button to random location if user clicks near circle

Time:05-22

I currently have an Android application that moves a drawn circle to a random location on the screen if I click somewhere on the screen that isn't the circle itself.


My question is, how could I implement it so that this logic only gets executed when the click happens relatively close to the circle? How close isn't strictly defined but, say, if a circle is on the bottom left side of the screen, a click on the top right of the screen would not trigger the circle to move to a new location. I'm imagining something like a square that's slightly larger than the circle which would serve as the hitbox(but still exclude clicks inside the circle)

I assume the changes would have to happen in the isInsideCircle function, but I lack the necessary knowledge to understand which variables I should add/subtract to to restrict the region which triggers the logic. Here is the code that I have at the moment:

    public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new Circle(this));
    }
    public class Circle extends View {
        private float x = 300;
        private float y = 300;
        private int r = 150;
        private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        private Random random = new Random();

        // draws circle
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(Color.RED);
            canvas.drawCircle(x, y, r, mPaint);
        }

        // constructor
        public Circle(Context context) {
            super(context);
        }

        // gets random coordinates
        void generateRandom() {

            int w = getWidth() - r - 50;
            int h = getHeight()- r - 50;

            int border = r   50;

            this.x = border   random.nextInt(w-border);
            this.y = border   random.nextInt(h-border);
        }

        // when screen is tapped, old circle removed, new circle drawn
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (!isInsideCircle(event.getX(), event.getY())) { // only register clicks outside of circle
                generateRandom();
                invalidate();
            }
            return super.onTouchEvent(event);
        }


        boolean isInsideCircle(float xPoint, float yPoint) {
            float dx = (x - xPoint);
            float dxPow = (float) Math.pow(dx, 2);
            float dy = (y - yPoint);
            float dyPow = (float) Math.pow(dy, 2);
            float radPow = (float) Math.pow(r, 2);
            return ((dxPow   dyPow) <= radPow));
        }
    }
}

Any help would be greatly appreciated, thanks!

CodePudding user response:

There's a couple parts to this question, that I want to ensure are quite clear before walking through it.

  1. We wish to modify the hit area to be a square, rather than a circle
  2. We want to invert our logic so instead of reacting when outside of the hit area we want to react when it's inside our hit area.

I'm actually going to start with the second point first, then move on to the first point.

Testing for inside, rather than outside

This one's rather straightforward as you're already testing if a click is inside the circle, at least that would be the assumption by the Circle class' isInsideCircle method. It just so happens that it's fairly safe to assume that if you're not (!) inside the circle then you're outside of it. Modifying if (!isInsideCircle(...)) to if(isInsideCircle(...)) (remove the not [!]) operator) should help us in that regard, no? Well that was rather painless, hopefully testing to be within a square is as easy!

Testing for a square, rather than a circle

... will not be as easy as checking if we're inside, rather than outside, a hit area.
I mean it does require an entire shift in the logic that isInsideCircle performs. Also, I'm not sure it's a good idea to change the body of isInsideCircle to test if it's inside a square. That might be a little confusing, no?

So, it's probably a better idea to make a new method to test if a given (x, y) coordinate are within a square. Perhaps isInsideSquare? Wait a second! A Square is most certainly not a Circle so, what information do we need to represent a Square? Well now we need a way to represent a Square AND a way to tell if a coordinate is within a square. A square is a special rectangle where you really only need a special coordinate position (a vertex or the exact center) and a side length to compare coordinates against it. We just so happen to have a coordinate position we can use as the center so, I guess we'll need a side length of the hitbox. Lets add a new field to our circle class to represent the hitbox side length. The following line will default it to 50 pixels more than the circle's diameter.

    private int hitboxLength = (2 * r)   50;

Now we have a coordinate point, which, a little unfortunately, is in the middle of our square and it's side length. So, how do we determine if a point is within our square hitbox? Well it's nice to know a rectangle can also be described as it's 4 vertices. For a square with side length 2 centered at (0, 0) you would have vertices at (1, 1), (1, -1), (-1, -1), and (-1, 1). In order find out if (x, y) is within our rectangle we just have to not it's within those 4 coordinates. With an X ranging from 1 to -1 and it's Y ranging from 1 to -1 as well. Meaning if x is between -1 and 1 -1 <= x <= 1 and y is between -1 and 1 -1 <= y <= 1.

    boolean isInsideSquare(float xPoint, float yPoint) {
        // The center of a square is HALF it's length from any vertex.
        // Always try to use equivalent number types when comparing
        float halfHitboxLength = this.hitboxLength / 2.0F;
        float xUpperBound = this.x   halfHitboxLength;
        float xLowerBound = this.x - halfHitboxLength;
        // smallest expected X <= xPoint <= largest expected X
        boolean xInSquare = xLowerBound <= xPoint && xPoint <= xUpperBound;

        float yUpperBound = this.y   halfHitboxLength;
        float yLowerBound = this.y - halfHitboxLength;
        boolean yInSquare = yLowerBound <= yPoint && yPoint <= yUpperBound;

        return xInSquare && yInSquare;
    }

Now this isn't the most efficient way to handle do this, no doubt, but it should provide us the logic of being within our given square.

Now we just have to use isInsideSquare rather than isInsideCircle within the onTouchEvent method.

    // This "if"
    if (!isInsideCircle(event.getX(), event.getY())) {
    // Would become
    if (!isInsideSquare(event.getX(), event.getY())) {

However, I can't help but think that representing a Circle with a square feels a little weird. At least, given how different of shapes they are. That's not to mention the question of if isInsideSquare even belongs inside a Circle class! How would wanting to represent our circle's hit area with another, slightly larger, circle look?

Testing for a slightly larger circle

We already have the logic for checking if an (x, y) coordinate pair is inside our current circle. I mean there IS a method called isInsideCircle.

        boolean isInsideCircle(float xPoint, float yPoint) {
            float dx = (x - xPoint);
            float dxPow = (float) Math.pow(dx, 2);
            float dy = (y - yPoint);
            float dyPow = (float) Math.pow(dy, 2);
            float radPow = (float) Math.pow(r, 2);
            return ((dxPow   dyPow) <= radPow));
        }

Personally, my attention is immediately drawn to a couple places

  1. The return return ((dxPow dyPow) <= radPow)); which utilizes a few variables, and
  2. The variable with no relation to user input float radPow = (float) Math.pow(r, 2);

The return, because that's how we determine if the given (xPoint, yPoint) pair are within our circle instance, but, more importantly, the radPow variable, as it appears to be referencing, what I can only assume is, the circle's radius, and to test against a larger circle all we really need to do is adjust the radius of it!

    int userWithinPixels = 50;
    float radPow = (float) Math.pow(r   userWithinPixels, 2);

Well that was rather painless!

Again worth pointing out that calling isInsideCircle on a Circle object might be expected to test if it's inside the the represented circle and not a slightly larger circle, but that's still personal preference and you should do what makes sense for the programmers of the project.


Using square

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new Circle(this));
    }
    public class Circle extends View {
        private float x = 300;
        private float y = 300;
        private int r = 150;
        // Add hitbox side length
        private int hitboxLength = (2 * r)   50;
        private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        private Random random = new Random();

        // draws circle
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(Color.RED);
            canvas.drawCircle(x, y, r, mPaint);
        }

        // constructor
        public Circle(Context context) {
            super(context);
        }

        // gets random coordinates
        void generateRandom() {

            int w = getWidth() - r - 50;
            int h = getHeight()- r - 50;

            int border = r   50;

            this.x = border   random.nextInt(w-border);
            this.y = border   random.nextInt(h-border);
        }

        // when screen is tapped, old circle removed, new circle drawn
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (isInsideSquare(event.getX(), event.getY())) { // only register clicks outside of circle
                generateRandom();
                invalidate();
            }
            return super.onTouchEvent(event);
        }

        boolean isInsideSquare(float xPoint, float yPoint) {
            // The center of a square is HALF it's length from any vertex.
            // Always try to use the same number types when comparing/computing
            float halfHitboxLength = this.hitboxLength / 2.0F;
            float xUpperBound = this.x   halfHitboxLength;
            float xLowerBound = this.x - halfHitboxLength;
            // smallest expected X <= xPoint <= largest expected X
            boolean xInSquare = xLowerBound <= xPoint && xPoint <= xUpperBound;

            float yUpperBound = this.y   halfHitboxLength;
            float yLowerBound = this.y - halfHitboxLength;
            boolean yInSquare = yLowerBound <= yPoint && yPoint <= yUpperBound;

            return xInSquare && yInSquare;
        }


        boolean isInsideCircle(float xPoint, float yPoint) {
            float dx = (x - xPoint);
            float dxPow = (float) Math.pow(dx, 2);
            float dy = (y - yPoint);
            float dyPow = (float) Math.pow(dy, 2);
            float radPow = (float) Math.pow(r, 2);
            return ((dxPow   dyPow) <= radPow));
        }
    }
}

Using larger circle

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new Circle(this));
    }
    public class Circle extends View {
        private float x = 300;
        private float y = 300;
        private int r = 150;
        private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        private Random random = new Random();

        // draws circle
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(Color.RED);
            canvas.drawCircle(x, y, r, mPaint);
        }

        // constructor
        public Circle(Context context) {
            super(context);
        }

        // gets random coordinates
        void generateRandom() {

            int w = getWidth() - r - 50;
            int h = getHeight()- r - 50;

            int border = r   50;

            this.x = border   random.nextInt(w-border);
            this.y = border   random.nextInt(h-border);
        }

        // when screen is tapped, old circle removed, new circle drawn
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (isInsideCircle(event.getX(), event.getY())) { // only register clicks outside of circle
                generateRandom();
                invalidate();
            }
            return super.onTouchEvent(event);
        }


        boolean isInsideCircle(float xPoint, float yPoint) {
            float dx = (x - xPoint);
            float dxPow = (float) Math.pow(dx, 2);
            float dy = (y - yPoint);
            float dyPow = (float) Math.pow(dy, 2);
            // maximum amount of pixels user can click off the circle and trigger event
            float userWithinPixels = 25;
            float radPow = (float) Math.pow(r   userWithinPixels, 2);
            return ((dxPow   dyPow) <= radPow));
        }
    }
}
  • Related