Zoom и Panning ImageView Android

Я создал класс масштабирования и панорамирования для ImageView.

Возможности, которые я пытаюсь создать. - Это кастрюли с одним касанием пальца и движением - Он масштабирует и кастрюли двумя касаниями пальцев и движением

По большей части это работает очень хорошо. У меня небольшая ошибка, когда я делаю следующее: - Я обнимаюсь одним пальцем (Status: No problem) - Я положил второй палец, масштабирование и панорамирование (Status: No problem) - Я отпускаю второй палец (Состояние: изображение немного подпрыгивает)

Было надеяться, что кто-то поможет мне решить эту проблему.

Я думаю, что это должно быть связано с перезагрузкой mLastTouchX и mLastTouchY в "case MotionEvent.ACTION_POINTER_UP"

Любая помощь будет очень признательна!

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.widget.ImageView;

public class MyImageView extends ImageView {

    private static final int INVALID_POINTER_ID = -1;

    private float mPosX;
    private float mPosY;

    private float mLastTouchX;
    private float mLastTouchY;
    private float mLastGestureX;
    private float mLastGestureY;
    private int mActivePointerId = INVALID_POINTER_ID;

    private ScaleGestureDetector mScaleDetector;
    private float mScaleFactor = 1.f;

    public MyImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
    }

    public MyImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // Let the ScaleGestureDetector inspect all events.
        mScaleDetector.onTouchEvent(ev);

        final int action = ev.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: {
                if (!mScaleDetector.isInProgress()) {
                    final float x = ev.getX();
                    final float y = ev.getY();

                    mLastTouchX = x;
                    mLastTouchY = y;
                    mActivePointerId = ev.getPointerId(0);
                }
                break;
            }
            case MotionEvent.ACTION_POINTER_1_DOWN: {
                if (mScaleDetector.isInProgress()) {
                    final float gx = mScaleDetector.getFocusX();
                    final float gy = mScaleDetector.getFocusY();
                    mLastGestureX = gx;
                    mLastGestureY = gy;
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {

                // Only move if the ScaleGestureDetector isn't processing a gesture.
                if (!mScaleDetector.isInProgress()) {
                    final int pointerIndex = ev.findPointerIndex(mActivePointerId);
                    final float x = ev.getX(pointerIndex);
                    final float y = ev.getY(pointerIndex);

                    final float dx = x - mLastTouchX;
                    final float dy = y - mLastTouchY;

                    mPosX += dx;
                    mPosY += dy;

                    invalidate();

                    mLastTouchX = x;
                    mLastTouchY = y;
                }
                else{
                    final float gx = mScaleDetector.getFocusX();
                    final float gy = mScaleDetector.getFocusY();

                    final float gdx = gx - mLastGestureX;
                    final float gdy = gy - mLastGestureY;

                    mPosX += gdx;
                    mPosY += gdy;

                    invalidate();

                    mLastGestureX = gx;
                    mLastGestureY = gy;
                }

                break;
            }
            case MotionEvent.ACTION_UP: {
                mActivePointerId = INVALID_POINTER_ID;
                break;
            }
            case MotionEvent.ACTION_CANCEL: {
                mActivePointerId = INVALID_POINTER_ID;
                break;
            }
            case MotionEvent.ACTION_POINTER_UP: {
                final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) 
                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                final int pointerId = ev.getPointerId(pointerIndex);
                if (pointerId == mActivePointerId) {
                    Log.d("DEBUG", "mActivePointerId");
                    // This was our active pointer going up. Choose a new
                    // active pointer and adjust accordingly.
                    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                    mLastTouchX = ev.getX(newPointerIndex);
                    mLastTouchY = ev.getY(newPointerIndex);
                    mActivePointerId = ev.getPointerId(newPointerIndex);
                }

                break;
            }
        }

        return true;
    }

    @Override
    public void onDraw(Canvas canvas) {

        canvas.save();

        canvas.translate(mPosX, mPosY);

        if (mScaleDetector.isInProgress()) {
            canvas.scale(mScaleFactor, mScaleFactor, mScaleDetector.getFocusX(), mScaleDetector.getFocusY());
        }
        else{
            canvas.scale(mScaleFactor, mScaleFactor);
        }
        super.onDraw(canvas);
        canvas.restore();
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            mScaleFactor *= detector.getScaleFactor();

            // Don't let the object get too small or too large.
            mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 10.0f));

            invalidate();
            return true;
        }
    }
}

Ответ 1

Кажется, canvas.scale() в инструкции 'else' метода 'onDraw' требовал, чтобы mLastGestureX и mLastGestureY прекратили прыжки. Я также обновляю mLastTouchX и mLastTouchY, возвращаясь к одностороннему панорамированию в "case MotionEvent.ACTION_POINTER_UP"

Здесь окончательный, может не устраивать всех, потому что это не ограничивает панорамирование границ изображений, но это должно быть легко сделать, есть много дискуссий по этой теме.

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.widget.ImageView;

public class MyImageView extends ImageView {

    private static final int INVALID_POINTER_ID = -1;

    private float mPosX;
    private float mPosY;

    private float mLastTouchX;
    private float mLastTouchY;
    private float mLastGestureX;
    private float mLastGestureY;
    private int mActivePointerId = INVALID_POINTER_ID;

    private ScaleGestureDetector mScaleDetector;
    private float mScaleFactor = 1.f;

    public MyImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
    }

    public MyImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // Let the ScaleGestureDetector inspect all events.
        mScaleDetector.onTouchEvent(ev);

        final int action = ev.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: {
                if (!mScaleDetector.isInProgress()) {
                    final float x = ev.getX();
                    final float y = ev.getY();

                    mLastTouchX = x;
                    mLastTouchY = y;
                    mActivePointerId = ev.getPointerId(0);
                }
                break;
            }
            case MotionEvent.ACTION_POINTER_1_DOWN: {
                if (mScaleDetector.isInProgress()) {
                    final float gx = mScaleDetector.getFocusX();
                    final float gy = mScaleDetector.getFocusY();
                    mLastGestureX = gx;
                    mLastGestureY = gy;
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {

                // Only move if the ScaleGestureDetector isn't processing a gesture.
                if (!mScaleDetector.isInProgress()) {
                    final int pointerIndex = ev.findPointerIndex(mActivePointerId);
                    final float x = ev.getX(pointerIndex);
                    final float y = ev.getY(pointerIndex);

                    final float dx = x - mLastTouchX;
                    final float dy = y - mLastTouchY;

                    mPosX += dx;
                    mPosY += dy;

                    invalidate();

                    mLastTouchX = x;
                    mLastTouchY = y;
                }
                else{
                    final float gx = mScaleDetector.getFocusX();
                    final float gy = mScaleDetector.getFocusY();

                    final float gdx = gx - mLastGestureX;
                    final float gdy = gy - mLastGestureY;

                    mPosX += gdx;
                    mPosY += gdy;

                    invalidate();

                    mLastGestureX = gx;
                    mLastGestureY = gy;
                }

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

                final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) 
                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                final int pointerId = ev.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;
                    mLastTouchX = ev.getX(newPointerIndex);
                    mLastTouchY = ev.getY(newPointerIndex);
                    mActivePointerId = ev.getPointerId(newPointerIndex);
                }
                else{
                    final int tempPointerIndex = ev.findPointerIndex(mActivePointerId);
                    mLastTouchX = ev.getX(tempPointerIndex);
                    mLastTouchY = ev.getY(tempPointerIndex);
                }

                break;
            }
        }

        return true;
    }

    @Override
    public void onDraw(Canvas canvas) {

        canvas.save();

        canvas.translate(mPosX, mPosY);

        if (mScaleDetector.isInProgress()) {
            canvas.scale(mScaleFactor, mScaleFactor, mScaleDetector.getFocusX(), mScaleDetector.getFocusY());
        }
        else{
            canvas.scale(mScaleFactor, mScaleFactor, mLastGestureX, mLastGestureY);
        }
        super.onDraw(canvas);
        canvas.restore();
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            mScaleFactor *= detector.getScaleFactor();

            // Don't let the object get too small or too large.
            mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 10.0f));

            invalidate();
            return true;
        }
    }
}

Ответ 2

Я работаю над решением этой проблемы уже более недели, и это дает мне массу проблем. Тем не менее, я сузил проблему. Ваше решение выше НЕ работало для меня, но мое решение ниже близко. Проблема в том, что он прыгает всякий раз, когда второй палец нажат или поднят. Я обнаружил, что это происходит потому, что mPosX и mPosY не всегда являются действительно тем, что должны представлять переменные. Вот что я имею в виду:

Когда вызывается ACTION_MOVE и код вводится в оператор else (для обработки событий масштабирования), mPosX и mPosY изменяются только в соответствии с изменением фокуса, а не с изменением с масштабированием. Это означает, что работает панорамирование с двумя пальцами, а увеличение с двумя пальцами работает, но mPosX и mPosY не изменяются соответствующим образом относительно изменяющегося увеличения.

Я пытался выяснить, как это исправить, используя дифференциальные изменения в масштабировании (mScaleDetector.getScaleFactor()) и дифференциальные изменения в фокусе, но я не могу работать с помощью логики достаточно хорошо, чтобы найти что-то работает.

Другое решение - переместить все операции масштабирования в OnTouchListener и полностью избавиться от ScaleListener. Это означает гораздо больше математики, но это определенно будет решением.

Здесь onDraw:

    @Override
    public void onDraw(Canvas c) {
        c.save();

        if (mScaleDetector.isInProgress()) {
            c.scale(mScaleFactor, mScaleFactor, mLastGestureX - mPosX,
                    mLastGestureY - mPosY);
        } else {
            c.scale(mScaleFactor, mScaleFactor, mLastGestureX, mLastGestureY);
        }

        c.translate(mPosX / mScaleFactor, mPosY / mScaleFactor);

        // drawing instruction here

        c.restore();
    }

Вот как код реагирует на нажатия пальцев:

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

        final int action = ev.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN: {
            if (!mScaleDetector.isInProgress()) {
                final float x = ev.getX();
                final float y = ev.getY();

                mLastTouchX = x;
                mLastTouchY = y;

                mActivePointerId = ev.getPointerId(0);
            }
            break;
        }

        case MotionEvent.ACTION_POINTER_DOWN: {
            if (!mScaleDetector.isInProgress()) {
                final float gx = mScaleDetector.getFocusX();
                final float gy = mScaleDetector.getFocusY();

                mLastGestureX = gx;
                mLastGestureY = gy; 
            }
            break;
        }

        case MotionEvent.ACTION_MOVE: {
            if (!mScaleDetector.isInProgress()) {
                Log.i("hi", "SD not in progress");
                final int pointerIndex = ev.findPointerIndex(mActivePointerId);
                final float x = ev.getX(pointerIndex);
                final float y = ev.getY(pointerIndex);

                final float dx = x - mLastTouchX;
                final float dy = y - mLastTouchY;

                mPosX += dx;
                mPosY += dy;

                invalidate();

                mLastTouchX = x;
                mLastTouchY = y;
            } else {
                Log.i("hi", "SD in progress");
                final float gx = mScaleDetector.getFocusX();
                final float gy = mScaleDetector.getFocusY();

                final float gdx = gx - mLastGestureX;
                final float gdy = gy - mLastGestureY;

                mPosX += gdx;
                mPosY += gdy;

                // SOMETHING NEEDS TO HAPPEN RIGHT HERE.

                invalidate();

                mLastGestureX = gx;
                mLastGestureY = gy;
            }

            break;
        }

        case MotionEvent.ACTION_POINTER_UP: {

            final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
            final int pointerId = ev.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;

                mLastTouchX = ev.getX(newPointerIndex);
                mLastTouchY = ev.getY(newPointerIndex);

                mActivePointerId = ev.getPointerId(newPointerIndex);
            } else {
                final int tempPointerIndex = ev.findPointerIndex(mActivePointerId);

                mLastTouchX = ev.getX(tempPointerIndex);
                mLastTouchY = ev.getY(tempPointerIndex);
            }

            break;
        }
        }

        return true;
    }

И хотя он в основном не связан, здесь ScaleListener:

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            mScaleFactor *= detector.getScaleFactor();

            invalidate();
            return true;
        }
    }

Опять же, этот код НЕ работает отлично, но он очень близок. Я объяснил точный вопрос выше, и у меня все еще есть проблемы с его работой. Я не знаю, будет ли это появляться в ваших уведомлениях, Хэнк, но, надеюсь, кто-то это увидит и поможет мне.

Ответ 3

Решение Хэнка работает для меня. Я добавил функцию reset, чтобы последующие изображения отображались нормально.

public void ResetView() {
        mScaleFactor = 1.f;
        mPosX = 0.f;
        mPosY = 0.f;
    }

Ответ 4

Не смотря на код, я бы предположил, что вы делаете расчет положения на основе 2 пальцев, когда есть 2 пальца. В этом случае вы всегда будете прыгать.