Android ImageView Scaling and translating issue

14,182

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;
}
}
Share:
14,182
Tifoo
Author by

Tifoo

Updated on July 28, 2022

Comments

  • Tifoo
    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
    Tifoo over 10 years
    Hello, 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
    pskink over 10 years
    gtreat, but do you really think its more compact, more simple and more elegant than using a Matrix ?
  • Tifoo
    Tifoo over 10 years
    In 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
    Tifoo over 10 years
    i 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
    pskink over 10 years
    no i dont understand what you mean, sorry, but btw what if two or more ImageViews overlap? will your code work?
  • Tifoo
    Tifoo over 10 years
    Yes 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
    pskink over 10 years
    can you paste your code to pastebin.com? i'd like to check it out
  • ik024
    ik024 about 10 years
    This code is master piece :) Nice work and thanks for sharing :)
  • viyancs
    viyancs about 10 years
    Hi Tiffo I try to look your code at pastebin.com , can you paste image class also ?
  • Er.Shreyansh Shah
    Er.Shreyansh Shah almost 10 years
    hey Tiffo I want same functionality to implement in my project can paste image class/ or code. pastebin.
  • LukeWaggoner
    LukeWaggoner about 9 years
    Wish I could upvote this a million times. Been looking for a simple solution for this for weeks...
  • Daniel Viglione
    Daniel Viglione almost 7 years
    I 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
    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
    Irhala over 6 years
    Hi, 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
    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
    pskink over 6 years
    @TimothyBomer use ViewPort (which extends View) as any other View like TextView, Button etc - or if you want to just check it out pass it to Activity#setContentView method
  • Aman Srivastava
    Aman Srivastava over 6 years
    Can you plz give me code for setTranslationX(), setTranslationY(), setRotation(), setScaleX(), setScaleY() methods
  • Gunaseelan
    Gunaseelan almost 6 years
    How 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
    Gunaseelan almost 6 years
    How to get the top, left, bottom, right positions of the layer in parent view?
  • pskink
    pskink almost 6 years
    @Gunaseelan check Matrix official documentation
  • Gunaseelan
    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
    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
    Gunaseelan over 5 years
    @pskink, can you please look at stackoverflow.com/questions/52015287/…
  • Gunaseelan
    Gunaseelan over 5 years
    @pskink, Can you please look at stackoverflow.com/questions/52040219/…