I'm trying to rotate a drawable inside a view by touching it and moving the finger. I've come up with several solutions, but none of them feels natural on the device.
Here's my first approach: Depending on where the user touched the screen and in which direction the finger was moved, I'm changing the drawable's rotation, calculated more or less arbitrarily.
private void updateRotation(float x, float y, float oldX, float oldY) {
int width = getWidth();
int height = getHeight();
float centerX = width / 2;
float centerY = height / 2;
float xSpeed = rotationSpeed(x, oldX, width);
float ySpeed = rotationSpeed(y, oldY, height);
if ((y < centerY && x > oldX)
|| (y > centerY && x < oldX))
rotation += xSpeed;
else if ((y < centerY && x < oldX)
|| (y > centerY && x > oldX))
rotation -= xSpeed;
if ((x > centerX && y > oldY)
|| (x < centerX && y < oldY))
rotation += ySpeed;
else if ((x > centerX && y < oldY)
|| (x < centerX && y > oldY))
rotation -= ySpeed;
}
private static float rotationSpeed(float pos, float oldPos, float max) {
return (Math.abs(pos - oldPos) * 180) / max;
}
This approach had a few annoying side effects: Sometimes the drawable would rotate while the finger wasn't moving and the rotation was generally not as fast as the user's finger.
Hence I threw this code away and started with my second approach. I'm using trigonometry to calculate the actual rotation that would be equivalent to the finger movement:
private void updateRotation(float x, float y, float oldX, float oldY) {
float centerX = getWidth() / 2;
float centerY = getHeight() / 2;
float a = distance(centerX, centerY, x, y);
float b = distance(centerX, centerY, oldX, oldY);
float c = distance(x, y, oldX, oldY);
double r = Math.acos((Math.pow(a, 2) + Math.pow(b, 2) - Math.pow(c, 2))
/ (2 * a * b));
if ((oldY < centerY && x < oldX)
|| (oldY > centerY && x > oldX)
|| (oldX > centerX && y < oldY)
|| (oldX < centerX && y > oldY))
r *= -1;
rotation += (int) Math.toDegrees(r);
}
private float distance(float x1, float y1, float x2, float y2) {
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}
Although this does sound like the correct solution to me, it's not working well either. Finger movement is sometimes ignored - could the calculation be too expensive? Furthermore, the rotation is still a bit faster than the actual finger movement.
Ideally, if I start rotating the drawable touching a specific point, this very point should stay below the finger at all times. Can you tell me how to achieve this?
Edit:
Here's my adoption of Snailer's suggestion. I had to switch the arguments for the atan2 method to rotate into the right direction, now it works great:
private void updateRotation(float x, float y) {
double r = Math.atan2(x - getWidth() / 2, getHeight() / 2 - y);
rotation = (int) Math.toDegrees(r);
}
This can be done easily by getting the angle created by your finger and the center of the screen. Similar to what you have above in the second example. In your onTouchEvent send the getX()/getY() to this method:
private double getDegreesFromTouchEvent(float x, float y){
double delta_x = x - (Screen Width) /2;
double delta_y = (Screen Height) /2 - y;
double radians = Math.atan2(delta_y, delta_x);
return Math.toDegrees(radians);
}
Then when you draw() you can just rotate the Canvas according to results. Obviously, you'll want to use if (MotionEvent.getAction() == MotionEvent.ACTION_MOVE) to update the angle.
Related
Currently I'm drawing a arrow head to my canvas by doing the following:
mPaint.setStyle(Style.FILL);
float deltaX = this.mPoints[1].x - this.mPoints[3].x;
float deltaY = this.mPoints[1].y - this.mPoints[3].y;
float frac = (float) 0.1;
float point_x_1 = this.mPoints[3].x + (1 - frac) * deltaX + frac * deltaY;
float point_y_1 = this.mPoints[3].y + (1 - frac) * deltaY - frac * deltaX;
float point_x_2 = this.mPoints[1].x;
float point_y_2 = this.mPoints[1].y;
float point_x_3 = this.mPoints[3].x + (1 - frac) * deltaX - frac * deltaY;
float point_y_3 = this.mPoints[3].y + (1 - frac) * deltaY + frac * deltaX;
Path path = new Path();
path.setFillType(Path.FillType.EVEN_ODD);
path.moveTo(point_x_1, point_y_1);
path.lineTo(point_x_2, point_y_2);
path.lineTo(point_x_3, point_y_3);
path.lineTo(point_x_1, point_y_1);
path.lineTo(point_x_1, point_y_1);
path.close();
canvas.drawPath(path, mPaint);
The above works fine. The problem I have is that the direction of the arrow is always pointing to the top right corner of my screen.
MY QUESTION:
How can I modify, what I already have, to draw the arrow in the direction of of my onTouchEvent?
It's also worth mentioning that I already get the direction in my onTouchEvent that I pass to my onDraw as shown below:
//Starting point X
float startX = mX; // MotionEvent.ACTION_DOWN ---> mX = event.getX();
//Starting point Y
float startY = mY; // MotionEvent.ACTION_DOWN ---> mY = event.getY();
//Move to X
float stopX = pX; // MotionEvent.ACTION_MOVE ---> pX = event.getX();
//Move to Y
float stopY = pY; // MotionEvent.ACTION_MOVE ---> pY = event.getY();
Using the above, when I draw a line, I would call:
canvas.drawLine(startX, startY, stopX, stopY, mPaint);
This will draw the line in the direction I want.
The problem is that canvas.drawPath takes 2 fields path and paint where canvas.drawLine takes 5 fields startX, startY, stopX, stopY, paint.
Any advise would greatly be appreciated.
I am creating a game which involves shooting in the direction where user clicked.
So from point A(x,y) to point B(x1,y1) i want the bullet bitmap to animate and i have done some calculation/math and figured out some way to do it, but it's not that great-looking doesn't feel so natural.
My approach to doing this is calculate the difference between x and x1 and y and y1 and just scale it.
In example if the X difference between x and x1 is 100 and Y difference between y and y1 i calculate X/Y and get 2.0 which is equal to 2:1 so I know that I should move X two times faster than Y.
Here is my code, if anyone has any suggestions how to make it better, let me know.
float proportion;
float diffX = (x1 - x);
if(diffX == 0) diffX = 0.00001f;
float diffY = (y1 - y);
if(diffY == 0) diffY = 0.00001f;
if(Math.abs(diffX)>Math.abs(diffY)){
proportion = Math.abs(diffX)/Math.abs(diffY);
speedY = 2;
speedX = proportion * speedY;
}
else if(Math.abs(diffX)<Math.abs(diffY)){
proportion = Math.abs(diffY)/Math.abs(diffX);
speedX = 2;
speedY = proportion * speedX;
}
else{
speedX = speedY = 2;
}
if(diffY<0) speedY = -speedY;
if(diffX<0) speedX = -speedX;
if(speedX>=10) speedX = 9;
if(speedX<=-10) speedX = -9;
if(speedY>=10) speedY = 9;
if(speedY<=-10) speedY = -10;
The following implements LERP (linear interpolation) to move you along a straight line.
// move from (x1, y1) to (x2,y2) with speed "speed" (that must be positive)
final double deltay = y2-y1;
final double deltax = x2-x1;
double deltalen = sqrt(deltay*deltay + deltax*deltax);
if (deltalen <= speed)
display(x2, y2);
else {
double finalx = x1 + deltax * speed/deltalen; // surely deltalen > 0, since speed >=0
double finaly = y1 + deltay * speed/deltalen;
display(finalx, finaly);
}
Here is the code to elaborate on my comment:
float slope = (x2 -x1)/(y2 - y1);
float dx = 0.1f; // tweake to set bullet's speed
float x = x1;
while(x < x2)
{
float y = slope*(x - x1) + y1;
DisplayBullet(x, y);
x += dx;
}
// at this point x = x2 and, if everything went right, y = y2
Here I'm assuming that x1 < x2. You'll have to swap points when that's not the case.
here's my problem: I'm making a pool game in android and I want to make the camera rotate freely around the center of the table. The thing is that when I stop my movement it looks like the glulookat resets itself because I only see the same thing ver and over again. If somebody knows a way on how to solve this or another way to do what I watn to do i'd be REALLY appreciated
This is my renderer class:
public void onDrawFrame(GL10 gl) {
// Redraw background color
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
// Set GL_MODELVIEW transformation mode
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity(); // reset the matrix to its default state
// Screen position to angle conversion
theta = (float) ((360.0/screenHeight)*mMoveY*3.0); //3.0 rotations possible
phi = (float) ((360.0/screenWidth)*mMoveX*3.0);
// Spherical to Cartesian conversion.
// Degrees to radians conversion factor 0.0174532
eyeX = (float) (r * Math.sin(theta/**0.0174532*/) * Math.sin(phi/**0.0174532*/));
eyeY = (float) (r * Math.cos(theta/**0.0174532*/));
eyeZ = (float) (r * Math.sin(theta/**0.0174532*/) * Math.cos(phi/**0.0174532*/));
// Reduce theta slightly to obtain another point on the same longitude line on the sphere.
eyeXtemp = (float) (r * Math.sin(theta/**0.0174532*/-dt) * Math.sin(phi/**0.0174532*/));
eyeYtemp = (float) (r * Math.cos(theta/**0.0174532*/-dt));
eyeZtemp = (float) (r * Math.sin(theta/**0.0174532*/-dt) * Math.cos(phi/**0.0174532*/));
// Connect these two points to obtain the camera's up vector.
upX=eyeXtemp-eyeX;
upY=eyeYtemp-eyeY;
upZ=eyeZtemp-eyeZ;
// Set the view point
GLU.gluLookAt(gl, eyeX, eyeY, eyeZ, 0,0,0, upX, upY, upZ);
and here's my onTouch method in my activity class:
public boolean onTouchEvent(MotionEvent e) {
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = x - mPreviousX;
float dy = y - mPreviousY;
// reverse direction of rotation above the mid-line
/*if (y > getHeight() / 2) {
dx = dx * -1 ;
}
// reverse direction of rotation to left of the mid-line
if (x < getWidth() / 2) {
dy = dy * -1 ;
}*/
mRenderer.mMoveX = dx * TOUCH_SCALE_FACTOR/100;
mRenderer.mMoveY = dy * TOUCH_SCALE_FACTOR/100;
requestRender();
}
mPreviousX = x;
mPreviousY = y;
return true;
}
When you do the math, you're using mMoveX and mMoveY. It looks like you should be adding those to other, more persistent x/y variables instead.
Something like:
mCamX += mMoveX;
mCamY += mMoveY;
theta = (float) ((360.0/screenHeight)*mCamY*3.0); //3.0 rotations possible
phi = (float) ((360.0/screenWidth)*mCamX*3.0);
I've been stuck on this problem for eight hours, so I figured it was time to get some help.
Before I begin my problem, I'll just let it be known that I've been through this site and Google, and none of the answers I've found have helped. (This is one, another, and another.)
Here's the deal: I have a class that extends SurfaceView (let's call it MySurface) and overrides many methods in it. Normally, it draws several squares and text boxes, which is all fine. As soon as a user starts touching, it converts to a Bitmap, then draws each frame that until the user releases.
Here's the rub: I want to implement such a functionality that the user can place two fingers on the screen, pinch to zoom, and also pan around (but ONLY with two fingers down).
I found a few implementations of pinch-to-zoom and adapted them to my Canvas object in MySurface via the following:
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.scale(mScaleVector.z, mScaleVector.z); // This is the scale factor as seen below
canvas.translate(mScaleVector.x, mScaleVector.y); // These are offset values from 0,0, both working fine
// Start draw code
// ...
// End draw code
canvas.restore();
}
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
float factor = detector.getScaleFactor();
if (Math.abs(factor - 1.0f) >= 0.0075f) {
mScaleVector.z *= factor;
mScaleVector.z = Math.max(MIN_ZOOM, Math.min(mScaleVector.z, MAX_ZOOM));
}
// ...
invalidate();
return true;
}
}
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction() & MotionEvent.ACTION_MASK;
int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
if (event.getPointerCount() == 2) {
if (action == MotionEvent.ACTION_POINTER_DOWN && pointerIndex == 1) {
// The various pivot coordinate codes would belong here
}
}
detector.onTouchEvent(event); // Calls the Scale Gesture Detector
return true;
}
While both elements work fine--the scrolling back and forth and the pinch-to-zoom--there is one large problem. The pinch-to-zoom, when used, zooms into the point 0,0, instead of zooming into the finger point.
I've tried a lot of ways to fix this:
Using canvas.scale(mScaleVector.z, mScaleVector.z, mScaleVector.x, mScaleVector.y);; obviously, this produces unwanted results as the mScaleVector x and y values are 0-offsets.
Managing a "pivot" coordinate that uses the same offset as the translate() method, but this produces either the same 0,0 issue, or jumping around when the view is touched.
Numerous other things... I've done a lot with the aforementioned pivot coordinate, trying to base its location on the user's first touch, and moving it relative to that touch each successive gesture.
Additionally, this canvas must be bounded, so the user cannot scroll forever. However, when I use the .scale(sx, sy, px, py) method, it pushes things beyond any bounds I set in .translate().
I'm... pretty much open to anything at this point. I know this functionality can be added, as it is seen in the Android 4.0 gallery (when viewing a single image). I've tried to track down the source code that handles this, to no avail.
Here is the code I use to implement pinch zoom in an ImageView using ScaleGestureDetector. With little or no modification you should be able to use it too, since you can use transformation marices too, to draw on a Canvas.
#Override
public boolean onScale(ScaleGestureDetector detector) {
float mScaleFactor = (float) Math.min(
Math.max(.8f, detector.getScaleFactor()), 1.2);
float origScale = saveScale;
saveScale *= mScaleFactor;
if (saveScale > maxScale) {
saveScale = maxScale;
mScaleFactor = maxScale / origScale;
} else if (saveScale < minScale) {
saveScale = minScale;
mScaleFactor = minScale / origScale;
}
right = width * saveScale - width
- (2 * redundantXSpace * saveScale);
bottom = height * saveScale - height
- (2 * redundantYSpace * saveScale);
if (origWidth * saveScale <= width
|| origHeight * saveScale <= height) {
matrix.postScale(mScaleFactor, mScaleFactor, width / 2, height / 2);
if (mScaleFactor < 1) {
matrix.getValues(m);
float x = m[Matrix.MTRANS_X];
float y = m[Matrix.MTRANS_Y];
if (mScaleFactor < 1) {
if (Math.round(origWidth * saveScale) < width) {
if (y < -bottom)
matrix.postTranslate(0, -(y + bottom));
else if (y > 0)
matrix.postTranslate(0, -y);
} else {
if (x < -right)
matrix.postTranslate(-(x + right), 0);
else if (x > 0)
matrix.postTranslate(-x, 0);
}
}
}
} else {
matrix.postScale(mScaleFactor, mScaleFactor, detector.getFocusX(), detector.getFocusY());
matrix.getValues(m);
float x = m[Matrix.MTRANS_X];
float y = m[Matrix.MTRANS_Y];
if (mScaleFactor < 1) {
if (x < -right)
matrix.postTranslate(-(x + right), 0);
else if (x > 0)
matrix.postTranslate(-x, 0);
if (y < -bottom)
matrix.postTranslate(0, -(y + bottom));
else if (y > 0)
matrix.postTranslate(0, -y);
}
}
return true;
}
In my case, I computed the neccesary values in the onMeasure() method of the View, you might want to do this somewhere else in your SurfaceView
width = MeasureSpec.getSize(widthMeasureSpec); // Change this according to your screen size
height = MeasureSpec.getSize(heightMeasureSpec); // Change this according to your screen size
// Fit to screen.
float scale;
float scaleX = (float) width / (float) bmWidth;
float scaleY = (float) height / (float) bmHeight;
scale = Math.min(scaleX, scaleY);
matrix.setScale(scale, scale);
setImageMatrix(matrix);
saveScale = 1f;
scaleMappingRatio = saveScale / scale;
// Center the image
redundantYSpace = (float) height - (scale * (float) bmHeight);
redundantXSpace = (float) width - (scale * (float) bmWidth);
redundantYSpace /= (float) 2;
redundantXSpace /= (float) 2;
matrix.postTranslate(redundantXSpace, redundantYSpace);
origWidth = width - 2 * redundantXSpace;
origHeight = height - 2 * redundantYSpace;
right = width * saveScale - width - (2 * redundantXSpace * saveScale);
bottom = height * saveScale - height - (2 * redundantYSpace * saveScale);
setImageMatrix(matrix);
A little explanation:
saveScale is the current scale ratio of the Bitmap
mScaleFactor is the factor you have to multiply your scale ratio with.
maxScale and minScale can be constant values.
width and height are the dimensions of the screen.
redundantXSpace and redundantYSpace are the empty between the image borders and screen borders since the image is centered when in it is smaller then the screen
origHeight and origWidth are the sizes of the bitmap
matrix is the current transformation matrix used to draw the bitmap
The trick is, that when I first scaled and centered the image on initialization, I picked that scale ratio to be 1 and with scaleMappingRatio I mapped the actual scale values of the image relative to that.
protected override void OnDraw(Canvas canvas)
{
canvas.Save();
int x = (int)(canvas.Width * mScaleFactor - canvas.Width) / 2;
int y = (int)(canvas.Height * mScaleFactor - canvas.Height) / 2;
if (mPosX > (canvas.Width + x - 50))
{
mPosX = canvas.Width + x - 50;
}
if (mPosX < (50 - canvas.Width - x))
{
mPosX = 50 - canvas.Width - x;
}
if (mPosY > (canvas.Height + y - 50))
{
mPosY = canvas.Height + y - 50;
}
if (mPosY < (50 - canvas.Height - y))
{
mPosY = 50 - canvas.Height - y;
}
canvas.Translate(mPosX, mPosY);
canvas.Scale(mScaleFactor, mScaleFactor, canvas.Width/2, canvas.Height/2);
base.OnDraw(canvas);
canvas.Restore();
}
This is monodroid code, but can be easily converted to JAVA.
You may choose whatever values for your bound (here is 50 pixels margin)
I'm writing an app for android (although I think this is a generic question) and I need to display a large image (in an ImageView) that can be scrolled and zoomed. I've managed to get scrolling to work by capturing touch events and performing matrix translations, and i'm now working on zooming.
If I simply apply a scale transformation to the image, it zooms in at the origin, which is the top left-hand corner of the screen. I would like to zoom in at the center of the screen.
From what i've read, this means I need a transformation to make the origin the center of the screen. I think what is required is something like the following- assume the center of the screen is (5, 5) for simplicity...
-Translate by (-5, -5)
-Scale by the zoom factor
-Translate by (+5, +5)*zoomfactor
Unfortunatly, this doesnt seem to work- the zoom seems to go anywhere BUT the center...can someone help me out here?
EDIT: This is the code that now works
Matrix zoommatrix = new Matrix();
float[] centerpoint = {targetimageview.getWidth()/2.0f, targetimageview.getHeight()/2.0f};
zoommatrix.postScale(zoomfactor, zoomfactor, centerpoint[0], centerpoint[1]);
zoommatrix.preConcat(targetimageview.getImageMatrix());
targetimageview.setImageMatrix(zoommatrix);
targetimageview.invalidate();
Check ImageViewTouchBase in the Android source code's Camera app; its "zoomTo" method does this:
protected void zoomTo(float scale, float centerX, float centerY) {
if (scale > mMaxZoom) {
scale = mMaxZoom;
}
float oldScale = getScale();
float deltaScale = scale / oldScale;
mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY);
setImageMatrix(getImageViewMatrix());
center(true, true);
}
That center method is probably the bit you'll really care about:
protected void center(boolean horizontal, boolean vertical) {
if (mBitmapDisplayed.getBitmap() == null) {
return;
}
Matrix m = getImageViewMatrix();
RectF rect = new RectF(0, 0,
mBitmapDisplayed.getBitmap().getWidth(),
mBitmapDisplayed.getBitmap().getHeight());
m.mapRect(rect);
float height = rect.height();
float width = rect.width();
float deltaX = 0, deltaY = 0;
if (vertical) {
int viewHeight = getHeight();
if (height < viewHeight) {
deltaY = (viewHeight - height) / 2 - rect.top;
} else if (rect.top > 0) {
deltaY = -rect.top;
} else if (rect.bottom < viewHeight) {
deltaY = getHeight() - rect.bottom;
}
}
if (horizontal) {
int viewWidth = getWidth();
if (width < viewWidth) {
deltaX = (viewWidth - width) / 2 - rect.left;
} else if (rect.left > 0) {
deltaX = -rect.left;
} else if (rect.right < viewWidth) {
deltaX = viewWidth - rect.right;
}
}
postTranslate(deltaX, deltaY);
setImageMatrix(getImageViewMatrix());
}