1
votes

I'm developing a ViewGroup layout which is able to detect touch events and scale/rotate/pan it's child view based on touch events. My problem is that I would like the child to rotate/zoom around the point between the user's fingers two fingers, performing the gesture, not around its center.

I've got the gesture recognizers in place and I am already able to scale/pan/rotate around the center of the child. I think the main issue is transforming the pivot point (which in my case would be the midpoint between the user's first and second pointers) to the child's current rotation. The first time I set the pivot, it works nicely, but as soon as I rotate the view, lift fingers and then try to rotate around another pivot, the view jumps around. Here's how it looks (only with rotation):

enter image description here

So when the first rotation happens it works as expected and it rotates around the point between the two fingers. However, once rotated, the next rotation jumps to a new location. Here's the relevant code:

@Override
public boolean onRotationBegin(RotationGestureDetector detector) {
    float[] coordinates = screenPointsToScaledPoints(new float[]{detector.getFocusX(), detector.getFocusY()});
    pivotX = coordinates[0];
    pivotY = coordinates[1];
    child().setPivotX(pivotX);
    child().setPivotY(pivotY);
    return true;
}

@Override
public boolean onRotate(RotationGestureDetector detector) {
    rotation += detector.getRotationDelta();
    return true;
}

private float[] screenPointsToScaledPoints(float[] a){
    mRotateMatrixInverse.mapPoints(a);
    return a;
}

private void applyScaleAndTranslation() {
    View child = child();
    child.setRotation(rotation);
    mRotateMatrix.setRotate(rotation, pivotX, pivotY);
    mRotateMatrix.invert(mRotateMatrixInverse);
}

Any ideas what I could be missing?

1
and no, no need for any invert()ed matrixpskink
Thanks @pskink! That link was actually quite useful and guided me towards the solution. Things work now. For the record, I needed an invert because I'm actually tracking single touch events and need to have their coordinates because I'm making a drawing app, where you can move the canvas around. If you post that link as an answer I can mark it as accepted and post what I did as a separate answer :)Georgi
then feed your detector with the events transform()ed by the current matrix (or i missed your point ;-( ), also feel free to auto answer...pskink
Yeah I ended up transforming the event with the current matrix if no scaling, panning or rotation will be performed. See answer for more info :)Georgi

1 Answers

0
votes

Okay so, thanks to @pskink I've managed to get to the answer. The key was here - Android ImageView Scaling and translating issue

My main mistake was that I was using separate matrices for scaling, moving and rotating whereas in the suggested answer there is only one which contains all operations, which is what I ended up doing. Here's the interesting part of the code:

private Matrix mMatrix = new Matrix();
private Matrix mMatrixInverse = new Matrix();

@Override
protected void dispatchDraw(Canvas canvas) {
    canvas.save();
    canvas.concat(mMatrix);
    super.dispatchDraw(canvas);
    canvas.restore();
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    doubleFingers = ev.getPointerCount() == 2;
    if (!doubleFingers) {
        // Map touch coordinates to our matrix to draw correctly in the canvas.
        float[] mapped = screenPointsToScaledPoints(new float[]{ev.getX(0), ev.getY(0)});
        ev.setLocation(mapped[0], mapped[1]);
    }
    return super.dispatchTouchEvent(ev);
}

private float[] screenPointsToScaledPoints(float[] a){
    mMatrix.invert(mMatrixInverse);
    mMatrixInverse.mapPoints(a);
    return a;
}

@Override
public boolean onScale(ScaleGestureDetector detector) {
    float scaleFactor = detector.getScaleFactor();
    if (scale >= MIN_CANVAS_ZOOM) {
        scale *= scaleFactor;
        scale = Math.max(MIN_CANVAS_ZOOM, Math.min(scale, MAX_CANVAS_ZOOM));
        mMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
    }
    return true;
}

@Override
public boolean onRotate(RotationGestureDetector detector) {
    rotation += detector.getRotationDelta();
    mMatrix.postRotate(detector.getRotationDelta(), detector.getFocusX(), detector.getFocusY());
    return true;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    scaleDetector.onTouchEvent(event);
    rotationDetector.onTouchEvent(event);
    // Lots of business logic
    dX = coordinates[0] - mLastTouchX;
    dY = coordinates[1] - mLastTouchY;
    mLastTouchX = coordinates[0];
    mLastTouchY = coordinates[1];
    mMatrix.postTranslate(dX, dY);
    invalidate();
    return false;
}

In my case I have to manually detect two-finger pan because the one-finger event is used to paint on an OpenGL surface, hence the translation from real-world to scaled matrix coordinates. Here's the result:

enter image description here