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

Tifoo
  • 131
  • 1
  • 1
  • 8
  • why do you use an ImageView with SurfaceView and not draw your Bitmap directly? – pskink Feb 07 '14 at 17:10
  • 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) – Tifoo Feb 07 '14 at 17:13
  • use a Matrix (canvas.drawBitmap(Bitmap, Matrix, Paint)) – pskink Feb 07 '14 at 17:17
  • I meant : How can I detect on which bitmap my fingers are interacting ? – Tifoo Feb 07 '14 at 17:19
  • see how i did it in one of my project https://github.com/pskink/PatchworkDrawable/blob/master/PatchworkDrawableLibrary/src/org/pskink/patchworkdrawable/drawable/PatchworkDrawable.java in method getLayersAt() – pskink Feb 07 '14 at 18:01
  • 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. – Tifoo Feb 07 '14 at 22:08
  • 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 code – pskink Feb 07 '14 at 23:37
  • 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 :http://stackoverflow.com/questions/3120124/make-bitmaps-listen-to-touch-events – Tifoo Feb 08 '14 at 10:47
  • did you see getLayersAt() i menrioned before? you need a inversed Matrix and call mapPoint on it, thats all – pskink Feb 08 '14 at 10:52
  • 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 you – Tifoo Feb 08 '14 at 14:48
  • ok so i wrote ~80 lines of ad hoc code: see my answer – pskink Feb 08 '14 at 16:17
  • ooops, seems that i forgot to add the answer, now fixed – pskink Feb 09 '14 at 08:57
  • see my update, i hope now its clear why to use a Matrix and direct Bitmap drawing instead of ImageView – pskink Feb 10 '14 at 08:49
  • Thank you for your time. I'll study / try this tonight. – Tifoo Feb 10 '14 at 09:30

4 Answers4

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;
        }
    }
}
pskink
  • 23,874
  • 6
  • 66
  • 77
  • 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. – Tifoo Feb 13 '14 at 18:05
  • 4
    This code is master piece :) Nice work and thanks for sharing :) – ik024 Apr 08 '14 at 12:19
  • Wish I could upvote this a million times. Been looking for a simple solution for this for weeks... – LukeWaggoner Feb 25 '15 at 17:07
  • 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(); } – Daniel Viglione Aug 02 '17 at 23:41
  • @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... ;-( – pskink Sep 12 '17 at 11:15
  • 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` ? – Irhala Sep 29 '17 at 11:18
  • @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? – Timothy Bomer Oct 27 '17 at 02:59
  • @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 – pskink Oct 27 '17 at 05:33
  • 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 Jul 24 '18 at 13:15
  • How to get the top, left, bottom, right positions of the layer in parent view? – Gunaseelan Jul 24 '18 at 13:20
  • @Gunaseelan check `Matrix` official documentation – pskink Jul 24 '18 at 17:23
  • @pskink, Thank you for this wonderful solution, Now I want to disable moving the layer out of screen, how to do that? – Gunaseelan Jul 31 '18 at 06:42
  • @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 – pskink Aug 06 '18 at 17:32
  • @pskink, can you please look at https://stackoverflow.com/questions/52015287/change-rectf-values-after-matrix-scalling – Gunaseelan Aug 25 '18 at 08:13
  • @pskink, Can you please look at https://stackoverflow.com/questions/52040219/rotate-and-scale-view-proportionally-using-matrix – Gunaseelan Aug 27 '18 at 13:14
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;
}
}
Android Leo
  • 666
  • 1
  • 8
  • 24
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; 
Tifoo
  • 131
  • 1
  • 1
  • 8
  • gtreat, but do you really think its more compact, more simple and more elegant than using a Matrix ? – pskink Feb 13 '14 at 18:40
  • 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 Feb 13 '14 at 18:42
  • 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) – Tifoo Feb 13 '14 at 18:50
  • no i dont understand what you mean, sorry, but btw what if two or more ImageViews overlap? will your code work? – pskink Feb 13 '14 at 19:50
  • 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. – Tifoo Feb 13 '14 at 19:56
  • 3
    can you paste your code to pastebin.com? i'd like to check it out – pskink Feb 14 '14 at 08:17
  • Hi Tiffo I try to look your code at pastebin.com , can you paste image class also ? – viyancs Apr 21 '14 at 08:29
  • hey Tiffo I want same functionality to implement in my project can paste image class/ or code. pastebin. – Er.Shreyansh Shah May 31 '14 at 07:18
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://stackoverflow.com/a/65697376/13339685

mohit48
  • 712
  • 7
  • 9