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.
- We wish to modify the hit area to be a square, rather than a circle
- 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
- The return
return ((dxPow dyPow) <= radPow));
which utilizes a few variables, and - 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));
}
}
}