I've been working on a very simple little application using an extended view.
The problem is that i can't find what been pressed in my onTouchEvent.
I've understood that i've to compare the pressure-points (x and y) to the elements and see which one it could be.
But...
I've declared a rectangle using:
Paint color= new Paint();
color.setColor(Color.BLACK);
rectF = new RectF(0.0f, 0.0f, 1.0f, 0.5f) ;
canvas.drawRect(rectF, color);
So far so good, but in the onTouchEvent function
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction() ;
float x = event.getX() ;
float y = event.getY() ;
switch(action) {
case MotionEvent.ACTION_DOWN:
if(rectF().contains(x, y)) {
// Something should happen
}
}
invalidate();
return true ;
}
I set the rectF with some kind of relative points ranging from 0-1, but the X and Y i get from the event ranges from 0 and upwards depending on screen-size.
Can I easily convert either of the values?
Edit
Thought someone was interested in the final solution...
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction() ;
float x = event.getX() ;
float y = event.getY() ;
int width = this.getWidth() ;
int height = this.getHeight() ;
switch(action) {
case MotionEvent.ACTION_DOWN:
if(rectF().contains(x/width, y/height)) {
// Something should happen
}
}
return true ;
}
The values of the touch point and the RectF have to be on the same reference scale in oreder to be compared properly. Meaning that if you declare your RectF to use relative sizes (0-1) you will either need to :
1: normalize the MotionEvent position by your screen size
OR 2: "denormalize" the RectF dimensions by your actual screen size.
I think 1 is generally preferrable as it lets you define your rects in terms of relative layout, independent of the actual screen size and convert the event positions on-the-fly.
like this : rectF.contains(x/screenWidth, y/screenHeight);
You can retrieve the screen dimensions by calling Display display = getWindowManager().getDefaultDisplay() in your Activity and then calling display.getWidth() or display.getHeight()
Related
I am working on android studio and I want to create the effect of throwing an object, in this case a circle drawn on the canvas. But I'm having problems. Can someone guide me?
I take the "X" and the "Y" value where the user touch but how can i make that the circle move in that direction?
Thanks
Game class:
public class Game extends SurfaceView implements View.OnTouchListener {
Paint paint;
int x, y, radius = 100, speedX = 5, speedY = 5, touchY, touchX;
boolean move = false;
boolean one_time = true;
public Game(Context context, AttributeSet attrs) {
super(context, attrs);
this.setOnTouchListener(this);
setFocusable(true);
paint = new Paint();
}
public void onDraw(Canvas canvas){
paint.setColor(Color.WHITE);
canvas.drawRect(0,0,canvas.getWidth(),canvas.getHeight(),paint);
if (one_time == true){
x = canvas.getWidth()/2;
y = canvas.getHeight()/2;
one_time = false;
}
paint.setColor(Color.BLACK);
canvas.drawCircle(x, y, radius, paint);
if (move == true){
if (x >= canvas.getWidth() - radius) {
speedX = -5;
}
if (x <= radius) {
speedX = 5;
}
if (y >= canvas.getHeight() - radius) {
speedY = -5;
}
if (y <= radius) {
speedY = 5;
}
x = x + speedX;
y = y + speedY;
}
invalidate();
}
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case (MotionEvent.ACTION_DOWN):
touchX = (int) motionEvent.getX();
touchY = (int) motionEvent.getY();
move = true;
return true;
default:
return super.onTouchEvent(motionEvent);
}
}
}
Gonna need to be an x+=speedX or y+=speedY in there somewhere so that the x or y value changes causing the ball to move. If your trying to implement some physics in there ur gonna need some maths.
You need a way to change the (x, y) values at a certain interval. You can do this with the Timer class, but then you get into some difficult multithreading code. I suggest looking at using OpenGL ES or a higher level library such as LibGDX. Both of these provide an event loop which allow you to update objects that will be drawn.
Basically you want to move the circle to the place the user touched the screen, using some type of constant speed?
If all you want is to move a circle the Timer can do it - or you could use something like: https://github.com/MasayukiSuda/FPSAnimator
Finally, are you always looking for a linear straight line constant speed movement? Then your math is fine. If your looking for something with gravity etc. then you can reference this: https://developer.android.com/guide/topics/graphics/physics-based-animation.html and consider this: https://github.com/google/liquidfun
This will move the circle exactly to the point the user touched:
theta = atan2(touchY - y,touchX - x)
speedX = cos(theta)
speedY = sin(theta)
x += speedX
y += speedY
What happens once the circle reaches the point depends on how the calculation is implemented. If you want the circle to continue on its path and travel the direction infinitely, you must only calculate the velocity of x and y once. That way the same x and y velocity will always be used to update the position of the circle.
If the velocities are recalculated every time the position of the circle is to be updated, the circle will continuously move to the point even once it has technically reached it and must manually be stopped.
The velocities can be increased by multiplying them by a value greater than 1. To maintain the correct direction, the value should be the same for both velocities.
Example:
theta = atan2(touchY - y,touchX - x)
speedX = maxSpeed*cos(theta)
speedY = maxSpeed*sin(theta)
x += speedX
y += speedY
I need to get the touch x and y with respect to the canvas to check for collisions and things like that after I have moved and scaled the canvas.
I already managed to get the touch coordinate whenever I translate the canvas or scale it around the origin (0,0) by using the following code:
private float convertToCanvasCoordinate(float touchx, float touchy) {
float newX=touchx/scale-translatex;
float newY=touchy/scale-translatey
}
But if I scale the canvas around another point like for example canvas.scale(scale,scale,50,50), it doesn't work .
I know it shouldn't work but I just couldn't figure out how to solve it. I already looked at other questions but none of the answers talks about how to get the coordinate if I scale according to a specific point.
The most basic way to properly do a scene in android is to use a matrix to modify the view and the inverse of that matrix to modify your touches. Here's a simplified answer. Kept very short.
public class SceneView extends View {
Matrix viewMatrix = new Matrix(), invertMatrix = new Matrix();
Paint paint = new Paint();
ArrayList<RectF> rectangles = new ArrayList<>();
RectF moving = null;
public SceneView(Context context) { super(context); }
public SceneView(Context context, AttributeSet attrs) { super(context, attrs); }
public SceneView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }
#Override
public boolean onTouchEvent(MotionEvent event) {
//transform touch. (inverted matrix)
event.transform(invertMatrix);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
moving = null;
//collision detection
for (RectF f : rectangles) {
if (f.contains(event.getX(), event.getY())) {
moving = f;
return true;
}
}
// adding arbitrary transforms.
viewMatrix.postTranslate(50,50);
viewMatrix.postScale(.99f,.99f);
viewMatrix.postRotate(5);
// inverse matrix is needed for touches.
invertMatrix = new Matrix(viewMatrix);
invertMatrix.invert(invertMatrix);
break;
case MotionEvent.ACTION_MOVE:
if (moving != null) {
moving.set(event.getX() - 50, event.getY() - 50, event.getX() + 50, event.getY() + 50);
}
break;
case MotionEvent.ACTION_UP:
if (moving == null) {
rectangles.add(new RectF(event.getX() - 50, event.getY() - 50, event.getX() + 50, event.getY() + 50));
}
break;
}
invalidate();
return true;
}
#Override
protected void onDraw(Canvas canvas) {
// transform the view by the matrix.
canvas.concat(viewMatrix);
// draw objects
for (RectF f : rectangles) {
canvas.drawRect(f,paint);
}
}
This is rather minimalist, but it shows all the relevant aspects.
Moving the view
Touch modification
Collision detection.
Each time you touch the screen it will move diagonally, zoomout, and rotate (basically moves in a spiral), and create a black rectangle. If you touch the rectangles you can move them around to your heart's content. When you click the background, more spiraling the view, dropping black rectangles.
See: https://youtu.be/-XSjanaAdWA
The other way does not work. You could, in theory, take the scene we want and convert that via the View class rather than in the canvas. This would make the touch events occur in the same space as the screen. But Android will void out touch events that occur outside of the view, So MotionEvents that begin outside of the original clipped part of the view will be discarded. So this is a non-starter. You want to transform the canvas, and counter transform the MotionEvents.
private float convertToCanvasXCoordinate(float touchx,float offsetx,float viewportVisibleWidth){
float newx=(touchx*viewportVisibleWidth)/getWidth()+offsetx;
return newx;
}
private float convertToCanvasYCoordinate(float touchy,float offsety,float viewportVisibleHeight){
float newy=(touchy*viewportVisibleHeight)/getHeight()+offsety;
return newy;
}
i just found out there is a function canvas.getClipBound() which is a rectangle representing the visible viewport that includes the offsetx offset y (the left and top of the rectangle respectively) and the viewport width and height
simply call these functions and it will get you touchx and touchy with respect to canvas
This should help:
float px = e.getX() / mScaleFactorX;
float py = e.getY() / mScaleFactorY;
int ipy = (int) py;
int ipx = (int) px;
Rect r = new Rect(ipx, ipy, ipx+2, ipy+2);
And where the canvas is:
final float scaleFactorX = getWidth()/(WIDTH*1.f);
final float scaleFactorY = getHeight()/(HEIGHT*1.f);
if(mScaleFactorX == INVALID){
mScaleFactorX = scaleFactorX;
mScaleFactorY = scaleFactorY;
}
This is a really simple way, and it works because it scales down the onTouch coordinates to be the same min/max as the canvas, causing them to be scaled. Do NOT use getRawX and getRawY because that will return wrong coordinates if you are using a custom layout with the view added and other elements around it. getX and getY returns the accurate coordinates scaled to your view.
This is really simple and does not take up a lot of code. scaleFactor can be used elsewhere to handle zoom(you take care of that code) but what this code does is to handle the issue of getting the pointer coordinates to match the scaled canvas
What I am doing:
I am trying to create a button dynamically onTouch
What is Happening:
I am able to create the button but the button is not created exactly
in the place I touch, instead its little bit in the bottom
right(might be adjustment of button in pixel).
How can i make sure i create the button exactly in the same place i
touch
#Override
public boolean onTouch(View v, MotionEvent event) {
//Get the x & y co-ordinates from the event
float x = event.getX();
float y = event.getY();
//Convert into Integer
int mX = (int) x;
int mY = (int) y;
//Perform Event on touch of canvas
performEventOnTouchOfCanvas(event, mX, mY);
return true;
}
private void performEventOnTouchOfCanvas(MotionEvent event,int mX, int mY) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Point mPoint=new Point(mX,mY);
createButton(mPoint.x,mPoint.y);
break;
}
}
private void createButton(float x, float y) {
Button btn = new Button(ActDrawAreaTwo.this);
RelativeLayout.LayoutParams bp = new RelativeLayout.LayoutParams(40, 40);
bp.leftMargin = (int) x;
bp.topMargin = (int) y;
//Assign the Id to the button
btn.setLayoutParams(bp);
CommonFunctions.setBackgroundDrawable(btn, ActDrawAreaTwo.this, R.drawable.white_circle_dot);//Set Button Drawable
String mTag=String.valueOf((int)x )+","+ String.valueOf((int) y);
btn.setTag(mTag);
canvasLayoutId.addView(btn);
}
Note: canvasLayoutId is a relative layout
Android start drawing from views topmost left side. So when you pass coordinates, it will assume those are topmost left side of the button. If you want your button to appear in the middle of where you touch you need to change your coordinates with these:
x = x + button_width / 2
y = y + button_height / 2
In Android default padding is also same button's border, so you can find button's width and height with using this:
button_width = mButton.getPaddingRight() - mButton.getPaddingLeft();
button_height = mButton.getPaddingBottom() - mButton.getPaddingTop();
You can also use button.getWidth() and button.getHeight() assuming you are not using MATCH_PARENT or WRAP_CONTENT as your paramters.
I am trying to implement zooming on a canvas which should focus on a pivot point. Zooming works fine, but afterwards the user should be able to select elements on the canvas. The problem is, that my translation values seem to be incorrect, because they have a different offset, than the ones where I don't zoom to the pivot point (zoom without pivot point and dragging works fine).
I used some code from this example.
The relevant code is:
class DragView extends View {
private static float MIN_ZOOM = 0.2f;
private static float MAX_ZOOM = 2f;
// These constants specify the mode that we're in
private static int NONE = 0;
private int mode = NONE;
private static int DRAG = 1;
private static int ZOOM = 2;
public ArrayList<ProcessElement> elements;
// Visualization
private boolean checkDisplay = false;
private float displayWidth;
private float displayHeight;
// These two variables keep track of the X and Y coordinate of the finger when it first
// touches the screen
private float startX = 0f;
private float startY = 0f;
// These two variables keep track of the amount we need to translate the canvas along the X
//and the Y coordinate
// Also the offset from initial 0,0
private float translateX = 0f;
private float translateY = 0f;
private float lastGestureX = 0;
private float lastGestureY = 0;
private float scaleFactor = 1.f;
private ScaleGestureDetector detector;
...
private void sharedConstructor() {
elements = new ArrayList<ProcessElement>();
flowElements = new ArrayList<ProcessFlow>();
detector = new ScaleGestureDetector(getContext(), new ScaleListener());
}
/**
* checked once to get the measured screen height/width
* #param hasWindowFocus
*/
#Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (!checkDisplay) {
displayHeight = getMeasuredHeight();
displayWidth = getMeasuredWidth();
checkDisplay = true;
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
ProcessBaseElement lastElement = null;
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mode = DRAG;
// Check if an Element has been touched.
// Need to use the absolute Position that's why we take the offset into consideration
touchedElement = isElementTouched(((translateX * -1) + event.getX()) / scaleFactor, (translateY * -1 + event.getY()) / scaleFactor);
//We assign the current X and Y coordinate of the finger to startX and startY minus the previously translated
//amount for each coordinates This works even when we are translating the first time because the initial
//values for these two variables is zero.
startX = event.getX() - translateX;
startY = event.getY() - translateY;
}
// if an element has been touched -> no need to take offset into consideration, because there's no dragging possible
else {
startX = event.getX();
startY = event.getY();
}
break;
case MotionEvent.ACTION_MOVE:
if (mode != ZOOM) {
if (touchedElement == null) {
translateX = event.getX() - startX;
translateY = event.getY() - startY;
} else {
startX = event.getX();
startY = event.getY();
}
}
if(detector.isInProgress()) {
lastGestureX = detector.getFocusX();
lastGestureY = detector.getFocusY();
}
break;
case MotionEvent.ACTION_UP:
mode = NONE;
break;
case MotionEvent.ACTION_POINTER_DOWN:
mode = ZOOM;
break;
case MotionEvent.ACTION_POINTER_UP:
break;
}
detector.onTouchEvent(event);
invalidate();
return true;
}
private ProcessBaseElement isElementTouched(float x, float y) {
for (int i = elements.size() - 1; i >= 0; i--) {
if (elements.get(i).isTouched(x, y))
return elements.get(i);
}
return null;
}
#Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
if(detector.isInProgress()) {
canvas.scale(scaleFactor,scaleFactor,detector.getFocusX(),detector.getFocusY());
} else
canvas.scale(scaleFactor, scaleFactor,lastGestureX,lastGestureY); // zoom
// canvas.scale(scaleFactor,scaleFactor);
//We need to divide by the scale factor here, otherwise we end up with excessive panning based on our zoom level
//because the translation amount also gets scaled according to how much we've zoomed into the canvas.
canvas.translate(translateX / scaleFactor, translateY / scaleFactor);
drawContent(canvas);
canvas.restore();
}
/**
* scales the canvas
*/
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();
scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));
return true;
}
}
}
Elements are saved with their absolute position on the canvas (with dragging in mind). I suspect that I don't take the new offset from the pivot point to translateX and translateY in consideration, but I can't figure out where and how I should do this.
Any help would be appreciated.
Okay, so you're basically trying to figure out where a certain screen X/Y coordinate corresponds to, after the view has been scaled (s) around a certain pivot point {Px, Py}.
So, let's try to break it down.
For the sake of argument, lets assume that Px & Py = 0, and that s = 2. This means the view was zoomed by a factor of 2, around the top left corner of the view.
In this case, the screen coordinate {0, 0} corresponds to {0, 0} in the view, because that point is the only point which hasn't changed. Generally speaking, if the screen coordinate is equal to the pivot point, then there is no change.
What happens if the user clicks on some other point, lets say {2, 3}? In this case, what was once {2, 3} has now moved by a factor of 2 from the pivot point (which is {0, 0}), and so the corresponding position is {4, 6}.
All this is easy when the pivot point is {0, 0}, but what happens when it's not?
Well, lets look at another case - the pivot point is now the bottom right corner of the view (Width = w, Height = h - {w, h}). Again, if the user clicks at the same position, then the corresponding position is also {w, h}, but lets say the user clicks on some other position, for example {w - 2, h - 3}? The same logic occurs here: The translated position is {w - 4, h - 6}.
To generalize, what we're trying to do is convert the screen coordinates to the translated coordinate. We need to perform the same action on this X/Y coordinate we received that we performed on every pixel in the zoomed view.
Step 1 - we'd like to translate the X/Y position according to the pivot point:
X = X - Px
Y = Y - Py
Step 2 - Then we scale X & Y:
X = X * s
Y = Y * s
Step 3 - Then we translate back:
X = X + Px
Y = Y + Py
If we apply this to the last example I gave (I will only demonstrate for X):
Original value: X = w - 2, Px = w
Step 1: X <-- X - Px = w - 2 - w = -2
Step 2: X <-- X * s = -2 * 2 = -4
Step 3: X <-- X + Px = -4 + w = w - 4
Once you apply this to any X/Y you receive which is relevant prior to the zoom, the point will be translated so that it is relative to the zoomed state.
Hope this helps.
I am using OpenCV4Android FaceDetection sample. I have global table with rectangles of currently spoted faces:
private Rect[] facesArray;
also global floats to store onClick coordinates,
private float onClickX;
private float onClickY;
which are generated from onTouchEvent:
#Override
public boolean onTouchEvent(MotionEvent e) {
onClickX = e.getX();
onClickY = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
// something not important for this matter happens here
}
return super.onTouchEvent(e);
}
In onCameraFrame method before returning mat with view I am doing:
Core.circle(mRgba,new Point(onClickX,onClickY), 40, new Scalar(0, 255, 0, 255));
So what happens. I draw a small, green circle on coordinates that are fetched in onTouchEvent and sent to global variables. Those variables (onClickX, onClickY) are read by onCameraFrame and used for core.circle() function. My problem is that, circle isn't drawn precisely under my finger but always in lower-right place. This is what happens:
https://dl.dropboxusercontent.com/u/108321090/device-2013-07-24-004207.png
And this circle is always in the same direction/position to my finger whenever it is on screen, and meets it in top-left corner, dissapears on bottom right corner (goes outside screen). I tried using getRawX, geyRawY -> result is the same, I don't understeand why get commands doesn't return precise tap position but somewhere near it. I have no clue how to fix this.
This is the solution to your problem.
which is,
#Override
public boolean onTouch(View arg0,MotionEvent event) {
double cols = mRgba.cols();// mRgba is your image frame
double rows = mRgba.rows();
double xOffset = (mOpenCvCameraView.getWidth() - cols) / 2;
double yOffset = (mOpenCvCameraView.getHeight() - rows) / 2;
onClickX = (float)(event).getX() - xOffset;
onClickY = (float)(event).getY() - yOffset;