I'm trying to get the canvas coordinates for an android app that I'm creating. It works great until I add the code to use a scale focus point (following two lines):
scalePoint.setX((int) detector.getFocusX());
scalePoint.setY((int) detector.getFocusY());
Here's my source code for my view class:
package us.kniffin.Jigsaw;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
public class TestView extends View {
private float mLastTouchX;
private float mLastTouchY;
private float mPosX;
private float mPosY;
private Rect rect;
private float cX, cY; // circle coords
// Scaling objects
private ScaleGestureDetector mScaleDetector;
private float mScaleFactor = 1.f;
// The focus point for the scaling
private float scalePointX;
private float scalePointY;
public TestView(Context context) {
super(context);
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint p = new Paint();
p.setColor(Color.RED);
rect = canvas.getClipBounds();
canvas.save();
canvas.scale(mScaleFactor, mScaleFactor, scalePointX, scalePointY);
canvas.translate(mPosX, mPosY);
canvas.drawCircle(cX, cY, 10, p);
canvas.restore();
}
#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()/mScaleFactor;// screen X position
final float y = ev.getY()/mScaleFactor;// screen Y position
cX = x - (rect.left * mScaleFactor) - mPosX; // canvas X
cY = y - (rect.top * mScaleFactor) - mPosY; // canvas Y
// Remember where we started
mLastTouchX = x;
mLastTouchY = y;
break;
}
case MotionEvent.ACTION_MOVE: {
final float x = ev.getX()/mScaleFactor;
final float y = ev.getY()/mScaleFactor;
cX = x - (rect.left * mScaleFactor) - mPosX; // canvas X
cY = y - (rect.top * mScaleFactor) - mPosY; // canvas Y
// Only move if the ScaleGestureDetector isn't processing a gesture.
if (!mScaleDetector.isInProgress()) {
final float dx = x - mLastTouchX; // change in X
final float dy = y - mLastTouchY; // change in Y
mPosX += dx;
mPosY += dy;
invalidate();
}
mLastTouchX = x;
mLastTouchY = y;
break;
}
case MotionEvent.ACTION_UP: {
mLastTouchX = 0;
mLastTouchY = 0;
invalidate();
}
}
return true;
}
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
mScaleFactor *= detector.getScaleFactor();
scalePointX = detector.getFocusX();
scalePointY = detector.getFocusY();
// Don't let the object get too small or too large.
mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 10.0f));
invalidate();
return true;
}
}
}
Any ideas what I need to do to get this working?
Update: I replaced the code sample with another that has the same issue, but is simplified to the essentials
Update Again: The issue occurs after scaling. Before scaling, the coordinates are correct, but afterwards, the coordinates are too far to the right and below where you click. It appears that the more you zoom out, the more wrong they get.
Haha! Success! It took me almost all day, but I figured it out with a bunch of guess and checks.
Here's the bit of code that I needed:
case MotionEvent.ACTION_DOWN: {
final float x = (ev.getX() - scalePointX)/mScaleFactor;
final float y = (ev.getY() - scalePointY)/mScaleFactor;
cX = x - mPosX + scalePointX; // canvas X
cY = y - mPosY + scalePointY; // canvas Y
[snip]
}
And a similar bit of code for ACTION_MOVE
Whenever you use translate, scale, rotate (etc.) you are changing the canvas' matrix.
Generally, this matrix is used to tell you for each (x,y) where it is on the canvas.
When you create a canvas the matrix is the identity matrix:
(1 )
( 1 )
( 1)
But whenever you make one of these changes this matrix also changes.
If you want to know the right coordinates then before scaling/rotating you should use canvas.save() and after scaling use canvas.restore()
I recently came upon this problem while doing something very similiar and after some trial and error and lots of googling I ended up adapting this answer (https://stackoverflow.com/a/9945896/1131180) :
(e is a MotionEvent, so the best place to use this code would be in onTouch or onLongPress)
mClickCoords = new float[2];
//e is the motionevent that contains the screen touch we
//want to translate into a canvas coordinate
mClickCoords[0] = e.getX();
mClickCoords[1] = e.getY();
Matrix matrix = new Matrix();
matrix.set(getMatrix());
//this is where you apply any translations/scaling/rotation etc.
//Typically you want to apply the same adjustments that you apply
//in your onDraw().
matrix.preTranslate(mVirtualX, mVirtualY);
matrix.preScale(mScaleFactor, mScaleFactor, mPivotX, mPivotY);
// invert the matrix, creating the mapping from screen
//coordinates to canvas coordinates
matrix.invert(matrix);
//apply the mapping
matrix.mapPoints(mClickCoords);
//mClickCoords[0] is the canvas x coordinate and
//mClickCoords[1] is the y coordinate.
There are some obvious optimizations that can be applied here but I think it is more clear this way.
Related
I've been trying to get the mouse coordinates from scaled canvas. The canvas.scale(scale, scale) gives me the correct coordinates but if i use canvas.scale(scale, scale, pivotX, pivotY) the coordinates are wrong.
//onDraw
canvas.scale(scaleFactor, scaleFactor); //default pivot x & y = 0, 0
//canvas.scale(scaleFactor, scaleFactor, pivotX, pivotY);
private float pixelWidth = 480;
private float pixelHeight = 320;
//onTouchEvent
touchX = (int) event.getX() * pixelWidth / getWidth();
touchX = (int) event.getY() * pixelHeight / getHeight();
worldX = touchX / scaleFactor;
worldY = touchY / scaleFactor;
What do i need to do to get the correct coordinates from canvas.scale with different pivot other than default 0, 0? I've read & tried about the matrix too but no luck.
solved it using matrix and this code from https://stackoverflow.com/a/20011005/7334711
Matrix matrix = new Matrix();
public void render(Canvas canvas) {
canvas.save();
matrix.setScale(mScaleFactor, mScaleFactor, pivotX, pivotY);
canvas.setMatrix(matrix);
...
float[] mv = new float[9];
#Override
public boolean onTouchEvent(MotionEvent event) {
// Get the values from the matrix into the float array
matrix.getValues(mv);
float touchX = (event.getX()*(1/mv[4]) - (mv[2]/mv[4]));
float touchY = (event.getY()*(1/mv[4]) - (mv[5]/mv[4]));
...
I have an app in which it has a main canvas and i have added another canvas of bitmap on top of it. In the main canvas I have eraser in which it will detect when the user touches the bitmap area. I want to know the x and y inside the bitmap where the eraser touches and so on while moving from main canvas since the bitmap canvas is different from the main canvas. I want the x and y of the main canvas be the same with the bitmap canvas to be move. Thanks
Here's my snippet
public void touch_move(float x, float y) {
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
mPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
mX = x;
mY = y;
}
if(istouchingBitmap(x, y) == true){
float xRatio = (float)bitmap.getWidth() / parent.getWidth();
float yRatio = (float)bitmap.getHeight() / parent.getHeight();
float xPos = lastX * xRatio;
float yPos = lastY * yRatio;
eraseBitmap(bitmap, xPos , yPos , 5);
}
}
function to detect the bitmap when touch
/**
*
* #param x The X location of the cursor in main View.
* #param y The Y location of the cursor in main View.
* #return This is only used to detect if the cursor is touching the Bitmap Image.
*/
public boolean istouchingBitmap(float x, float y) {
matrix.invert(inverse);
float[] pts = {x, y};
inverse.mapPoints(pts);
if (!bounds.contains(pts[0], pts[1])) {
return false;
}
// copy the location
lastX = x;
lastY = y;
return Color.alpha(bitmap.getPixel((int) pts[0], (int) pts[1])) != 0;
}
You only need to get the X and Y values from the touch event.
Then sustract the top,left margin/offset values respectively.
The result is the X,Y coordinate relative to the start of the bitmap.
PD: When doing this, i was having problems handling the status bar height, since the way of measuring it changes between Android version and device model.
Hope this helps.
Finally :). This code will map the screen X and Y to the x and y inside the bitmap. Hope it help others. Good luck
float[] point = new float[] {lastX, lastY};
Matrix inverse = new Matrix();
matrix.invert(inverse);
inverse.mapPoints(point);
bitmapX = point[0];
bitmapY = point[1];
canvas.drawCircle(bitmapX, bitmapY, radius, mPaint); // punch a hole
i have following problem. I have a bitmap and on this bitmap certain draw operations can be done. Like drawing a line, arrwow, etc.
This works pretty well, but the bitmap can be zoomed, dragged and rotated. For this a drawMatrix is used. Everything this works pretty good but, for drawing i need the original coordinates so that the drawings come in the right size, etc.
I can get this original coordinates with the following function. But it doesn't work with rotations. Does anybody have an idea, how to solve this?
/**
* When we have moved or zoomed we need the original image coordinates calculated from
* the screen coordinates. This function gives that back
*/
private Point translateScreenCoordsToOriginalImageCoords(MotionEvent e) {
float[] m = new float[9];
drawMatrix.getValues(m);
float transX = m[Matrix.MTRANS_X] * -1;
float transY = m[Matrix.MTRANS_Y] * -1;
float scaleX = m[Matrix.MSCALE_X];
float scaleY = m[Matrix.MSCALE_Y];
// TODO function does not work when rotated by 90 degree
if (scaleX == 0.0) {
scaleX = -1.0f;
scaleY = -1.0f;
}
int lastTouchX = (int) ((e.getX() + transX) / scaleX);
int lastTouchY = (int) ((e.getY() + transY) / scaleY);
lastTouchX = Math.abs(lastTouchX);
lastTouchY = Math.abs(lastTouchY);
return new Point(lastTouchX, lastTouchY);
}
This is how scaling is performed
/**
* Scale the canvas by
*
* #param scaleFactor e.g 2 is double
* #param focusX the center x point
* #param focusY the center y point
*
* #return true if scaling is performed
*/
private boolean doScale(float scaleFactor, int focusX, int focusY) {
if (this.scaleBoundaries(scaleFactor))
return false;
Matrix transformationMatrix = new Matrix();
// Zoom focus is where the fingers are centered
transformationMatrix.postTranslate(-focusX, -focusY);
transformationMatrix.postScale(scaleFactor, scaleFactor);
transformationMatrix.postTranslate(focusX, focusY);
drawMatrix.postConcat(transformationMatrix);
invalidate();
return true;
}
e.g. This is the draw function of a Line
#Override
public void draw(Canvas c, Matrix drawMatrix, boolean translateCoordiantes) {
float pts[] = new float[] {this.start.x, this.start.y, this.end.x, this.end.y};
if (translateCoordiantes)
drawMatrix.mapPoints(pts);
c.drawLine(pts[0], pts[1], pts[2], pts[3], this.paint);
}
Wow pskink thanks a lot! That was the answer! To get the orignal coordinates back, use:
private Point translateCoordinatesBack(MotionEvent event) {
Matrix inverse = new Matrix();
drawMatrix.invert(inverse);
float[] pts = {event.getX(), event.getY()};
inverse.mapPoints(pts);
return new Point((int) pts[0], (int) pts[1]);
}
Hi,
I have an application which zoom/pans the image within screen bounds.
I have custom ImageView class inside my CustomFrameLayout as defined below.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/darker_gray" >
<com.example.panzoomapplication.panzoom.CustomFrameLayout
android:id="#+id/custom_frame_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.example.panzoomapplication.panzoom.CustomImageView
android:id="#+id/drawing_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawingCacheQuality="high"
android:layout_gravity="center"
android:src="#drawable/image"
android:adjustViewBounds="true" />
</com.example.panzoomapplication.panzoom.CustomFrameLayout>
</FrameLayout>
I am scaling canvas in CustomFrameLayout class using,
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(MIN_ZOOM, Math.min(mScaleFactor, MAX_ZOOM));
calculatePanZoomMatrix();
return true;
}
}
private void calculatePanZoomMatrix() {
mPanZoomMatrix.reset();
// scale the view w.r.t to center of the canvas.
mPanZoomMatrix.setScale(mScaleFactor, mScaleFactor, getWidth() / 2, getHeight() / 2);
invalidate();
}
and in ondraw() of CustomFrameLayout,
#Override
protected void onDraw(Canvas canvas) {
canvas.concat(mPanZoomMatrix);
}
The canvas zooms well and takes correct touch position when mScalFactor = 1 (or in initial state). But when scaled by a factor greater than 1, the touch event coordinate shifts by a certain amount. I have tried to simulate this issue by drawing a circle at touch position in CustomImageView class.
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setPathEffect(null);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setColor(getResources().getColor(R.color.red));
if (mCirclePoint != null)
canvas.drawCircle(touchEvent.x, touchEvent.y, 6, paint);
}
The circle draws well at initial state of canvas (mScalFactor = 1), but shifts its position from actual touch point when scaled by a factor greater than 1. I have tried to fix this issue using matrix as follows,
public float[] getAbsolutePosition(float touchX, float touchY) {
float[] scaledPoints = new float[2];
scaledPoints[0] = touchX ;
scaledPoints[1] = touchY;
Matrix matrix = new Matrix();
matrix.reset();
foat scaleFactor = mCustomFrameLayout.getScaleFactor();
float centerX = mCustomFrameLayout.getCenterScaleX();
float centerY = mCustomFrameLayout.getCenterScaleY();
matrix.setScale(scaleFactor, scaleFactor, centerX, centerY);
matrix.mapPoints(scaledPoints);
return scaledPoints;
}
but end up with same problem.
Could you please help me to fix this issue? I have googled a lot but with no perfect solution.*
You shouldn't use Canvas to scale, is better to use matrix
to get touch coordinates from image(not screen):
void coorImagen(MotionEvent e){
float []m = new float[9];
matrix.getValues(m);
float transX = m[Matrix.MTRANS_X] * -1;
float transY = m[Matrix.MTRANS_Y] * -1;
float scaleX = m[Matrix.MSCALE_X];
float scaleY = m[Matrix.MSCALE_Y];
lastTouchX = (int) ((e.getX() + transX) / scaleX);
lastTouchY = (int) ((e.getY() + transY) / scaleY);
lastTouchX = Math.abs(lastTouchX);
lastTouchY = Math.abs(lastTouchY);
}
to get coordinates of screen use:
e.getX();
e.getY();
i am translating and scaling image by matrix, now i have matrix values . so from matrix value i want to convert my touch coordinates to its position which is getting by matrix. so what should i do for that ?
please help me asap.
private void drawPoint(float x, float y, float pressure, float width) {
// left is tx of matrix
// top is ty of matrix
float curX = (x - left) / (scale * scale);
float curY = (y - top) / (scale * scale);
canvas.drawPoint((curX - left), (curY - top) , mPaint);
}
I know it's an old post, but I had the same problem.
I have an image which is applied a matrix transformation, first I resize it, and then I do a translation.
image = new Matrix();
image.setScale(zoom, zoom);
Paint drawPaint = new Paint();
drawPaint.setAntiAlias(true);
drawPaint.setFilterBitmap(true);
float centerScaledWidth = image_center.x * zoom / 2;
float centerScaledHeigth = image_center.y * zoom / 2;
image.postTranslate(screen_center.x - centerScaledWidth,
screen_center.y - centerScaledHeigth);
canvas.drawBitmap(bmp, image, drawPaint);
To get the points touched on the image I get trace of the matrix image and this is the method:
#Override
public boolean onTouchEvent(MotionEvent 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();
float[] pts = {x, y};
Matrix m = new Matrix();
// This is the magic, I set the new matrix m
// as the inverse of
// the matrix applied to the image
image.invert(m);
// transform the points using the inverse matrix
m.mapPoints(pts);
// do what you have to do
.......
Log.i("transformed", pts[0] +" "+pts[1]);
break;
}
}
return super.onTouchEvent(ev);
}