Android ImageView Scaling and translating issue
Asked Answered
C

4

10

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

Clemmie answered 7/2, 2014 at 16:57 Comment(14)
why do you use an ImageView with SurfaceView and not draw your Bitmap directly?Thyroxine
If I draw bitmaps directly, how can I move them, scale, rotate them with the finger? (I can have dozen of different views in my surface views that i want to be manage individually with gesture)Clemmie
use a Matrix (canvas.drawBitmap(Bitmap, Matrix, Paint))Thyroxine
I meant : How can I detect on which bitmap my fingers are interacting ?Clemmie
see how i did it in one of my project github.com/pskink/PatchworkDrawable/blob/master/… in method getLayersAt()Thyroxine
Thank you for your code but : -is it not recommended to use Imageview? It seems much more complicated than my code and I still don't see how you can make each of your bitmap simply follow your finger to move, or scale/rotate when you pinch them.Clemmie
complicated? all you need to do is scale/translate/rotate the Matrix, nothing more, simple, plain and obvious, btw using the Matrix you can apply any srd Animations on the Bitmap: you need just ten or so lines of codeThyroxine
I know how to draw or scale a bitmap. I dont know how to detect touch event on them that is why i use image view ( each one have an onsettouchlistener) édit :#3120624Clemmie
did you see getLayersAt() i menrioned before? you need a inversed Matrix and call mapPoint on it, thats allThyroxine
Yes but it's not helping me here :(. Please either make an detailled answer on WHY (because I'm OK with the use of imageview and got no performance issue) I should use custom Bitmap instead of custom ImageView and HOW I could do it in the touch event handling aspect or answer my question which is why I have a strange moving comportement after scaling down an imageview.. Thanks youClemmie
ok so i wrote ~80 lines of ad hoc code: see my answerThyroxine
ooops, seems that i forgot to add the answer, now fixedThyroxine
see my update, i hope now its clear why to use a Matrix and direct Bitmap drawing instead of ImageViewThyroxine
Thank you for your time. I'll study / try this tonight.Clemmie
T
24

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;
        }
    }
}
Thyroxine answered 9/2, 2014 at 8:58 Comment(15)
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.Clemmie
This code is master piece :) Nice work and thanks for sharing :)Mythicize
Wish I could upvote this a million times. Been looking for a simple solution for this for weeks...Prolactin
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(); }Ashantiashbaugh
@Prolactin 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... ;-(Thyroxine
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 ?Jenn
@Thyroxine 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?Chickadee
@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 methodThyroxine
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?Bumkin
How to get the top, left, bottom, right positions of the layer in parent view?Bumkin
@Bumkin check Matrix official documentationThyroxine
@pskink, Thank you for this wonderful solution, Now I want to disable moving the layer out of screen, how to do that?Bumkin
@Bumkin 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 sidesThyroxine
@pskink, can you please look at #52015787Bumkin
@pskink, Can you please look at #52040719Bumkin
M
1

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;
}
}
Myer answered 26/6, 2014 at 6:22 Comment(1)
Can you plz give me code for setTranslationX(), setTranslationY(), setRotation(), setScaleX(), setScaleY() methodsOlympium
C
0

I finally use this (spacing is used to calculated the distance between two fingers), I offset the imageview after scaling to keep it centered, works fine for now :

    float newDist = spacing(event);
            float scale = newDist / oldDist;

            int oldH =getLayoutParams().height;
            int oldW =getLayoutParams().width;

            int newH =(int) (getLayoutParams().height*scale);
            int newW =(int) (getLayoutParams().width*scale);

            if(newH<MAX_SIZE && newW<MAX_SIZE){
                //scale the height and width of the view
                getLayoutParams().height = newH;
                getLayoutParams().width = newW;

                //calculate the X and Y offset to apply after scaling to keep the image centered
                int xOffset = (int)(getLayoutParams().height - oldH)/2;
                int yOffset = (int)(getLayoutParams().width - oldW)/2;

                setX(getX()-xOffset);
                setY(getY()-yOffset);
                requestLayout();
                setAdjustViewBounds(true);

                oldDist=newDist; 
Clemmie answered 13/2, 2014 at 18:10 Comment(8)
gtreat, but do you really think its more compact, more simple and more elegant than using a Matrix ?Thyroxine
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.Clemmie
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)Clemmie
no i dont understand what you mean, sorry, but btw what if two or more ImageViews overlap? will your code work?Thyroxine
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.Clemmie
can you paste your code to pastebin.com? i'd like to check it outThyroxine
Hi Tiffo I try to look your code at pastebin.com , can you paste image class also ?Carn
hey Tiffo I want same functionality to implement in my project can paste image class/ or code. pastebin.Wellington
D
0

All these examples had a glitchy gesture support because of scaleType was set to matrix. When I tried to zoom, I was not able to keep the image in center and control the amount of zoom. So I did some study and wrote a small, easy but very pleasing code for this: https://mcmap.net/q/122184/-android-image-view-pinch-zooming-duplicate

Dramatize answered 12/1, 2021 at 5:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.