Canvas gamecamera - locationtranslate values messed up by applied canvas scaling - android

I'm working on an android game and am currently busy with a gamecamera type of component.
The best method seems to be translating the canvas (possibly with a matrix) to move the entire canvas to act like a gamecamera. (please correct me if I'm wrong).
this is all getting along pretty nicely, the gamecamera is moving and the x and y values of the camera are being used as guidelines to translate the canvas location:
public void onMove(float distanceX, float distanceY){
Camera.x += (int) distanceX;
Camera.y += (int) distanceY;
}
protected void onDraw(Canvas canvas) {
canvas.translate(Camera.x, Camera.y);
level.draw(canvas);
//the text is a ui component so shouldn't move with the level
canvas.restore();
canvas.drawText(fps + " fps", 0, 32, textPaint);
}
When trying to interact with the world I simply add the camera's coordinates to the touch's coordinates to get the correct point in the world:
public void onSingleTap(MotionEvent event) {
float mouseX = event.getX() - Camera.x;
float mouseY = event.getY() - Camera.y;
}
However, The user also has the choice to scale the screen, zooming to a point on the world.
case MotionEvent.ACTION_MOVE:
if (MODE == ZOOM && event.getPointerCount() == 2) {
float newDist = spacing(event);
if (newDist > 10f) {
Camera.zoom = newDist / PrevMultiDist;
//the midpoint for the zoom
midPoint(midpoint, event);
Camera.setPivot(midpoint.x, midpoint.y);
translateMatrix.postScale(Camera.zoom, Camera.zoom, Camera.pivotX, Camera.pivotY);
}
}
break;
The zoom is messing up the world-touch translation coordinates since the view is being moved to zoom on the chosen spot. I think if I substract the zoom offset value from the camera location that I will get the correct position in the world again.
So the touchevent becomes something like this:
public void onSingleTap(MotionEvent event) {
//ideally the zoomoffset might be added in the camera's location already?
float mouseX = event.getX() - Camera.x - zoomOffsetX;
float mouseY = event.getY() - Camera.y - zoomOffsetY;
}
I already found this question: Android get Bitmap Rect (left, top, right, bottom) on a Canvas but it doesn't provide the awsner I want and I can't figure out if his calculations can help me.
I feel like I'm missing something really simple. Hope someone can help!

Related

Android Canvas. Moving and Rotating Bitmap along Circular Path based on Touch?

Is it Possible to move and rotate an Image along a Circular Path based on a touch event as follows:
I have looked at this question:
Moving an Image in circular motion based on touch events in android
But it only tells me how to move the image along a circle, not rotate it.
Update: Full example posted on GitHub at https://github.com/jselbie/xkcdclock
Every time you get a touch event, grab the touch point's x,y coordinates and compute the angle of the rotation relative to the center of bitmap. Use that value to determine how much to rotate the bitmap you want draw.
First, let's assume a logical coordinate system in which the center point of your element above is at (0,0) in x,y space.
Therefore, the angle (in degrees) between any touch point relative to the center can be computed as follows:
double ComputeAngle(float x, float y)
{
final double RADS_TO_DEGREES = 360 / (java.lang.Math.PI*2);
double result = java.lang.Math.atan2(y,x) * RADS_TO_DEGREES;
if (result < 0)
{
result = 360 + result;
}
return result;
}
Note - the normalization of negative angles to positive angles. So if the touch point is (20,20), this function above will return 45 degrees.
To make use of this method, your Activity will need the following member variables defined:
float _refX; // x coordinate of last touch event
float _refY; // y coordinate or last touch event
float _rotation; // what angle should the source image be rotated at
float _centerX; // the actual center coordinate of the canvas we are drawing on
float _centerY; // the actual center coordinate of the canvas we are drawing on
Now let's examine how to keep track of touch coordinates to we can always have an up to date "_rotation" variable.
So our "touch handler" for Android will look something like this:
boolean onTouch(View v, MotionEvent event)
{
int action = event.getAction();
int actionmasked = event.getActionMasked();
if (!_initialized)
{
// if we haven't computed _centerX and _centerY yet, just bail
return false;
}
if (actionmasked == MotionEvent.ACTION_DOWN)
{
_refX = event.getX();
_refY = event.getY();
return true;
}
else if (actionmasked == MotionEvent.ACTION_MOVE)
{
// normalize our touch event's X and Y coordinates to be relative to the center coordinate
float x = event.getX() - _centerX;
float y = _centerY - event.getY();
if ((x != 0) && (y != 0))
{
double angleB = ComputeAngle(x, y);
x = _refX - _centerX;
y = _centerY - _refY;
double angleA = ComputeAngle(x,y);
_rotation += (float)(angleA - angleB);
this.invalidate(); // tell the view to redraw itself
}
}
There's some fine details left out such as drawing the actual bitmap. You might also want to handle the ACTION_UP and ACTION_CANCEL events to normalize _rotation to always be between 0 and 360. But the main point is that the above code is a framework for computing the _rotation at which your Bitmap should be drawn on the View. Something like the following:
void DrawBitmapInCenter(Bitmap bmp, float scale, float rotation, Canvas canvas)
{
canvas.save();
canvas.translate(canvas.getWidth()/2, canvas.getHeight()/2);
canvas.scale(scale, scale);
canvas.rotate(rotation);
canvas.translate(-bmp.getWidth()/2, -bmp.getHeight()/2);
canvas.drawBitmap(bmp, 0, 0, _paint);
canvas.restore();
}

Issue with rotating a bitmap on custom view based on touch position on the screen on Android

I am having problems implementing this pattern. I have an custom view which has an image drawn on it. The user can take a corner of the image and drag it around, which results in rotation of the image.
In order to achieve this result I did the following:
The bitmap is stored in an object which also has angle, x, y, centerX and centerY properties.
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int currentX = (int) event.getX();
int currentY = (int) event.getY();
double rotationAngleRadians = Math.atan2(currentX - image.getCenterX(), image.getCenterY() - currentY);
int rotationAngleDegrees = (int) Math.toDegrees(rotationAngleRadians);
rotateMatrix.setRotate(rotationAngleDegrees , image.getCenterX(), image.getCenterX());
.....
}
and then in onDraw I use the matrix to rotate the canvas
#Override
protected void onDraw(Canvas canvas) {
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.setMatrix(rotateMatrix);
canvas.drawBitmap(image.getBitmap, image.getX(), image.getY(), imagePaint);
canvas.restore();
}
The problem with this approach is the following: when The user starts to drag the button, the whole image "jumps up" 25 pixels and after that it starts to rotate. How can I fix this problem ?
It's possible that when you give the canvas your matrix, the canvas is disregarding a previous translation which was defined in its previous matrix.
A cleaner approach would be to just use the canvas.rotate(...) method, instead of using matrixes. If you must use the matrix class, then make sure you're basing it on the original canvas.getMatrix()

How to keep a dot drawn on canvas at a fixed point on screen, even when the canvas is pinch zoomed? - Android

I have a background image as a drawable in my custom view. This drawable may be pinch zoomed or moved.
Currently I need a green dot that is drawn on the image to be stationary relative to the screen. That is, it should be always at the same position with the pin as shown below. (Of course, the pin is simply an ImageView and does NOT move at all!)
I have successfully made it stationary relative to the screen, when the map behind is moved as follows in my custom view, MapView:
#Override
public boolean onTouchEvent(MotionEvent ev) {
// Let the ScaleGestureDetector inspect all events.
mScaleDetector.onTouchEvent(ev);
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
mLastTouchX = x;
mLastTouchY = y;
mActivePointerId = ev.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE: { // triggered as long as finger movers
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(pointerIndex);
final float y = ev.getY(pointerIndex);
// Only move if the ScaleGestureDetector isn't processing a gesture.
if (!mScaleDetector.isInProgress()) {
final float dx = x - mLastTouchX;
final float dy = y - mLastTouchY;
mPosX += dx;
mPosY += dy;
// update the starting point if the 'Start' button is not yet pressed
// to ensure the screen center (i.e. the pin) is always the starting point
if (!isStarted) {
Constant.setInitialX(Constant.INITIAL_X - dx);
Constant.setInitialY(Constant.INITIAL_Y - dy);
if ((historyXSeries.size() > 0) && (historyYSeries.size() > 0)) {
// new initial starting point
historyXSeries.set(0, Constant.INITIAL_X);
historyYSeries.set(0, Constant.INITIAL_Y);
}
}
invalidate();
}
mLastTouchX = x;
mLastTouchY = y;
break;
}
By doing that above, my green dot stays there, when the background image is moved.
But I have problems in trying to make it stay there, when the background image is zoomed.
Essentially, I don't really understand how canvas.scale(mScaleFactor, mScaleFactor) works, and therefore I cannot move the green dot accordingly like what I have done in the simple moving case.
I think something should be added in the scale listener handler below, could anybody help me fill that part?
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
mScaleFactor *= detector.getScaleFactor();
// Don't let the object get too small or too large.
mScaleFactor = Math.max(1f, Math.min(mScaleFactor, 10.0f)); // 1 ~ 10
// HOW TO MOVE THE GREEN DOT HERE??
invalidate();
return true;
}
Or please at least explain how canvas.scale(mScaleFactor, mScaleFactor) works, and how may I move the green dot accordingly?
Keep in mind that the canvas is thought to scale everything according to the scale factor, so while going against the zoom is possible, it is probably not the best approach. However, if this is what you're looking for, I will help you as best as I can.
I am assuming the following:
Scale factor is relative to the current zoom (old zoom is always scale factor 1). If this is not the case, then you should observe the zoom values after scaling roughly 200% two times and seeing if the resulting scale factor is 4 or 3 (exponential or linear). You can achieve the results below by normalizing the scale factor to 2 for a zoom factor of 200%, for example. You'll have to remember the old scale factor in order to do so.
No rotation is performed
If this is the case then following can be said for a marker with respect to the zoom center.
For every horizonal pixel x away from the zoom center after zoom, its original position could be calculated to be: zoom_center_x + *x* / scale_factor (or alternatively zoom_center_x + (marker_x - zoom_center_x) / scale_factor). In other words, if zoom center is (50, 0) and the marker is (100, 0) with a scale factor of 2, then the x position of the marker prior to the zoom was 50 + (100 - 50) / 2 or 75. Obviously, if the marker is in the same position of the zoom center, then the x position will be the same as the zoom center. Similarly, if the scale is 1, then the x position for the marker will be the same as it is now.
The same can be applied to the y axis.
While I can't know exactly how to set the position of your marker, I would expect the code to look something like:
Point zoomCenter = detector.getZoomCenter();
// Set marker variable here
marker.setX(Math.round(zoomCenter.getX() + ((double)(marker.getX() - zoomCenter.getX())) / mScaleFactor));
marker.setY(Math.round(zoomCenter.getY() + ((double)(marker.getY() - zoomCenter.getY())) / mScaleFactor));
I hope that helps.

How to find absolute position of click while zoomed in

Please see each section below for a description of my problem described in three separate ways. Hopefully should help people to answer.
Problem: How do you find a pair of coordinate expressed in canvas/userspace when you only have the coordinate expressed in terms of a zoomed image, given the original scale point & scale factor?
Problem in practice:
I'm currently trying to replicate the zoom functionality used in apps such as the gallery / maps, when you can pinch to zoom/zoom out with the zoom moving towards the midpoint of the pinch.
On down I save the centre point of the zoom (which is in X,Y coordinates based on the current screen). I then have this function act when a "scale" gesture is detected:
class ImageScaleGestureDetector extends SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
if(mScaleAllowed)
mCustomImageView.scale(detector.getScaleFactor(), mCenterX, mCenterY);
return true;
}
}
The scale function of the CustomImageView look like this:
public boolean scale(float scaleFactor, float focusX, float focusY) {
mScaleFactor *= scaleFactor;
// Don't let the object get too small or too large.
mScaleFactor = Math.max(MINIMUM_SCALE_VALUE, Math.min(mScaleFactor, 5.0f));
mCenterScaleX = focusX;
mCenterScaleY = focusY;
invalidate();
return true;
}
The drawing of the scaled image is achieved by overriding the onDraw method which scales the canvas around the centre ands draw's the image to it.
#Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.translate(mCenterScaleX, mCenterScaleY);
canvas.scale(mScaleFactor, mScaleFactor);
canvas.translate(-mCenterScaleX, -mCenterScaleY);
mIcon.draw(canvas);
canvas.restore();
}
This all works fine when scaling from ScaleFactor 1, this is because the initial mCenterX and mCenterY are coordinates which are based on the device screen. 10, 10 on the device is 10, 10 on the canvas.
After you have already zoomed however, then next time you click position 10, 10 it will no longer correspond to 10, 10 in the canvas because of the scaling & transforming that has already been performed.
Problem in abstraction:
The image below is an example of a zoom operation around centre point A. Each box represents the position and size of the view when at that scale factor (1, 2, 3, 4, 5).
In the example if you scaled by a factor of 2 around A then you clicked on position B, the X, Y reported as B would be based on the screen position - not on the position relative to 0,0 of the initial canvas.
I need to find a way of getting the absolute position of B.
So, after redrawing the problem I've found the solution I was looking for. It's gone through a few iteration's but here's how I worked it out:
B - Point, Center of the scale operation
A1, A2, A3 - Points, equal in user-space but different in canvas-space.
You know the values for Bx and By because they are always constant no matter what the scale factor (You know this value in both canvas-space and in user-space).
You know Ax & Ay in user-space so you can find the distance between Ax to Bx and Ay to By. This measurement is in user-space, to convert it to a canvas-space measurement simply divide it by the scale factor. (Once converted to canvas-space, you can see these lines in red, orange and yellow).
As point B is constant, the distance between it and the edges are constant (These are represented by Blue Lines). This value is equal in user-space and canvas-space.
You know the width of the Canvas in canvas-space so by subtracting these two canvas space measurements (Ax to Bx and Bx to Edge) from the total width you are left with the coordinates for point A in canvas-space:
public float[] getAbsolutePosition(float Ax, float Ay) {
float fromAxToBxInCanvasSpace = (mCenterScaleX - Ax) / mScaleFactor;
float fromBxToCanvasEdge = mCanvasWidth - Bx;
float x = mCanvasWidth - fromAxToBxInCanvasSpace - fromBxToCanvasEdge;
float fromAyToByInCanvasSpace = (mCenterScaleY - Ay) / mScaleFactor;
float fromByToCanvasEdge = mCanvasHeight - By;
float y = mCanvasHeight - fromAyToByInCanvasSpace - fromByToCanvasEdge;
return new float[] { x, y };
}
The above code and image describe when you're clicking to the top left of the original centre. I used the same logic to find A no matter which quadrant it was located in and refactored to the following:
public float[] getAbsolutePosition(float Ax, float Ay) {
float x = getAbsolutePosition(mBx, Ax);
float y = getAbsolutePosition(mBy, Ay);
return new float[] { x, y };
}
private float getAbsolutePosition(float oldCenter, float newCenter, float mScaleFactor) {
if(newCenter > oldCenter) {
return oldCenter + ((newCenter - oldCenter) / mScaleFactor);
} else {
return oldCenter - ((oldCenter - newCenter) / mScaleFactor);
}
}
Here is my solution based on Graeme's answer:
public float[] getAbsolutePosition(float Ax, float Ay) {
MatrixContext.drawMatrix.getValues(mMatrixValues);
float x = mWidth - ((mMatrixValues[Matrix.MTRANS_X] - Ax) / mMatrixValues[Matrix.MSCALE_X])
- (mWidth - getTranslationX());
float y = mHeight - ((mMatrixValues[Matrix.MTRANS_Y] - Ay) / mMatrixValues[Matrix.MSCALE_X])
- (mHeight - getTranslationY());
return new float[] { x, y };
}
the parameters Ax and Ay are the points which user touch via onTouch(), I owned my static matrix instance in MatrixContext class to hold the previous scaled/translated values.
Really sorry this is a brief answer, in a rush. But I've been looking at this recently too - I found http://code.google.com/p/android-multitouch-controller/ to do what you want (I think - I had to skim read your post). Hope this helps. I'll have a proper look tonight if this doesn't help and see if I can help further.

Limit down scaling of a Bitmap in a SurfaceView, Android

im having a single Bitmap in a surfaceview.
I am using multi touch to handle gestures such as zoom and drag, the problem is that when i scale the bitmap i dont want it to be able to be down scaled that much it wont cover the whole display(surface view). I have found no way to work around this since i cant find a way to get the current downscaled bitmaps current height or width.
Here's some of my code:
private void multiTouchBehavior(MotionEvent event)
{
if(event.getAction() == MotionEvent.ACTION_POINTER_2_DOWN)
{
_oldDist = spacing(event);
if(_oldDist > 10f) // just be secure of a bug in the API
{
_saved.set(_matrix);
_mid = midPoint(event);
}
}
else if(event.getAction() == MotionEvent.ACTION_MOVE)
{
_newDist = spacing(event);
if(_newDist > 10f)
{
_matrix.set(_saved);
float scale = _newDist / _oldDist;
_matrix.postScale(scale, scale, _mid.x, _mid.y);
}
}
}
And here is what happens in method spacing(event) :
private float spacing(MotionEvent event) {
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return FloatMath.sqrt(x * x + y * y);
}
My onDraw(canvas) is just canvas.drawBitmap(myBitmap, _matrix, null)
Anyone know how i could solve this problem?
So just to make sure you get my problem right think of it as a big map of the world that i want to able to zoom in to, and of course zoom out on when iv'e already zoomed in, but NOT allowing it to be out zoomed more than it is at start.
Found my solution, there probably is a better way but now I use:
matrix.mapRadius(1);
that i can compare to a variable initialized to that method-value in beggining.
Edit:
float[] f = new float[9];
matrix.getValues(f);
float xScale = f[0];
float yScale = f[3];
works better and is easier tho.

Categories

Resources