Android ImageView Scaling and translating issue
Solution 1
this is a working example of two fingers move/scale/rotate (note: the code is quite short due to smart detector used - see MatrixGestureDetector
):
class ViewPort extends View {
List<Layer> layers = new LinkedList<Layer>();
int[] ids = {R.drawable.layer0, R.drawable.layer1, R.drawable.layer2};
public ViewPort(Context context) {
super(context);
Resources res = getResources();
for (int i = 0; i < ids.length; i++) {
Layer l = new Layer(context, this, BitmapFactory.decodeResource(res, ids[i]));
layers.add(l);
}
}
@Override
protected void onDraw(Canvas canvas) {
for (Layer l : layers) {
l.draw(canvas);
}
}
private Layer target;
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
target = null;
for (int i = layers.size() - 1; i >= 0; i--) {
Layer l = layers.get(i);
if (l.contains(event)) {
target = l;
layers.remove(l);
layers.add(l);
invalidate();
break;
}
}
}
if (target == null) {
return false;
}
return target.onTouchEvent(event);
}
}
class Layer implements MatrixGestureDetector.OnMatrixChangeListener {
Matrix matrix = new Matrix();
Matrix inverse = new Matrix();
RectF bounds;
View parent;
Bitmap bitmap;
MatrixGestureDetector mgd = new MatrixGestureDetector(matrix, this);
public Layer(Context ctx, View p, Bitmap b) {
parent = p;
bitmap = b;
bounds = new RectF(0, 0, b.getWidth(), b.getHeight());
matrix.postTranslate(50 + (float) Math.random() * 50, 50 + (float) Math.random() * 50);
}
public boolean contains(MotionEvent event) {
matrix.invert(inverse);
float[] pts = {event.getX(), event.getY()};
inverse.mapPoints(pts);
if (!bounds.contains(pts[0], pts[1])) {
return false;
}
return Color.alpha(bitmap.getPixel((int) pts[0], (int) pts[1])) != 0;
}
public boolean onTouchEvent(MotionEvent event) {
mgd.onTouchEvent(event);
return true;
}
@Override
public void onChange(Matrix matrix) {
parent.invalidate();
}
public void draw(Canvas canvas) {
canvas.drawBitmap(bitmap, matrix, null);
}
}
class MatrixGestureDetector {
private static final String TAG = "MatrixGestureDetector";
private int ptpIdx = 0;
private Matrix mTempMatrix = new Matrix();
private Matrix mMatrix;
private OnMatrixChangeListener mListener;
private float[] mSrc = new float[4];
private float[] mDst = new float[4];
private int mCount;
interface OnMatrixChangeListener {
void onChange(Matrix matrix);
}
public MatrixGestureDetector(Matrix matrix, MatrixGestureDetector.OnMatrixChangeListener listener) {
this.mMatrix = matrix;
this.mListener = listener;
}
public void onTouchEvent(MotionEvent event) {
if (event.getPointerCount() > 2) {
return;
}
int action = event.getActionMasked();
int index = event.getActionIndex();
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
int idx = index * 2;
mSrc[idx] = event.getX(index);
mSrc[idx + 1] = event.getY(index);
mCount++;
ptpIdx = 0;
break;
case MotionEvent.ACTION_MOVE:
for (int i = 0; i < mCount; i++) {
idx = ptpIdx + i * 2;
mDst[idx] = event.getX(i);
mDst[idx + 1] = event.getY(i);
}
mTempMatrix.setPolyToPoly(mSrc, ptpIdx, mDst, ptpIdx, mCount);
mMatrix.postConcat(mTempMatrix);
if(mListener != null) {
mListener.onChange(mMatrix);
}
System.arraycopy(mDst, 0, mSrc, 0, mDst.length);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
if (event.getPointerId(index) == 0) ptpIdx = 2;
mCount--;
break;
}
}
}
Solution 2
I tried to implementation of multiple touch on view not on bitmap using matrix, now i success. Now i think it will helpful to you for individual gesture for multiple image. Try it, it work best for me.
public class MultiTouchImageView extends ImageView implements OnTouchListener{
float[] lastEvent = null;
float d = 0f;
float newRot = 0f;
public static String fileNAME;
public static int framePos = 0;
//private ImageView view;
private boolean isZoomAndRotate;
private boolean isOutSide;
// We can be in one of these 3 states
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();
float oldDist = 1f;
public MultiTouchImageView(Context context) {
super(context);
}
public MultiTouchImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MultiTouchImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@SuppressWarnings("deprecation")
@Override
public boolean onTouch(View v, MotionEvent event) {
//view = (ImageView) v;
bringToFront();
// Handle touch events here...
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
//savedMatrix.set(matrix);
start.set(event.getX(), event.getY());
mode = DRAG;
lastEvent = null;
break;
case MotionEvent.ACTION_POINTER_DOWN:
oldDist = spacing(event);
if (oldDist > 10f) {
midPoint(mid, event);
mode = ZOOM;
}
lastEvent = new float[4];
lastEvent[0] = event.getX(0);
lastEvent[1] = event.getX(1);
lastEvent[2] = event.getY(0);
lastEvent[3] = event.getY(1);
d = rotation(event);
break;
case MotionEvent.ACTION_UP:
isZoomAndRotate = false;
case MotionEvent.ACTION_OUTSIDE:
isOutSide = true;
mode = NONE;
lastEvent = null;
case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
lastEvent = null;
break;
case MotionEvent.ACTION_MOVE:
if(!isOutSide){
if (mode == DRAG && !isZoomAndRotate) {
isZoomAndRotate = false;
setTranslationX((event.getX() - start.x) + getTranslationX());
setTranslationY((event.getY() - start.y) + getTranslationY());
} else if (mode == ZOOM && event.getPointerCount() == 2) {
isZoomAndRotate = true;
boolean isZoom = false;
if(!isRotate(event)){
float newDist = spacing(event);
if (newDist > 10f) {
float scale = newDist / oldDist * getScaleX();
setScaleX(scale);
setScaleY(scale);
isZoom = true;
}
}
else if(!isZoom){
newRot = rotation(event);
setRotation((float)(getRotation() + (newRot - d)));
}
}
}
break;
}
new GestureDetector(new MyGestureDectore());
Constants.currentSticker = this;
return true;
}
private class MyGestureDectore extends GestureDetector.SimpleOnGestureListener{
@Override
public boolean onDoubleTap(MotionEvent e) {
bringToFront();
return false;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
}
private float rotation(MotionEvent event) {
double delta_x = (event.getX(0) - event.getX(1));
double delta_y = (event.getY(0) - event.getY(1));
double radians = Math.atan2(delta_y, delta_x);
return (float) Math.toDegrees(radians);
}
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 boolean isRotate(MotionEvent event){
int dx1 = (int) (event.getX(0) - lastEvent[0]);
int dy1 = (int) (event.getY(0) - lastEvent[2]);
int dx2 = (int) (event.getX(1) - lastEvent[1]);
int dy2 = (int) (event.getY(1) - lastEvent[3]);
Log.d("dx1 ", ""+ dx1);
Log.d("dx2 ", "" + dx2);
Log.d("dy1 ", "" + dy1);
Log.d("dy2 ", "" + dy2);
//pointer 1
if(Math.abs(dx1) > Math.abs(dy1) && Math.abs(dx2) > Math.abs(dy2)) {
if(dx1 >= 2.0 && dx2 <= -2.0){
Log.d("first pointer ", "right");
return true;
}
else if(dx1 <= -2.0 && dx2 >= 2.0){
Log.d("first pointer ", "left");
return true;
}
}
else {
if(dy1 >= 2.0 && dy2 <= -2.0){
Log.d("seccond pointer ", "top");
return true;
}
else if(dy1 <= -2.0 && dy2 >= 2.0){
Log.d("second pointer ", "bottom");
return true;
}
}
return false;
}
}
Tifoo
Updated on July 28, 2022Comments
-
Tifoo almost 2 years
I’m developing an android application (API 19 4.4) and I encounter some issue with ImageViews. I have a SurfaceView, in which I dynamically add ImageViews which I want to react to touch events. On so far, I have managed to make the ImageView move and scale smoothly but I have an annoying behavior.
When I scale down the image to a certain limit (I would say half the original size) and I try to move it, the image flicker. After a short analysis, it seems that it’s switching its position symmetrically around the finger point on the screen, cumulating distance, and finally gets out of sight (all that happens very fast ( < 1s). I think I am missing something with the relative value of the touch event to the ImageView/SurfaceView, but I’m a quite a noob and I’m stucked…
Here is my code
public class MyImageView extends ImageView { private ScaleGestureDetector mScaleDetector ; private static final int MAX_SIZE = 1024; private static final String TAG = "MyImageView"; PointF DownPT = new PointF(); // Record Mouse Position When Pressed Down PointF StartPT = new PointF(); // Record Start Position of 'img' public MyImageView(Context context) { super(context); mScaleDetector = new ScaleGestureDetector(context,new MySimpleOnScaleGestureListener()); setBackgroundColor(Color.RED); setScaleType(ScaleType.MATRIX); setAdjustViewBounds(true); RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); lp.setMargins(-MAX_SIZE, -MAX_SIZE, -MAX_SIZE, -MAX_SIZE); this.setLayoutParams(lp); this.setX(MAX_SIZE); this.setY(MAX_SIZE); } int firstPointerID; boolean inScaling=false; @Override public boolean onTouchEvent(MotionEvent event) { // get pointer index from the event object int pointerIndex = event.getActionIndex(); // get pointer ID int pointerId = event.getPointerId(pointerIndex); //First send event to scale detector to find out, if it's a scale boolean res = mScaleDetector.onTouchEvent(event); if (!mScaleDetector.isInProgress()) { int eid = event.getAction(); switch (eid & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE : if(pointerId == firstPointerID) { PointF mv = new PointF( (int)(event.getX() - DownPT.x), (int)( event.getY() - DownPT.y)); this.setX((int)(StartPT.x+mv.x)); this.setY((int)(StartPT.y+mv.y)); StartPT = new PointF( this.getX(), this.getY() ); } break; case MotionEvent.ACTION_DOWN : { firstPointerID = pointerId; DownPT.x = (int) event.getX(); DownPT.y = (int) event.getY(); StartPT = new PointF( this.getX(), this.getY() ); break; } case MotionEvent.ACTION_POINTER_DOWN: { break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_CANCEL: { firstPointerID = -1; break; } default : break; } return true; } return true; } public boolean onScaling(ScaleGestureDetector detector) { this.setScaleX(this.getScaleX()*detector.getScaleFactor()); this.setScaleY(this.getScaleY()*detector.getScaleFactor()); invalidate(); return true; } private class MySimpleOnScaleGestureListener extends SimpleOnScaleGestureListener { @Override public boolean onScale(ScaleGestureDetector detector) { return onScaling(detector); } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { Log.d(TAG, "onScaleBegin"); return true; } @Override public void onScaleEnd(ScaleGestureDetector arg0) { Log.d(TAG, "onScaleEnd"); } }
}
I have also another questions about rotations. How should I implement this? Could I use the ScalegestureDetector in some way or have I to make this works in the view touch event? I would like to be able to scale and rotate in the same gesture (and move in another).
Thank for helping me, I would really appreciate!
Sorry for my english
-
Tifoo over 10 yearsHello, sorry for the delay. Thanks again for your time. I solved my issue by not scaling my view using "setscale" but with setting layoutparam. Universalimageloader loader takes care of the bitmaps just fine.
-
pskink over 10 yearsgtreat, but do you really think its more compact, more simple and more elegant than using a Matrix ?
-
Tifoo over 10 yearsIn fact no I don't, that's why i keep your code in my bags for the future... My test with the matrix mode scaled the canvas inside my views and not the view itself, maybe I was missing something.
-
Tifoo over 10 yearsi scale my view and not the canvas to keep my touchevent only to the visible parts of the view (if I reduce the size of my canvas, only the new visible part should fire touch events) and not the whole view(I'm not sure if you understand what i mean). If your code will do the trick, I'll change that (I had no times to test yet, but i will)
-
pskink over 10 yearsno i dont understand what you mean, sorry, but btw what if two or more ImageViews overlap? will your code work?
-
Tifoo over 10 yearsYes it works fine when multiples views overlap (that was pretty much my worries about your code!). It reacts fine with two fingers interaction on one image, or one finger on each one to make them move simultaneously.
-
pskink over 10 yearscan you paste your code to pastebin.com? i'd like to check it out
-
ik024 about 10 yearsThis code is master piece :) Nice work and thanks for sharing :)
-
viyancs about 10 yearsHi Tiffo I try to look your code at pastebin.com , can you paste image class also ?
-
Er.Shreyansh Shah almost 10 yearshey Tiffo I want same functionality to implement in my project can paste image class/ or code. pastebin.
-
LukeWaggoner about 9 yearsWish I could upvote this a million times. Been looking for a simple solution for this for weeks...
-
Daniel Viglione almost 7 yearsI added a method to ViewPort to allow users to add images from the MainActivity. Unfortunately when I call invalidate() within my method, onDraw does not get called and image does not appear: public void addLayer(Integer id){ Resources res = getResources(); ids.add(id); Layer l = new Layer(mContext, this, BitmapFactory.decodeResource(res, id)); layers.add(l); invalidate(); }
-
pskink over 6 years@LukeWaggoner now the solution is even more simple - see
MatrixGestureDetector
class - i made it in half of hour so it can still have some bugs thou... ;-( -
Irhala over 6 yearsHi, i'm trying to use this with the ViewPort being a FrameLayout, essentially trying to zoom on all its content, which has clickable/draggable elements. Is it possible and if so, what should I use as
Layer
? -
Timothy Bomer over 6 years@pskink Hello! I am trying to figure out how to implement this code into my application. I have it in as a class right now. Could you tell me how I would actually put this to use?
-
pskink over 6 years@TimothyBomer use
ViewPort
(which extendsView
) as any otherView
likeTextView
,Button
etc - or if you want to just check it out pass it toActivity#setContentView
method -
Aman Srivastava over 6 yearsCan you plz give me code for setTranslationX(), setTranslationY(), setRotation(), setScaleX(), setScaleY() methods
-
Gunaseelan almost 6 yearsHow to found this layer is fully inside the parent view's bound or some half of the layer is outside the parent view's bound?
-
Gunaseelan almost 6 yearsHow to get the top, left, bottom, right positions of the layer in parent view?
-
pskink almost 6 years@Gunaseelan check
Matrix
official documentation -
Gunaseelan almost 6 years@pskink, Thank you for this wonderful solution, Now I want to disable moving the layer out of screen, how to do that?
-
pskink almost 6 years@Gunaseelan if the matrix is translated too much to the right simply translate it to the left, do the same checks with top, bottom and left sides
-
Gunaseelan over 5 years@pskink, can you please look at stackoverflow.com/questions/52015287/…
-
Gunaseelan over 5 years@pskink, Can you please look at stackoverflow.com/questions/52040219/…