使用Android内置的手势监听器和缩放监听器实现捏合缩放和拖动。

22

我正在尝试使用Android的手势监听器和缩放监听器实现捏合缩放和拖动。问题是,当我执行捏合缩放时,图像(我正在尝试缩放的图像)会反弹到特定的位置。而且缩放的位置也不是居中的。 以下代码演示了我想要实现的内容。有什么想法可以解决图像跳跃的问题(以及如何纠正它)?

public class CustomView extends View {
    Bitmap image;
    int screenHeight;
    int screenWidth;
    Paint paint;
    GestureDetector gestures;
    ScaleGestureDetector scaleGesture;
    float scale = 1.0f;
    float horizontalOffset, verticalOffset;

    int NORMAL = 0;
    int ZOOM = 1;
    int DRAG = 2;
    boolean isScaling = false;
    float touchX, touchY;
        int mode = NORMAL;

    public CustomView(Context context) {
    super(context);
            //initializing variables
    image = BitmapFactory.decodeResource(getResources(),
            R.drawable.image_name);
            //This is a full screen view
    screenWidth = getResources().getDisplayMetrics().widthPixels;
    screenHeight = getResources().getDisplayMetrics().heightPixels;
    paint = new Paint();
    paint.setAntiAlias(true);
    paint.setFilterBitmap(true);
    paint.setDither(true);
    paint.setColor(Color.WHITE);

    scaleGesture = new ScaleGestureDetector(getContext(),
            new ScaleListener());
    gestures = new GestureDetector(getContext(), new GestureListener());
    mode = NORMAL;
    initialize();
}

//Best fit image display on canvas 
    private void initialize() {
    float imgPartRatio = image.getWidth() / (float) image.getHeight();
    float screenRatio = (float) screenWidth / (float) screenHeight;

    if (screenRatio > imgPartRatio) {
        scale = ((float) screenHeight) / (float) (image.getHeight()); // fit height
        horizontalOffset = ((float) screenWidth - scale
                * (float) (image.getWidth())) / 2.0f;
        verticalOffset = 0;
    } else {
        scale = ((float) screenWidth) / (float) (image.getWidth()); // fit width
        horizontalOffset = 0;
        verticalOffset = ((float) screenHeight - scale
                * (float) (image.getHeight())) / 2.0f;
    }
        invalidate();
}

@Override
protected void onDraw(Canvas canvas) {
    canvas.save();
    canvas.drawColor(0, Mode.CLEAR);
    canvas.drawColor(Color.WHITE);
    if(mode == DRAG || mode == NORMAL) {
        //This works perfectly as expected
        canvas.translate(horizontalOffset, verticalOffset);
        canvas.scale(scale, scale);
        canvas.drawBitmap(image, getMatrix(), paint);
    }
    else if (mode == ZOOM) {
        //PROBLEM AREA - when applying pinch zoom,
        //the image jumps to a position abruptly
        canvas.scale(scale, scale, touchX, touchY);
        canvas.drawBitmap(image, getMatrix(), paint);
    }
    canvas.restore();
}

public class ScaleListener implements OnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        float scaleFactorNew = detector.getScaleFactor();
        if (detector.isInProgress()) {
            touchX = detector.getFocusX();
            touchY = detector.getFocusY();
            scale *= scaleFactorNew;
            invalidate(0, 0, screenWidth, screenHeight);
        }
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        isScaling = true;
        mode=ZOOM;
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        mode = NORMAL;
        isScaling = false;
    }

}

public class GestureListener implements GestureDetector.OnGestureListener,
        GestureDetector.OnDoubleTapListener {

    @Override
    public boolean onDown(MotionEvent e) {
        isScaling = false;
        return true;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2,
            float distanceX, float distanceY) {
        if (!isScaling) {
            mode = DRAG;
            isScaling = false;
            horizontalOffset -= distanceX;
            verticalOffset -= distanceY;
            invalidate(0, 0, screenWidth, screenHeight);
        } else {
            mode = ZOOM;
            isScaling = true;
        }
        return true;
    }
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    scaleGesture.onTouchEvent(event);
    gestures.onTouchEvent(event);
    return true;
}
}

提前感谢。


如果在使用ZOOM模式之前没有使用DRAG模式,也就是说,在应用任何平移之前,首先进行的是ZOOM操作,那么图像是否仍然会跳动? - Gil Moshayof
是的,图像仍然会跳动。 - chochim
在缩放时,水平/垂直偏移量不应该也被应用吗?你试过在缩放时应用这些偏移量吗? - Gil Moshayof
@GilMoshayof - 水平和垂直缩放也应在缩放过程中应用。但这并没有什么区别-图像仍然会跳动。似乎我需要调整缩放支点。这里也有一个类似的问题(尽管实现更清晰):https://dev59.com/7XjZa4cB1Zd3GeqPairc 根据此问题提供的答案,需要调整我们进行缩放的支点。我仍在苦苦思索如何正确计算支点坐标。 - chochim
你可以尝试在onScaleBegin中设置touchX/touchY,看看会不会有所改变。 - Gil Moshayof
@GilMoshayof - 没有帮助 :( - chochim
1个回答

41

我已经实现了这个功能,使用矩阵来处理所有的缩放、滚动(以及在我的情况下旋转)。这使得代码整洁,操作流畅。

把一个矩阵作为类成员变量存储:

Matrix drawMatrix;

编辑:存储旧的焦点,用于在缩放期间获取焦点位移。

float lastFocusX;
float lastFocusY;

编辑:在onScaleBegin函数中设置lastFocus变量

@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
    lastFocusX = detector.getFocusX();
    lastFocusY = detector.getFocusY();
    return true;
}

替换你的onScale:

@Override
public boolean onScale(ScaleGestureDetector detector) {
    Matrix transformationMatrix = new Matrix();
    float focusX = detector.getFocusX();
    float focusY = detector.getFocusY();

    //Zoom focus is where the fingers are centered, 
    transformationMatrix.postTranslate(-focusX, -focusY);

    transformationMatrix.postScale(detector.getScaleFactor(), detector.getScaleFactor());

/* Adding focus shift to allow for scrolling with two pointers down. Remove it to skip this functionality. This could be done in fewer lines, but for clarity I do it this way here */
    //Edited after comment by chochim
    float focusShiftX = focusX - lastFocusX;
    float focusShiftY = focusY - lastFocusY;
    transformationMatrix.postTranslate(focusX + focusShiftX, focusY + focusShiftY);
    drawMatrix.postConcat(transformationMatrix);
    lastFocusX = focusX;
    lastFocusY = focusY;
    invalidate();
    return true;
}

同样在 onScroll 中:

@Override
public boolean onScroll(MotionEvent downEvent, MotionEvent currentEvent,
            float distanceX, float distanceY) {
    drawMatrix.postTranslate(-distanceX, -distanceY);
    invalidate();
    return true;
}

在 onDraw 方法中,使用您的 drawMatrix 进行绘制:

canvas.drawBitmap(image, drawMatrix, paint);

开心编码!


1
清晰、优雅的解决方案。detector.getFocusShiftX() 是什么? - chochim
1
啊,抱歉。当我实现这个功能时,我自己编写了手势检测器来获取旋转信息。getFocusShift是一个实用方法,在ScaleGestureDetector中显然不可用。为了完整起见,我会编辑我的答案。 - Tore Rudberg
1
如果您已经在 onScroll 中翻译了矩阵,那么您就不需要使用 focusShift。为了正确工作,请将 transformationMatrix.postTranslate(focusX + focusShiftX), focusY + focusShiftY); 更改为 transformationMatrix.postTranslate(focusX, focusY); - David Novák
@DavidNovák:如果手势被识别为缩放,那么您将无法获得该手势的滚动更新,我的代码允许用户在缩放事件期间滚动。如果您不想要这种行为,那么请按照您自己的方式进行操作。 - Tore Rudberg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接