When I research approaches for touch pan/zoom on an image, I generally find effective, simple code--but nothing that does quite what I want. The image needs to never show a blank space between the edge of the actual image (bitmap) and its View. If the bitmap is 200x100 and the View is 50x50, the user should only be able to zoom out to 100x50, allowing them to slide the image horizontally, but not vertically.
My code does this well when moving (translating) the image--until the image is zoomed. Then something is thrown off; I can move the bitmap far enough to see gaps around it. It's probably something simple and obvious related to factoring pixel measurements by the current scale factor, but I can't find it. I suspect it has to do with the calculations of maxX and maxY in onDraw() below. Any ideas?
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.widget.ImageView;
/**
* Most code from
* http://android-developers.blogspot.com/2010/06/making-sense-of-multitouch.html
*
* #author Chad Schultz
*
*/
public class PanZoomImageView extends ImageView {
public static final String TAG = PanZoomImageView.class.getName();
private static final int INVALID_POINTER_ID = -1;
// The ‘active pointer’ is the one currently moving our object.
private int mActivePointerId = INVALID_POINTER_ID;
private Bitmap bitmap;
private ScaleGestureDetector mScaleDetector;
private float mScaleFactor = 1.f;
private float minScaleFactor;
private float mPosX;
private float mPosY;
private float mLastTouchX, mLastTouchY;
private boolean firstDraw = true;
private boolean panEnabled = true;
private boolean zoomEnabled = true;
public PanZoomImageView(Context context) {
super(context);
setup();
}
public PanZoomImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setup();
}
public PanZoomImageView(Context context, AttributeSet attrs) {
super(context, attrs);
setup();
}
private void setup() {
mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
}
#Override
public void setImageBitmap(Bitmap bmp) {
super.setImageBitmap(bmp);
bitmap = bmp;
firstDraw = true;
}
#Override
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
bitmap = ((BitmapDrawable) drawable).getBitmap();
firstDraw = true;
}
public void onDraw(Canvas canvas) {
Log.v(TAG, "onDraw()");
if (bitmap == null) {
Log.w(TAG, "nothing to draw - bitmap is null");
super.onDraw(canvas);
return;
}
if (firstDraw
&& (bitmap.getHeight() > 0)
&& (bitmap.getWidth() > 0)
&& (canvas.getHeight() > 0)
&& (canvas.getWidth() > 0)) {
//Don't let the user zoom out so much that the image is smaller
//than its containing frame
float minXScaleFactor = (float) canvas.getWidth() / (float) bitmap.getWidth();
float minYScaleFactor = (float) canvas.getHeight() / (float) bitmap.getHeight();
minScaleFactor = Math.max(minXScaleFactor, minYScaleFactor);
Log.d(TAG, "minScaleFactor: " + minScaleFactor);
firstDraw = false;
}
mScaleFactor = Math.max(mScaleFactor, minScaleFactor);
Log.d(TAG, "mScaleFactor: " + mScaleFactor);
//Save the canvas without translating (panning) or scaling (zooming)
//After each change, restore to this state, instead of compounding
//changes upon changes
canvas.save();
int maxX, minX, maxY, minY;
//How far can we move the image horizontally without having a gap between image and frame?
maxX = (int) (mScaleFactor * (bitmap.getWidth() / 2) - (canvas.getWidth() / 2));
minX = -1 * maxX;
//How far can we move the image vertically without having a gap between image and frame?
maxY = (int) (mScaleFactor * (bitmap.getHeight() / 2) - (canvas.getWidth() / 2));
minY = -1 * maxY;
//Do not go beyond the boundaries of the image
if (mPosX > maxX) {
mPosX = maxX;
}
if (mPosX < minX) {
mPosX = minX;
}
if (mPosY > maxY) {
mPosY = maxY;
}
if (mPosY < minY) {
mPosY = minY;
}
Log.d(TAG, "canvas width: " + canvas.getWidth() + " canvas height: "
+ canvas.getHeight());
Log.d(TAG, "bitmap width: " + bitmap.getWidth() + " height: " + bitmap.getHeight());
Log.d(TAG, "translating mPosX: " + mPosX + " mPosY: " + mPosY);
if (zoomEnabled) {
Log.d(TAG, "zooming to scale factor of " + mScaleFactor);
canvas.scale(mScaleFactor, mScaleFactor);
} else {
Log.d(TAG, "zooming disabled");
}
if (panEnabled) {
Log.d(TAG, "panning to " + mPosX + "," + mPosY);
canvas.translate(mPosX, mPosY);
} else {
Log.d(TAG, "panning disabled");
}
super.onDraw(canvas);
canvas.restore(); //clear translation/scaling
}
#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: {
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()) {
float dx = x - mLastTouchX;
float dy = y - mLastTouchY;
//Adjust for zoom factor. Otherwise, the user's finger moving 10 pixels
//at 200% zoom causes the image to slide 20 pixels instead of perfectly
//following the user's touch
dx /= mScaleFactor;
dy /= mScaleFactor;
mPosX += dx;
mPosY += dy;
invalidate();
}
mLastTouchX = x;
mLastTouchY = y;
break;
}
case MotionEvent.ACTION_UP: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_CANCEL: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
break;
}
}
return true;
}
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(0.1f, Math.min(mScaleFactor, 5.0f));
Log.d(TAG, "detector scale factor: " + detector.getScaleFactor() + " mscalefactor: " + mScaleFactor);
invalidate();
return true;
}
}
//Currently zoomEnabled/panEnabled can only be set programmatically, not in XML
public boolean isPanEnabled() {
return panEnabled;
}
public void setPanEnabled(boolean panEnabled) {
this.panEnabled = panEnabled;
}
public boolean isZoomEnabled() {
return zoomEnabled;
}
public void setZoomEnabled(boolean zoomEnabled) {
this.zoomEnabled = zoomEnabled;
}
}
Here's what I eventually came up with on my own after a great deal of painful experimentation--learning some interesting things along the way about how Bitmaps are handled in Android. This code is far from perfect, but it suits my purposes--hopefully it will help others as well.
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
/**
* #author Chad Schultz
* #version 1
*/
public class PanZoomView extends View {
public static final String TAG = PanZoomView.class.getName();
private static final int INVALID_POINTER_ID = -1;
// The ‘active pointer’ is the one currently moving our object.
private int mActivePointerId = INVALID_POINTER_ID;
private Bitmap bitmap;
private float viewHeight;
private float viewWidth;
float canvasWidth, canvasHeight;
private ScaleGestureDetector mScaleDetector;
private float mScaleFactor = 1.f;
private float minScaleFactor;
private float mPosX;
private float mPosY;
private float mLastTouchX, mLastTouchY;
private boolean firstDraw = true;
private boolean panEnabled = true;
private boolean zoomEnabled = true;
public PanZoomView(Context context) {
super(context);
setup();
}
public PanZoomView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setup();
}
public PanZoomView(Context context, AttributeSet attrs) {
super(context, attrs);
setup();
}
private void setup() {
mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
}
public void setBitmap(Bitmap bmp) {
setImageBitmap(bmp);
}
public void setImageBitmap(Bitmap bmp) {
bitmap = bmp;
resetZoom();
resetPan();
firstDraw = true;
invalidate();
}
public Bitmap getImageBitmap() {
return bitmap;
}
public Bitmap getBitmap() {
return getImageBitmap();
}
public void resetZoom() {
mScaleFactor = 1.0f;
}
public void resetPan() {
mPosX = 0f;
mPosY = 0f;
}
public void setImageDrawable(Drawable drawable) {
setImageBitmap(((BitmapDrawable) drawable).getBitmap());
}
public BitmapDrawable getImageDrawable() {
BitmapDrawable bd = new BitmapDrawable(getContext().getResources(), bitmap);
return bd;
}
public BitmapDrawable getDrawable() {
return getImageDrawable();
}
public void onDraw(Canvas canvas) {
// Log.v(TAG, "onDraw()");
if (bitmap == null) {
Log.w(TAG, "nothing to draw - bitmap is null");
super.onDraw(canvas);
return;
}
if (firstDraw
&& (bitmap.getHeight() > 0)
&& (bitmap.getWidth() > 0)) {
//Don't let the user zoom out so much that the image is smaller
//than its containing frame
float minXScaleFactor = (float) viewWidth / (float) bitmap.getWidth();
float minYScaleFactor = (float) viewHeight / (float) bitmap.getHeight();
minScaleFactor = Math.max(minXScaleFactor, minYScaleFactor);
Log.d(TAG, "minScaleFactor: " + minScaleFactor);
mScaleFactor = minScaleFactor; //start out "zoomed out" all the way
mPosX = mPosY = 0;
firstDraw = false;
}
mScaleFactor = Math.max(mScaleFactor, minScaleFactor);
canvasHeight = canvas.getHeight();
canvasWidth = canvas.getWidth();
// Log.d(TAG, "canvas density: " + canvas.getDensity() + " bitmap density: " + bitmap.getDensity());
// Log.d(TAG, "mScaleFactor: " + mScaleFactor);
//Save the canvas without translating (panning) or scaling (zooming)
//After each change, restore to this state, instead of compounding
//changes upon changes
canvas.save();
int maxX, minX, maxY, minY;
//Regardless of the screen density (HDPI, MDPI) or the scale factor,
//The image always consists of bitmap width divided by 2 pixels. If an image
//is 200 pixels wide and you scroll right 100 pixels, you just scrolled the image
//off the screen to the left.
minX = (int) (((viewWidth / mScaleFactor) - bitmap.getWidth()) / 2);
maxX = 0;
//How far can we move the image vertically without having a gap between image and frame?
minY = (int) (((viewHeight / mScaleFactor) - bitmap.getHeight()) / 2);
maxY = 0;
Log.d(TAG, "minX: " + minX + " maxX: " + maxX + " minY: " + minY + " maxY: " + maxY);
//Do not go beyond the boundaries of the image
if (mPosX > maxX) {
mPosX = maxX;
}
if (mPosX < minX) {
mPosX = minX;
}
if (mPosY > maxY) {
mPosY = maxY;
}
if (mPosY < minY) {
mPosY = minY;
}
// Log.d(TAG, "view width: " + viewWidth + " view height: "
// + viewHeight);
// Log.d(TAG, "bitmap width: " + bitmap.getWidth() + " height: " + bitmap.getHeight());
// Log.d(TAG, "translating mPosX: " + mPosX + " mPosY: " + mPosY);
// Log.d(TAG, "zooming to scale factor of " + mScaleFactor);
canvas.scale(mScaleFactor, mScaleFactor);
// Log.d(TAG, "panning to " + mPosX + "," + mPosY);
canvas.translate(mPosX, mPosY);
super.onDraw(canvas);
canvas.drawBitmap(bitmap, mPosX, mPosY, null);
canvas.restore(); //clear translation/scaling
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
// Let the ScaleGestureDetector inspect all events.
if (zoomEnabled) {
mScaleDetector.onTouchEvent(ev);
}
if (panEnabled) {
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: {
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()) {
float dx = x - mLastTouchX;
float dy = y - mLastTouchY;
//Adjust for zoom factor. Otherwise, the user's finger moving 10 pixels
//at 200% zoom causes the image to slide 20 pixels instead of perfectly
//following the user's touch
dx /= (mScaleFactor * 2);
dy /= (mScaleFactor * 2);
mPosX += dx;
mPosY += dy;
Log.v(TAG, "moving by " + dx + "," + dy + " mScaleFactor: " + mScaleFactor);
invalidate();
}
mLastTouchX = x;
mLastTouchY = y;
break;
}
case MotionEvent.ACTION_UP: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_CANCEL: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
break;
}
}
}
return true;
}
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(0.1f, Math.min(mScaleFactor, 5.0f));
// Log.d(TAG, "detector scale factor: " + detector.getScaleFactor() + " mscalefactor: " + mScaleFactor);
invalidate();
return true;
}
}
//Currently zoomEnabled/panEnabled can only be set programmatically, not in XML
public boolean isPanEnabled() {
return panEnabled;
}
public void setPanEnabled(boolean panEnabled) {
this.panEnabled = panEnabled;
}
public boolean isZoomEnabled() {
return zoomEnabled;
}
public void setZoomEnabled(boolean zoomEnabled) {
this.zoomEnabled = zoomEnabled;
}
/**
* Calls getCroppedBitmap(int outputWidth, int outputHeight) without
* scaling the resulting bitmap to any specific size.
* #return
*/
public Bitmap getCroppedBitmap() {
return getCroppedBitmap(0, 0);
}
/**
* Takes the section of the bitmap visible in its View object
* and exports that to a Bitmap object, taking into account both
* the translation (panning) and zoom (scaling).
* WARNING: run this in a separate thread, not on the UI thread!
* If you specify that a 200x200 image should have an outputWidth
* of 400 and an outputHeight of 50, the image will be squished
* and stretched to those dimensions.
* #param outputWidth desired width of output Bitmap in pixels
* #param outputHeight desired height of output Bitmap in pixels
* #return the visible portion of the image in the PanZoomImageView
*/
public Bitmap getCroppedBitmap(int outputWidth, int outputHeight) {
int origX = -1 * (int) mPosX * 2;
int origY = -1 * (int) mPosY * 2;
int width = (int) (viewWidth / mScaleFactor);
int height = (int) (viewHeight / mScaleFactor);
Log.e(TAG, "origX: " + origX + " origY: " + origY + " width: " + width + " height: " + height + " outputWidth: " + outputWidth + " outputHeight: " + outputHeight + "getLayoutParams().width: " + getLayoutParams().width + " getLayoutParams().height: " + getLayoutParams().height);
Bitmap b = Bitmap.createBitmap(bitmap, origX, origY, width, height);
if (outputWidth > 0 && outputWidth > 0) {
//Use the exact dimensions given--chance this won't match the aspect ratio
b = Bitmap.createScaledBitmap(b, outputWidth, outputHeight, true);
}
return b;
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewHeight = h;
viewWidth = w;
}
}
mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));
This way you can scale up to 5 times, calculate the maximum scale based on the size you want and add to this instead of using 5 all the time.
Also when I worked with pinch zoom on a project I find it easier to use if you use absolute values instead of multiplying. Just get the distance of the fingers on the first touch and when moving the fingers, calculate the distance and then the scale based on the first distance. This way it follows the fingers better and works better when you limit the minimum and maximum scale.
This post is quite old and already answered, however I found another solution which worked even better for me then the accepted answer.
protected override void OnDraw(Canvas canvas)
{
// Calculate the boundaries of the canvas
var minX = (int)((_viewWidth / _scaleFactor) - canvas.Width);
var minY = (int)((_viewHeight / _scaleFactor) - canvas.Height);
if (_posX > 0)
_posX = 0;
else if (_posX < minX)
_posX = minX;
if (_posY > 0)
_posY = 0;
else if (_posY < minY)
_posY = minY;
// Change image position
canvas.Scale(_scaleFactor, _scaleFactor);
canvas.Translate(_posX, _posY);
base.OnDraw(canvas);
}
protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
{
base.OnSizeChanged(w, h, oldw, oldh);
_viewHeight = h;
_viewWidth = w;
}
Note that this code is written in Xamarin.Android, however converting this to Java will be easy.
This keeps the image perfectly within it's boundaries.
Related
I'm using custom TextureView which has zooming feature(for android player). The problem is I can not do accurate zooming. when you try zooming in first time it seems work without problem. After some zooming view jumps to another position. I will put custom textureView code here:
public class PanZoomTextureView extends TextureView {
protected Context mContext;
protected float mPosX;
protected float mPosY;
protected float mFocusX; // these two focus variables are not needed
protected float mFocusY;
protected float mLastTouchX;
protected float mLastTouchY;
protected static final int INVALID_POINTER_ID = -1;
// The ‘active pointer’ is the one currently moving our object.
protected int mActivePointerId = INVALID_POINTER_ID;
protected ScaleGestureDetector mScaleDetector;
protected float mScaleFactor = 1.f;
// The next three are set by calling supportsPan, supportsZoom, ...
protected boolean mSupportsPan = true;
protected boolean mSupportsZoom = true;
private MediaController controller;
/**
*/
public PanZoomTextureView(Context context) {
this(context, null, 0);
}
public PanZoomTextureView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PanZoomTextureView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mContext = context;
setupScaleDetector(context);
}
private void invalidateDraw() {
Matrix ma = new Matrix();
float x = 0, y = 0;
x = mPosX;
y = mPosY;
if (mSupportsZoom || mSupportsPan) {
if (mSupportsPan && mSupportsZoom) {
if (mScaleDetector.isInProgress()) {
ma.setScale(mScaleFactor, mScaleFactor, mFocusX, mFocusY);
mPosX = (getWidth() - getWidth() * mScaleFactor) * (mFocusX / getWidth()) / 2;
mPosY = (getHeight() - getHeight() * mScaleFactor) * (mFocusY / getHeight()) / 2;
Log.d("Multitouch", "'in progress' mposx, mposy, focusX, focusY: " + mPosX + " " + mPosY + " " + mFocusX + " " + mFocusY);
} else {
// Pinch zoom is not in progress. Just do translation
// of canvas at whatever the current scale is.
ma.setTranslate(x, y);
ma.postScale(mScaleFactor, mScaleFactor);
Log.d("Multitouch", "x, y , focusX, focusY , scalefactor : " + x + " " + y + " " + mFocusX + " " + mFocusY + " " + mScaleFactor);
}
}
}
setTransform(ma);
}
public void setMediaController(MediaController c) {
this.controller = c;
}
/**
* Handle touch and multitouch events so panning and zooming can be supported.
*/
#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);
Log.d("OnTouchDown:", "lastX, lastY: " + mLastTouchX + " , " + mLastTouchY);
if (ev.getPointerCount() == 1) {
if (controller.isShowing()) {
controller.hide();
} else {
controller.show(10000);
}
}
break;
}
case MotionEvent.ACTION_MOVE: {
//register active pointer index
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
// x and y values from active pointer
final float x = ev.getX(pointerIndex);
final float y = ev.getY(pointerIndex);
final float dx = (x - mLastTouchX);
final float dy = (y - mLastTouchY);
// Only move if the view supports panning and
// ScaleGestureDetector isn't processing a gesture.
if (!mScaleDetector.isInProgress()) {
Log.d("OnTouch1:", "dX, dY: " + dx + " , " + dy);
if (mPosX > 5) {
mPosX = 3;
}
if (mPosY > 5) {
mPosY = 3;
}
if (mPosX < ((-1) * (getWidth() - getWidth() / mScaleFactor) - 5)) {
mPosX = ((-1) * (getWidth() - getWidth() / mScaleFactor) - 2);
}
if (mPosY < ((-1) * (getHeight() - getHeight() / mScaleFactor) - 5)) {
mPosY = ((-1) * (getHeight() - getHeight() / mScaleFactor) - 2);
}
if ((mPosX + dx) > 5 && (mPosY + dy) > 5) {
mPosX = 3;
mPosY = 3;
Log.d("OnTouch1:", "mPosx, mPosy: " + mPosX + " , " + mPosY);
} else if ((mPosY + dy) > 5 && (mPosX + dx) < 5 && (mPosX + dx) > ((-1) * (getWidth() - getWidth() / mScaleFactor) - 5)) {
mPosY = 3;
mPosX += dx;
Log.d("OnTouch2:", "mPosx, mPosy: " + mPosX + " , " + mPosY);
} else {
mPosX += dx;
mPosY += dy;
Log.d("OnTouch8:", "mPosx, mPosy: " + mPosX + " , " + mPosY);
}
invalidateDraw();
}
mLastTouchX = x;
mLastTouchY = y;
break;
}
case MotionEvent.ACTION_UP: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_CANCEL: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
>> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
break;
}
}
return true;
}
/**
* This method sets up the scale detector object used by the view. It is called by the constructor.
*
* #return void
*/
protected void setupScaleDetector(Context context) {
// Create our ScaleGestureDetector
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
}
/**
* ScaleListener
*/
protected class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
mScaleFactor *= detector.getScaleFactor();
Log.e("Multitouch", "-------scaling factor:" + mScaleFactor);
// Don't let the object get too small or too large.
mScaleFactor = Math.max(1.f, Math.min(mScaleFactor, 4));
mFocusX = detector.getFocusX();
mFocusY = detector.getFocusY();
invalidateDraw();
return true;
}
}
}
Also if someone wants to test real project here is the link: https://github.com/Shuhrat-java/PanZoomPlayer
I want to calculate the un-scaled absolute position of the image drawn on the canvas based on the position clicked by the user on the scaled canvas.
I used following zoom implementation
Translate/scale bitmap within boundaries?
So far,
public boolean inMe(int x, int y, Region ClickRegion) {
if(mScaleFactor == 0)
mScaleFactor = 1;
float curX = ((x*1.0f)/ mScaleFactor) - (mPosX * mScaleFactor);
float curY = ((y*1.0f) / mScaleFactor) - (mPosY * mScaleFactor);
x = (int)curX;
y = (int)curY;
//ClickRegion is a grapics.Region computed on non-zoomed coordinates
if (ClickRegion.contains(x, y))
return true;
else
return false;
}
This works fine, when there is no zooming, but when its zoomed there are significant issues.
EDIT
This is the algo I used for zooming and panning.
public class PanZoomView extends View {
public static final String TAG = PanZoomView.class.getName();
private static final int INVALID_POINTER_ID = -1;
// The ‘active pointer’ is the one currently moving our object.
private int mActivePointerId = INVALID_POINTER_ID;
private Bitmap bitmap;
private float viewHeight;
private float viewWidth;
float canvasWidth, canvasHeight;
private ScaleGestureDetector mScaleDetector;
private float mScaleFactor = 1.f;
private float minScaleFactor;
private float mPosX;
private float mPosY;
private float mLastTouchX, mLastTouchY;
private boolean firstDraw = true;
private boolean panEnabled = true;
private boolean zoomEnabled = true;
public PanZoomView(Context context) {
super(context);
setup();
}
public PanZoomView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setup();
}
public PanZoomView(Context context, AttributeSet attrs) {
super(context, attrs);
setup();
}
private void setup() {
mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
}
public void setBitmap(Bitmap bmp) {
setImageBitmap(bmp);
}
public void setImageBitmap(Bitmap bmp) {
bitmap = bmp;
resetZoom();
resetPan();
firstDraw = true;
invalidate();
}
public Bitmap getImageBitmap() {
return bitmap;
}
public Bitmap getBitmap() {
return getImageBitmap();
}
public void resetZoom() {
mScaleFactor = 1.0f;
}
public void resetPan() {
mPosX = 0f;
mPosY = 0f;
}
public void setImageDrawable(Drawable drawable) {
setImageBitmap(((BitmapDrawable) drawable).getBitmap());
}
public BitmapDrawable getImageDrawable() {
BitmapDrawable bd = new BitmapDrawable(getContext().getResources(), bitmap);
return bd;
}
public BitmapDrawable getDrawable() {
return getImageDrawable();
}
public void onDraw(Canvas canvas) {
// Log.v(TAG, "onDraw()");
if (bitmap == null) {
Log.w(TAG, "nothing to draw - bitmap is null");
super.onDraw(canvas);
return;
}
if (firstDraw
&& (bitmap.getHeight() > 0)
&& (bitmap.getWidth() > 0)) {
//Don't let the user zoom out so much that the image is smaller
//than its containing frame
float minXScaleFactor = (float) viewWidth / (float) bitmap.getWidth();
float minYScaleFactor = (float) viewHeight / (float) bitmap.getHeight();
minScaleFactor = Math.max(minXScaleFactor, minYScaleFactor);
Log.d(TAG, "minScaleFactor: " + minScaleFactor);
mScaleFactor = minScaleFactor; //start out "zoomed out" all the way
mPosX = mPosY = 0;
firstDraw = false;
}
mScaleFactor = Math.max(mScaleFactor, minScaleFactor);
canvasHeight = canvas.getHeight();
canvasWidth = canvas.getWidth();
// Log.d(TAG, "canvas density: " + canvas.getDensity() + " bitmap density: " + bitmap.getDensity());
// Log.d(TAG, "mScaleFactor: " + mScaleFactor);
//Save the canvas without translating (panning) or scaling (zooming)
//After each change, restore to this state, instead of compounding
//changes upon changes
canvas.save();
int maxX, minX, maxY, minY;
//Regardless of the screen density (HDPI, MDPI) or the scale factor,
//The image always consists of bitmap width divided by 2 pixels. If an image
//is 200 pixels wide and you scroll right 100 pixels, you just scrolled the image
//off the screen to the left.
minX = (int) (((viewWidth / mScaleFactor) - bitmap.getWidth()) / 2);
maxX = 0;
//How far can we move the image vertically without having a gap between image and frame?
minY = (int) (((viewHeight / mScaleFactor) - bitmap.getHeight()) / 2);
maxY = 0;
Log.d(TAG, "minX: " + minX + " maxX: " + maxX + " minY: " + minY + " maxY: " + maxY);
//Do not go beyond the boundaries of the image
if (mPosX > maxX) {
mPosX = maxX;
}
if (mPosX < minX) {
mPosX = minX;
}
if (mPosY > maxY) {
mPosY = maxY;
}
if (mPosY < minY) {
mPosY = minY;
}
// Log.d(TAG, "view width: " + viewWidth + " view height: "
// + viewHeight);
// Log.d(TAG, "bitmap width: " + bitmap.getWidth() + " height: " + bitmap.getHeight());
// Log.d(TAG, "translating mPosX: " + mPosX + " mPosY: " + mPosY);
// Log.d(TAG, "zooming to scale factor of " + mScaleFactor);
canvas.scale(mScaleFactor, mScaleFactor);
// Log.d(TAG, "panning to " + mPosX + "," + mPosY);
canvas.translate(mPosX, mPosY);
super.onDraw(canvas);
canvas.drawBitmap(bitmap, mPosX, mPosY, null);
canvas.restore(); //clear translation/scaling
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
// Let the ScaleGestureDetector inspect all events.
if (zoomEnabled) {
mScaleDetector.onTouchEvent(ev);
}
if (panEnabled) {
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: {
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()) {
float dx = x - mLastTouchX;
float dy = y - mLastTouchY;
//Adjust for zoom factor. Otherwise, the user's finger moving 10 pixels
//at 200% zoom causes the image to slide 20 pixels instead of perfectly
//following the user's touch
dx /= (mScaleFactor * 2);
dy /= (mScaleFactor * 2);
mPosX += dx;
mPosY += dy;
Log.v(TAG, "moving by " + dx + "," + dy + " mScaleFactor: " + mScaleFactor);
invalidate();
}
mLastTouchX = x;
mLastTouchY = y;
break;
}
case MotionEvent.ACTION_UP: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_CANCEL: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
break;
}
}
}
return true;
}
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(0.1f, Math.min(mScaleFactor, 5.0f));
// Log.d(TAG, "detector scale factor: " + detector.getScaleFactor() + " mscalefactor: " + mScaleFactor);
invalidate();
return true;
}
}
//Currently zoomEnabled/panEnabled can only be set programmatically, not in XML
public boolean isPanEnabled() {
return panEnabled;
}
public void setPanEnabled(boolean panEnabled) {
this.panEnabled = panEnabled;
}
public boolean isZoomEnabled() {
return zoomEnabled;
}
public void setZoomEnabled(boolean zoomEnabled) {
this.zoomEnabled = zoomEnabled;
}
/**
* Calls getCroppedBitmap(int outputWidth, int outputHeight) without
* scaling the resulting bitmap to any specific size.
* #return
*/
public Bitmap getCroppedBitmap() {
return getCroppedBitmap(0, 0);
}
/**
* Takes the section of the bitmap visible in its View object
* and exports that to a Bitmap object, taking into account both
* the translation (panning) and zoom (scaling).
* WARNING: run this in a separate thread, not on the UI thread!
* If you specify that a 200x200 image should have an outputWidth
* of 400 and an outputHeight of 50, the image will be squished
* and stretched to those dimensions.
* #param outputWidth desired width of output Bitmap in pixels
* #param outputHeight desired height of output Bitmap in pixels
* #return the visible portion of the image in the PanZoomImageView
*/
public Bitmap getCroppedBitmap(int outputWidth, int outputHeight) {
int origX = -1 * (int) mPosX * 2;
int origY = -1 * (int) mPosY * 2;
int width = (int) (viewWidth / mScaleFactor);
int height = (int) (viewHeight / mScaleFactor);
Log.e(TAG, "origX: " + origX + " origY: " + origY + " width: " + width + " height: " + height + " outputWidth: " + outputWidth + " outputHeight: " + outputHeight + "getLayoutParams().width: " + getLayoutParams().width + " getLayoutParams().height: " + getLayoutParams().height);
Bitmap b = Bitmap.createBitmap(bitmap, origX, origY, width, height);
if (outputWidth > 0 && outputWidth > 0) {
//Use the exact dimensions given--chance this won't match the aspect ratio
b = Bitmap.createScaledBitmap(b, outputWidth, outputHeight, true);
}
return b;
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewHeight = h;
viewWidth = w;
}
}
Thanks for all comment, but this worked for me.
public boolean inMe(int x, int y, Region ClickRegion) {
float curX = ((x*1.0f)) + (-1*mPosX * 2* mScaleFactor);
float curY = ((y*1.0f)) + (-1*mPosY * 2* mScaleFactor);
x = (int)((curX/ mScaleFactor));
y = (int)((curY/ mScaleFactor));
if (cluster.ClickRegion.contains(x, y))
return true;
else
return false;
}
This image is my question. Could you help me?
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.FloatMath;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ImageView;
import android.widget.Toast;
import android.widget.ImageView.ScaleType;
public class ImageViewTouch extends ImageView{
private static final String TAG = "ImageViewTouch";
private static final boolean D = false;
private Matrix matrix;
private Matrix savedMatrix;
private Matrix savedMatrix2;
private Drawable d;
private static final int NONE = 0;
private static final int DRAG = 1;
private static final int ZOOM = 2;
private int mode = NONE;
private PointF start = new PointF();
private PointF mid = new PointF();
private float oldDist = 1f;
private static final int WIDTH = 0;
private static final int HEIGHT = 1;
private boolean isInit = true;
private boolean isMoving;
private boolean isScaling;
private boolean isRestoring;
public ImageViewTouch(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
setScaleType(ScaleType.MATRIX);
matrix = new Matrix();
savedMatrix = new Matrix();
savedMatrix2 = new Matrix();
}
public ImageViewTouch(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ImageViewTouch(Context context) {
this(context, null);
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom)
{
if (D) Log.i(TAG, "onLayout");
d = this.getDrawable();
super.onLayout(changed, left, top, right, bottom);
if (isInit == false)
{
init();
isInit = true;
}
}
#Override
public void setImageBitmap(Bitmap bm)
{
if (D) Log.i(TAG, "setImageBitmap");
super.setImageBitmap(bm);
if(isInit == true)
{
init();
}
}
#Override
public void setImageResource(int resId) {
if (D) Log.i(TAG, "setImageResource");
super.setImageResource(resId);
d = getDrawable();
isInit = false;
init();
}
protected void init()
{
d = this.getDrawable();
initImgPos();
setImageMatrix(matrix);
isInit = false;
}
public void initImgPos()
{
float[] value = new float[9];
this.matrix.getValues(value);
int width = this.getWidth();
int height = this.getHeight();
if (d == null) return;
int imageWidth = d.getIntrinsicWidth();
int imageHeight = d.getIntrinsicHeight();
int scaleWidth = (int) (imageWidth * value[0]);
int scaleHeight = (int) (imageHeight * value[4]);
if (imageWidth > width || imageHeight > height)
{
float xratio = (float)width / (float)imageWidth;
float yratio = (float)height / (float)imageHeight;
// Math.min fits the image to the shorter axis. (with letterboxes around)
// Math.max fits the image th the longer axis. (with the other axis cropped)
value[0] = value[4] = Math.max(xratio, yratio);
}
scaleWidth = (int) (imageWidth * value[0]);
scaleHeight = (int) (imageHeight * value[4]);
// align the image to the top left corner
value[2] = 0;
value[5] = 0;
// center the image. it will be aligned to the top left corner otherwise.
value[2] = (float) width / 2 - (float)scaleWidth / 2;
value[5] = (float) height / 2 - (float)scaleHeight / 2;
matrix.setValues(value);
setImageMatrix(matrix);
}
#Override
public boolean onTouchEvent(MotionEvent event)
{
if(D) dumpEvent(event);
switch (event.getAction() & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN:
savedMatrix.set(matrix);
start.set(event.getX(), event.getY());
mode = DRAG;
break;
case MotionEvent.ACTION_POINTER_DOWN:
oldDist = spacing(event);
if (oldDist > 10f)
{
savedMatrix.set(matrix);
midPoint(mid, event);
mode = ZOOM;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
restore(matrix);
break;
case MotionEvent.ACTION_MOVE:
if (mode == DRAG)
{
matrix.set(savedMatrix);
matrix.postTranslate(event.getX() - start.x, event.getY() - start.y);
}
else if (mode == ZOOM)
{
float newDist = spacing(event);
if (newDist > 10f)
{
matrix.set(savedMatrix);
float scale = newDist / oldDist;
matrix.postScale(scale, scale, mid.x, mid.y);
}
}
break;
}
// Matrix value modification
// comment out below 2 lines to remove all restrictions on image transformation.
matrixTuning(matrix);
setImageMatrix(savedMatrix2);
return true;
}
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);
}
private void midPoint(PointF point, MotionEvent event)
{
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
point.set(x / 2, y / 2);
}
private void matrixTuning(Matrix matrix)
{
float[] value = new float[9];
matrix.getValues(value);
float[] savedValue = new float[9];
savedMatrix2.getValues(savedValue);
int width = getWidth();
int height = getHeight();
Drawable d = getDrawable();
if (d == null) return;
int imageWidth = d.getIntrinsicWidth();
int imageHeight = d.getIntrinsicHeight();
int scaleWidth = (int) (imageWidth * value[0]);
int scaleHeight = (int) (imageHeight * value[4]);
// don't let the image go outside
if (value[2] < width - scaleWidth) value[2] = width - scaleWidth;
if (value[5] < height - scaleHeight) value[5] = height - scaleHeight;
if (value[2] > 0) value[2] = 0;
if (value[5] > 0) value[5] = 0;
// maximum zoom ratio: 2x
if (value[0] > 2 || value[4] > 2)
{
value[0] = savedValue[0];
value[4] = savedValue[4];
value[2] = savedValue[2];
value[5] = savedValue[5];
}
// don't let the image become smaller than the screen
if (imageWidth > width || imageHeight > height)
{
if (scaleWidth < width && scaleHeight < height)
{
int target = WIDTH;
if (imageWidth < imageHeight) target = HEIGHT;
if (target == WIDTH) value[0] = value[4] = (float)width / imageWidth;
if (target == HEIGHT) value[0] = value[4] = (float)height / imageHeight;
scaleWidth = (int) (imageWidth * value[0]);
scaleHeight = (int) (imageHeight * value[4]);
if (scaleWidth > width) value[0] = value[4] = (float)width / imageWidth;
if (scaleHeight > height) value[0] = value[4] = (float)height / imageHeight;
}
}
// don't allow scale down under its size
else
{
if (value[0] < 1) value[0] = 1;
if (value[4] < 1) value[4] = 1;
}
// center the image
scaleWidth = (int) (imageWidth * value[0]);
scaleHeight = (int) (imageHeight * value[4]);
if (scaleWidth < width)
{
value[2] = (float) width / 2 - (float)scaleWidth / 2;
}
if (scaleHeight < height)
{
value[5] = (float) height / 2 - (float)scaleHeight / 2;
}
matrix.setValues(value);
savedMatrix2.set(matrix);
}
private void restore(Matrix m)
{
setImageMatrix(matrix);
}
private void dumpEvent(MotionEvent event)
{
String names[] = { "DOWN" , "UP" , "MOVE" , "CANCEL" , "OUTSIDE" , "POINTER_DOWN" , "POINTER_UP" , "7?" , "8?" , "9?" };
StringBuilder sb = new StringBuilder();
int action = event.getAction();
int actionCode = action & MotionEvent.ACTION_MASK;
sb.append("event ACTION_" ).append(names[actionCode]);
if (actionCode == MotionEvent.ACTION_POINTER_DOWN || actionCode == MotionEvent.ACTION_POINTER_UP)
{
sb.append("(pid " ).append(action >> MotionEvent.ACTION_POINTER_ID_SHIFT);
sb.append(")" );
}
sb.append("[" );
for (int i = 0; i < event.getPointerCount(); i++)
{
sb.append("#" ).append(i);
sb.append("(pid " ).append(event.getPointerId(i));
sb.append(")=" ).append((int) event.getX(i));
sb.append("," ).append((int) event.getY(i));
if (i + 1 < event.getPointerCount())
sb.append(";" );
}
sb.append("]" );
Log.d(TAG, sb.toString());
}
}
Please give me solution of this question. it operaters this code well for pinch to zoom. but, I cant know point after pinch to zoom. I wnat to know image of absolute point.
Am trying out an Android App, where am marking the points where the user does a long press.
For a touch-sensitive image view, I used the most of the code from here:
http://android-developers.blogspot.in/2010/06/making-sense-of-multitouch.html
http://developer.android.com/training/gestures/scale.html
along with some suggestions from various posts in stackoverflow.
This is the code for the touch image view :
public class SimpleImageView extends ImageView implements OnTouchListener,OnGestureListener {
public HashMap<meeCoordinates, Integer> plotPointsMap = new HashMap<meeCoordinates, Integer>();
private float mPosX = 0f;
private float mPosY = 0f;
private boolean didLongPress = false;
private float mLastTouchX;
private float mLastTouchY;
private float magicX;
private float magicY;
private static final int INVALID_POINTER_ID = -1;
Context context;
Canvas canvas;
private GestureDetector gestureScanner;
int deviceWidth, deviceHeight;
Matrix matrix = new Matrix();
Matrix savedMatrix = new Matrix();
static final int NONE = 0;
static final int DRAG = 1;
static final int ZOOM = 2;
int mode = NONE;
PointF start = new PointF();
PointF mid = new PointF();
float oldDist = 1f;
String savedItemClicked;
private int mActivePointerId = INVALID_POINTER_ID;
public SimpleTouchImageView(Context ctx) {
// The ‘active pointer’ is the one currently moving our object.
this(ctx, null, 0);
this.context = ctx;
}
public SimpleTouchImageView(Context ctx, AttributeSet attrs) {
this(ctx, attrs, 0);
this.context = ctx;
}
private ScaleGestureDetector mScaleDetector;
private float mScaleFactor = 1.f;
public SimpleTouchImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// Create our ScaleGestureDetector
if (!this.isInEditMode()) {
mScaleDetector = new ScaleGestureDetector(context,new ScaleListener());
}
gestureScanner = new GestureDetector(context, this);
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
if (mScaleDetector != null) {
mScaleDetector.onTouchEvent(ev);
}
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
Log.d(TAG, "onTouchEvent MotionEvent.ACTION_DOWN");
final float x = ev.getX();
final float y = ev.getY();
// Set them to the X, Y at the beginning of the touch event
mLastTouchX = x;
mLastTouchY = y;
mActivePointerId = ev.getPointerId(0);
// _handler.postDelayed(_longPressed, LONG_PRESS_TIME);
break;
}
case MotionEvent.ACTION_MOVE: {
// _handler.removeCallbacks(_longPressed);
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(pointerIndex);
final float y = ev.getY(pointerIndex);
if (mScaleDetector != null) {
if (!mScaleDetector.isInProgress()) {
// Calculate the distance moved
final float dx = x - mLastTouchX;
final float dy = y - mLastTouchY;
// Move the object
mPosX += dx;
mPosY += dy;
// Remember this touch position for the next move event
mLastTouchX = x;
mLastTouchY = y;
invalidate();
}
}
mLastTouchX = x;
mLastTouchY = y;
break;
}
case MotionEvent.ACTION_UP: {
// _handler.removeCallbacks(_longPressed);
mActivePointerId = INVALID_POINTER_ID;
// Calculate the Actual X & Y
Drawable drawable = this.getDrawable();
Rect imageBounds = drawable.getBounds();
int intrinsicHeight = this.getDrawable().getIntrinsicHeight();
int intrinsicWidth = this.getDrawable().getIntrinsicWidth();
int imageOffsetX = (int) (ev.getX() - imageBounds.left);
int imageOffsetY = (int) (ev.getY() - imageBounds.top);
float[] f = newfloat[9];
this.getImageMatrix().getValues(f);
final float scaleX = f[Matrix.MSCALE_X];
final float scaleY = f[Matrix.MSCALE_Y];
final int actualWd = Math.round(intrinsicWidth * mScaleFactor);
final int actualHt = Math.round(intrinsicHeight * mScaleFactor);
// CALCULATE THE X,Y COORDINATES CORRESPONDING TO THE POINT OF TOUCH
// IN THE ACTUAL IMAGE,
magicX = ((ev.getX() + (-1 * mPosX)) / mScaleFactor);
magicY = ((ev.getY() + (-1 * mPosY)) / mScaleFactor);
mLastTouchX = ev.getX();
mLastTouchY = ev.getY();
if (didLongPress == true) {
// STORE THE Point where user did long press IN AN ARRAY
plotPointsMap.put ( coordinates, marker );
invalidate();
didLongPress = false;
}
break;
}
case MotionEvent.ACTION_CANCEL: {
// _handler.removeCallbacks(_longPressed);
Log.d(TAG, "onTouchEvent MotionEvent.ACTION_CANCEL");
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
// _handler.removeCallbacks(_longPressed);
Log.d(TAG, "onTouchEvent MotionEvent.ACTION_POINTER_UP");
break;
}
}
// return true;
return gestureScanner.onTouchEvent(ev);
}
/** Determine the space between the first two fingers */
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);
}
/** Calculate the mid point of the first two fingers */
private void midPoint(PointF point, MotionEvent event) {
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
point.set(x / 2, y / 2);
}
#Override
public void draw(Canvas canvas) {
super.draw(canvas);
}
#Override
public void onDraw(Canvas canvas) {
WindowManager wm = (WindowManager) this.context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(metrics);
deviceWidth = metrics.widthPixels;
deviceHeight = metrics.heightPixels;
canvas.drawRect(0, 0, getWidth(), getHeight(), backgroundPaint);
if (this.getDrawable() != null) {
canvas.save();
canvas.translate(mPosX, mPosY);
Matrix matrix = new Matrix();
matrix.postScale(mScaleFactor, mScaleFactor, pivotPointX,pivotPointY);
canvas.drawBitmap(((BitmapDrawable) this.getDrawable()).getBitmap(), matrix,null);
// ---add the marker---
if ((plotPointsMap != null) && (plotPointsMap.size() > 0)) {
for (int index = 0; index < plotPointsMap.size(); index++) {
Set<MyCoordinates> setCoordinates = plotPointsMap.keySet();
if ((setCoordinates != null) && (setCoordinates.size() > 0)) {
Iterator<MyCoordinates> setIterator =setCoordinates.iterator();
if (setIterator != null) {
while (setIterator.hasNext()) {
MyCoordinates coordinates = setIterator.next();
int resource = plotPointsMap.get(coordinates).intValue();
int resourceId = R.drawable.location_marker;
Bitmap marker = BitmapFactory.decodeResource(getResources(), resourceId);
float xCoordinate = coordinates.getCoordinateX();
float yCoordinate = coordinates.getCoordinateY();
canvas.drawBitmap(marker, xCoordinate * mScaleFactor, yCoordinate * mScaleFactor, null);
}
}
}
}
}
canvas.restore();
}
}
#Override
public void setImageDrawable(Drawable drawable) {
// Constrain to given size but keep aspect ratio
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
mLastTouchX = mPosX = 0;
mLastTouchY = mPosY = 0;
mScaleFactor = Math.min((float) getLayoutParams().width/ width, (float) getLayoutParams().height/ height);
pivotPointX = ((float) getLayoutParams().width - (int) (width * mScaleFactor)) / 2;
pivotPointY = ((float) getLayoutParams().height - (int) (height * mScaleFactor)) / 2;
super.setImageDrawable(drawable);
}
float pivotPointX = 0f;
float pivotPointY = 0f;
...
...
}
The Issue
Let's keep aside scaling for now. Say, the image is loaded in its actual size. The point where the user touches is recorded correctly. Say, if the user touches at (120, 50) with x=120 and y=50, then it detects correctly as (120, 50). Am storing the points in an array and re-drawing a marker at each of the points in
public void onDraw(Canvas canvas)
The problem is that when the marker is drawn at the point (am printing it within onDraw(canvas)), the marker is drawn about 30px away from the actual point. i.e. if actual (x,y) is (120,50), then the marker image is drawn at about (150,80). Always there is this 30px difference. Why is this? Am breaking my head on this since the last two weeks in vain. Can someone please help?
here is an image showing the touch point in blue (over the letter 'S' in 'U.S.'), and you can see that the black marker is being drawn off the touch point :
EDIT
When I pan the image, inverse.mapPoints() doesn't account for the offset of the image away from the screen.
For example, here is the image when the App begins, this is the image loaded
and when the user pans, the image might go to the left of the screen-edge as below:
In such a case, inverse.mapPoints() only returns the value from the edge of the screen, but I want the value from the edge of the original image. How do I do it?
I tried googl'ing and tried few things given in stackoverflow (like getTop(), and getLocationOnscreen()) but in vain.
can you help?
1 use getImageMatrix()
2 invert it - invert()
3 use inverted.mapPoints()
I'm developing an application in which I'm pasting images, doing drawing and painting on canvas. This app can also Scale up/down the canvas or drag it to different location.
My problem is: I can't get the correct canvas coordinates after scaling or dragging the canvas. I want to draw finger paint after the canvas is scaled or dragged but unable to retrieve the right place where i've touched..:(
Also I'm new bee. Here is the code.
#Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
//canvas.translate(mPosX, mPosY);
canvas.scale(mScaleFactor, mScaleFactor, super.getWidth() * 0.5f,
super.getHeight() * 0.5f);
mIcon.draw(canvas);
for (Path path : listPath) {
canvas.drawPath(path, paint);
}
canvas.restore();
}
public TouchExampleView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#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: {
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;
invalidate();
}
mLastTouchX = x;
mLastTouchY = y;
break;
}
case MotionEvent.ACTION_UP: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_CANCEL: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
break;
}
}
float objectNewX,objectNewY;
if (mScaleFactor >= 1) {
objectNewX = ev.getX() + (ev.getX() - super.getWidth() * 0.5f) * (mScaleFactor - 1);
objectNewY = ev.getY() + (ev.getY() - super.getHeight() * 0.5f) * (mScaleFactor - 1);
} else {
objectNewX = ev.getX() - (ev.getX() - super.getWidth() * 0.5f) * (1 - mScaleFactor);
objectNewY = ev.getY() - (ev.getY() - super.getHeight() * 0.5f) * (1 - mScaleFactor);
}
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
path = new Path();
path.moveTo(objectNewX,objectNewY);
path.lineTo(objectNewX,objectNewY);
} else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
path.lineTo(objectNewX,objectNewY);
listPath.add(path);
} else if (ev.getAction() == MotionEvent.ACTION_UP) {
path.lineTo(objectNewX,objectNewY);
listPath.add(path);
}
return true;
}
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(0.1f, Math.min(mScaleFactor, 5.0f));
invalidate();
return true;
}
}
Done it finally by myself.
Draw everything by applying this formula to (px,py) coordinates:
float px = ev.getX() / mScaleFactor + rect.left;
float py = ev.getY() / mScaleFactor + rect.top;
rect = canvas.getClipBounds();
//Get them in on Draw function and apply above formula before drawing
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
clipBounds_canvas = canvas.getClipBounds();
/////Do whatever you want to do..!!!
};
#Override
public boolean onTouchEvent(MotionEvent ev) {
int x = ev.getX() / zoomFactor + clipBounds_canvas.left;
int y = ev.getY() / zoomFactor + clipBounds_canvas.top;
//Use the above two values insted of ev.getX() and ev.getY();
}
Hope this will help.