我怎样才能放大 ImageView 而不会在其他地方稍微跳动? (Android)

How can I zoom into an ImageView without it jumping slightly elsewhere? (Android)

我正在尝试创建一个用户可以在其中拖动和缩放 ImageView 的应用程序。但是我在使用以下代码时遇到了问题。

当 scaleFactor 不是 1 并且第二根手指向下时,它会稍微平移到其他地方。我不知道这个翻译是从哪里来的...

这是完整的 class:

package me.miutaltbati.ramaview;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.widget.ImageView;

import static android.view.MotionEvent.INVALID_POINTER_ID;

public class RamaView extends ImageView {
    private Context context;
    private Matrix matrix = new Matrix();
    private Matrix translateMatrix = new Matrix();
    private Matrix scaleMatrix = new Matrix();

    // Properties coming from outside:
    private int drawableLayoutId;
    private int width;
    private int height;

    private static float MIN_ZOOM = 0.33333F;
    private static float MAX_ZOOM = 5F;

    private PointF mLastTouch = new PointF(0, 0);
    private PointF mLastFocus = new PointF(0, 0);
    private PointF mLastPivot = new PointF(0, 0);
    private float mPosX = 0F;
    private float mPosY = 0F;

    public float scaleFactor = 1F;
    private int mActivePointerId = INVALID_POINTER_ID;

    private Paint paint;
    private Bitmap bitmapLayout;

    private OnFactorChangedListener mListener;
    private ScaleGestureDetector mScaleDetector;

    public RamaView(Context context) {
        super(context);
        initializeInConstructor(context);
    }

    public RamaView(Context context, @android.support.annotation.Nullable AttributeSet attrs) {
        super(context, attrs);
        initializeInConstructor(context);
    }

    public RamaView(Context context, @android.support.annotation.Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initializeInConstructor(context);
    }

    public void initializeInConstructor(Context context) {
        this.context = context;
        paint = new Paint();
        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
        mScaleDetector.setQuickScaleEnabled(false);

        setScaleType(ScaleType.MATRIX);
    }

    public Bitmap decodeSampledBitmap() {
        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inMutable = true;
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(getResources(), drawableLayoutId, options);

        // Calculate inSampleSize
        options.inSampleSize = Util.calculateInSampleSize(options, width, height); // e.g.: 4, 8

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(getResources(), drawableLayoutId, options);
    }

    public void setDrawable(int drawableId) {
        drawableLayoutId = drawableId;
    }

    public void setSize(int width, int height) {
        this.width = width;
        this.height = height;

        bitmapLayout = decodeSampledBitmap();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mScaleDetector.onTouchEvent(event);

        int action = event.getActionMasked();

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                int pointerIndex = event.getActionIndex();
                float x = event.getX(pointerIndex);
                float y = event.getY(pointerIndex);

                // Remember where we started (for dragging)
                mLastTouch = new PointF(x, y);

                // Save the ID of this pointer (for dragging)
                mActivePointerId = event.getPointerId(0);
            }

            case MotionEvent.ACTION_POINTER_DOWN: {
                if (event.getPointerCount() == 2) {
                    mLastFocus = new PointF(mScaleDetector.getFocusX(), mScaleDetector.getFocusY());
                }
            }

            case MotionEvent.ACTION_MOVE: {
                // Find the index of the active pointer and fetch its position
                int pointerIndex = event.findPointerIndex(mActivePointerId);

                float x = event.getX(pointerIndex);
                float y = event.getY(pointerIndex);

                // Calculate the distance moved
                float dx = 0;
                float dy = 0;

                if (event.getPointerCount() == 1) {
                    // Calculate the distance moved
                    dx = x - mLastTouch.x;
                    dy = y - mLastTouch.y;

                    matrix.setScale(scaleFactor, scaleFactor, mLastPivot.x, mLastPivot.y);

                    // Remember this touch position for the next move event
                    mLastTouch = new PointF(x, y);
                } else if (event.getPointerCount() == 2) {
                    // Calculate the distance moved
                    dx = mScaleDetector.getFocusX() - mLastFocus.x;
                    dy = mScaleDetector.getFocusY() - mLastFocus.y;

                    matrix.setScale(scaleFactor, scaleFactor, -mPosX + mScaleDetector.getFocusX(), -mPosY + mScaleDetector.getFocusY());

                    mLastPivot = new PointF(-mPosX + mScaleDetector.getFocusX(), -mPosY + mScaleDetector.getFocusY());
                    mLastFocus = new PointF(mScaleDetector.getFocusX(), mScaleDetector.getFocusY());
                }

                mPosX += dx;
                mPosY += dy;

                matrix.postTranslate(mPosX, mPosY);

                break;
            }

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL: {
                mActivePointerId = INVALID_POINTER_ID;
                break;
            }

            case MotionEvent.ACTION_POINTER_UP: {
                final int pointerIndex = event.getActionIndex();
                final int pointerId = event.getPointerId(pointerIndex);

                if (pointerId == mActivePointerId) {
                    // This was our active pointer going up. Choose a new
                    // active pointer and adjust accordingly.
                    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                    mLastTouch = new PointF(event.getX(newPointerIndex), event.getY(newPointerIndex));
                    mActivePointerId = event.getPointerId(newPointerIndex);
                } else {
                    final int tempPointerIndex = event.findPointerIndex(mActivePointerId);
                    mLastTouch = new PointF(event.getX(tempPointerIndex), event.getY(tempPointerIndex));
                }

                break;
            }
        }

        invalidate();
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.save();

        canvas.setMatrix(matrix);
        canvas.drawColor(Color.BLACK);
        canvas.drawBitmap(bitmapLayout, 0, 0, paint);

        canvas.restore();
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            scaleFactor *= detector.getScaleFactor();
            scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));

            return true;
        }
    }
}

我认为问题出在这一行:

matrix.setScale(scaleFactor, scaleFactor, -mPosX + mScaleDetector.getFocusX(), -mPosY + mScaleDetector.getFocusY());

我尝试了很多东西,但我无法让它正常工作。

更新:

以下是初始化 RamaView 实例的方法:

主要 activity 的 onCreate:

rvRamaView = findViewById(R.id.rvRamaView);

final int[] rvSize = new int[2];
ViewTreeObserver vto = rvRamaView.getViewTreeObserver();
vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw() {
        rvRamaView.getViewTreeObserver().removeOnPreDrawListener(this);
        rvSize[0] = rvRamaView.getMeasuredWidth();
        rvSize[1] = rvRamaView.getMeasuredHeight();

        rvRamaView.setSize(rvSize[0], rvSize[1]);
        return true;
    }
});

rvRamaView.setDrawable(R.drawable.original_jpg);

似乎代码不完整(例如,我看不到矩阵是如何使用的以及 scaleFactor 的分配位置),但我认为翻译不一致的原因是因为如果有 2 个指针,你会得到 [x , y] 位置从 mScaleDetector.getFocus。正如 ScaleGestureDetector.getFocusX() 的文档所述:

Get the X coordinate of the current gesture's focal point. If a gesture is in progress, the focal point is between each of the pointers forming the gesture.

您应该只使用 mScaleDetector 来获取当前比例,但平移应该始终计算为 mLastTouchevent.getXY(pointerIndex) 之间的差异,以便只考虑一个指针用于翻译。如果用户添加第二根手指并释放第一根手指,请确保重新分配 pointerIndex 并且不执行任何转换以避免跳转。

最好使用矩阵来累积 变化,而不是尝试自己重新计算转换。您可以使用矩阵 post...pre... 方法执行此操作并远离 set... 重置矩阵的方法。

这里是 RamaView class 的返工,除了上面提到的对矩阵的特定处理外,它在很大程度上符合目标。模组是 onTouchEvent() 方法。该视频是在示例应用程序中运行的代码的输出。

RamaView.java

public class RamaView extends ImageView {
    private final Matrix matrix = new Matrix();

    // Properties coming from outside:
    private int drawableLayoutId;
    private int width;
    private int height;

    private static final float MIN_ZOOM = 0.33333F;
    private static final float MAX_ZOOM = 5F;

    private PointF mLastTouch = new PointF(0, 0);
    private PointF mLastFocus = new PointF(0, 0);

    public float scaleFactor = 1F;
    private int mActivePointerId = INVALID_POINTER_ID;

    private Paint paint;
    private Bitmap bitmapLayout;

    //    private OnFactorChangedListener mListener;
    private ScaleGestureDetector mScaleDetector;

    public RamaView(Context context) {
        super(context);
        initializeInConstructor(context);
    }

    public RamaView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initializeInConstructor(context);
    }

    public RamaView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initializeInConstructor(context);
    }

    public void initializeInConstructor(Context context) {
        paint = new Paint();
        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
        mScaleDetector.setQuickScaleEnabled(false);

        setScaleType(ScaleType.MATRIX);
    }

    public Bitmap decodeSampledBitmap() {
        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inMutable = true;
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(getResources(), drawableLayoutId, options);

        options.inSampleSize = Util.calculateInSampleSize(options, width, height); // e.g.: 4, 8

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(getResources(), drawableLayoutId, options);
    }

    public void setDrawable(int drawableId) {
        drawableLayoutId = drawableId;
    }

    public void setSize(int width, int height) {
        this.width = width;
        this.height = height;

        bitmapLayout = decodeSampledBitmap();
    }

    private float mLastScaleFactor = 1.0f;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mScaleDetector.onTouchEvent(event);

        int action = event.getActionMasked();

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                int pointerIndex = event.getActionIndex();
                float x = event.getX(pointerIndex);
                float y = event.getY(pointerIndex);

                // Remember where we started (for dragging)
                mLastTouch = new PointF(x, y);

                // Save the ID of this pointer (for dragging)
                mActivePointerId = event.getPointerId(0);
            }

            case MotionEvent.ACTION_POINTER_DOWN: {
                if (event.getPointerCount() == 2) {
                    mLastFocus = new PointF(mScaleDetector.getFocusX(), mScaleDetector.getFocusY());
                }
            }

            case MotionEvent.ACTION_MOVE: {
                // Find the index of the active pointer and fetch its position
                int pointerIndex = event.findPointerIndex(mActivePointerId);

                float x = event.getX(pointerIndex);
                float y = event.getY(pointerIndex);

                // Calculate the distance moved
                float dx = 0;
                float dy = 0;

                if (event.getPointerCount() == 1) {
                    // Calculate the distance moved
                    dx = x - mLastTouch.x;
                    dy = y - mLastTouch.y;

                    // Remember this touch position for the next move event
                    mLastTouch = new PointF(x, y);
                } else if (event.getPointerCount() == 2) {
                    // Calculate the distance moved
                    float focusX = mScaleDetector.getFocusX();
                    float focusY = mScaleDetector.getFocusY();
                    dx = focusX - mLastFocus.x;
                    dy = focusY - mLastFocus.y;

                    // Since we are accumating translation/scaling, we are just adding to
                    // the previous scale.
                    matrix.postScale(scaleFactor/mLastScaleFactor, scaleFactor/mLastScaleFactor, focusX, focusY);
                    mLastScaleFactor = scaleFactor;

                    mLastFocus = new PointF(focusX, focusY);
                }

                // Translation is cumulative.
                matrix.postTranslate(dx, dy);

                break;
            }

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL: {
                mActivePointerId = INVALID_POINTER_ID;
                break;
            }

            case MotionEvent.ACTION_POINTER_UP: {
                final int pointerIndex = event.getActionIndex();
                final int pointerId = event.getPointerId(pointerIndex);

                if (pointerId == mActivePointerId) {
                    // This was our active pointer going up. Choose a new
                    // active pointer and adjust accordingly.
                    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                    mLastTouch = new PointF(event.getX(newPointerIndex), event.getY(newPointerIndex));
                    mActivePointerId = event.getPointerId(newPointerIndex);
                } else {
                    final int tempPointerIndex = event.findPointerIndex(mActivePointerId);
                    mLastTouch = new PointF(event.getX(tempPointerIndex), event.getY(tempPointerIndex));
                }

                break;
            }
        }

        invalidate();
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.save();

        canvas.setMatrix(matrix);
        canvas.drawColor(Color.BLACK);
        canvas.drawBitmap(bitmapLayout, 0, 0, paint);

        canvas.restore();
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            scaleFactor *= detector.getScaleFactor();
            scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));

            return true;
        }
    }
}