Как исправить фокусную точку масштабирования в пользовательском представлении? - PullRequest
8 голосов
/ 20 марта 2019

Для моего вопроса я подготовил очень простое тестовое приложение на Github.

Для простоты я удалил бросание, ограничения прокрутки и эффекты краев (которые на самом деле хорошо работают в моем реальном приложении):

test app screenshot

Итак пользовательское представление в моем тестовом приложении поддерживает только прокрутку:

mGestureDetector = new GestureDetector(context,
        new GestureDetector.SimpleOnGestureListener() {
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float dX, float dY) {
        mBoardScrollX -= dX;
        mBoardScrollY -= dY;

        ViewCompat.postInvalidateOnAnimation(MyView.this);
        return true;
    }
});

и ущипните масштабирование двумя пальцами (хотя фокус нарушен!):

mScaleDetector = new ScaleGestureDetector(context,
        new ScaleGestureDetector.SimpleOnScaleGestureListener() {
    @Override
    public boolean onScale(ScaleGestureDetector scaleDetector) {
        float focusX = scaleDetector.getFocusX();
        float focusY = scaleDetector.getFocusY();
        float factor = scaleDetector.getScaleFactor();

        mBoardScrollX = mBoardScrollX + focusX * (1 - factor) * mBoardScale;
        mBoardScrollY = mBoardScrollY + focusY * (1 - factor) * mBoardScale;
        mBoardScale *= factor;

        ViewCompat.postInvalidateOnAnimation(MyView.this);
        return true;
    }
});

Наконец, вот код, рисующий масштабированное и смещенное игровое поле Drawable:

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

    canvas.save();
    canvas.scale(mBoardScale, mBoardScale);
    canvas.translate(mBoardScrollX / mBoardScale, mBoardScrollY / mBoardScale);
    mBoard.draw(canvas);
    canvas.restore();
}

ЕслиПопробовав запустить мое приложение, вы заметите, что при масштабировании игрового поля жестом масштабирования двумя пальцами работает фокусировка масштабирования.

Насколько я понимаю, при масштабировании Drawable мне нужно панорамироватьнастраивая значения mBoardScrollX и mBoardScrollY, чтобы точка фокусировки оставалась в той же точке в координатах игрового поля.Итак, мой расчет -

Позиция old этой точки:

-mBoardScrollX + focusX * mBoardScale
-mBoardScrollY + focusY * mBoardScale

, а позиция new будет в:

-mBoardScrollX + focusX * mBoardScale * factor
-mBoardScrollY + focusY * mBoardScale * factor

Решая эти 2 линейных уравнения, я получаю:

mBoardScrollX = mBoardScrollX + focusX * (1 - factor) * mBoardScale;
mBoardScrollY = mBoardScrollY + focusY * (1 - factor) * mBoardScale;

Однако это не работает!

Чтобы устранить любые ошибки, я даже пытался жестко кодировать фокуссередина моего собственного представления - и все же центр игрового поля покачивается при масштабировании:

float focusX = getWidth() / 2f;
float focusY = getHeight() / 2f;

Я думаю, что упускаю что-то незначительное, пожалуйста, помогите мне.

Я бы предпочел найтирешение без использования Matrix, потому что я считаю, что что-то действительно незначительное отсутствует в приведенных выше расчетах.И да, я уже изучил много сравнимого кода, включая PhotoView Криса Бейнса и пример InteractiveChart от Google.

DOUBLE TAP UPDATE:

Решение на основе Matrix от pskink работает очень хорошо, однако у меня все еще есть одна проблема с неправильным фокусом -

Я пытался добавить код к пользовательский вид для увеличения зума на 100% при жесте двойного касания:

public boolean onDoubleTap(final MotionEvent e) {
    float[] values = new float[9];
    mMatrix.getValues(values);
    float scale = values[Matrix.MSCALE_X];
    ValueAnimator animator = ValueAnimator.ofFloat(scale, 2f * scale);
    animator.setDuration(3000);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animator){
            float scale = (float) animator.getAnimatedValue();
            mMatrix.setScale(scale, scale, e.getX(), e.getY());
            ViewCompat.postInvalidateOnAnimation(MyView.this);
        }
    });
    animator.start();
    return true;
}

И хотя зум изменяется правильно, фокус снова становится неправильным - даже если координаты фокусировки пройденына каждый setScale вызов.

Например, когда я дважды нажимаю на середину экрана, результат будет панорамироваться слишком далеко вправо и вниз:

emulator screenshot

1 Ответ

3 голосов
/ 22 марта 2019

Использование Matrix - это действительно лучшая идея - код намного проще, и вам не нужно доказывать свои математические навыки ;-), посмотрите, как используются методы Matrix#postTranslate и Matrix#postScale:

class MyView extends View {
    private static final String TAG = "MyView";

    private final ScaleGestureDetector mScaleDetector;
    private final GestureDetector mGestureDetector;

    private final Drawable mBoard;
    private final float mBoardWidth;
    private final float mBoardHeight;
    private Matrix mMatrix;

    public MyView(Context context) {
        this(context, null, 0);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        mBoard = ResourcesCompat.getDrawable(context.getResources(), R.drawable.chrome, null);
        mBoardWidth = mBoard.getIntrinsicWidth();
        mBoardHeight = mBoard.getIntrinsicHeight();
        mBoard.setBounds(0, 0, (int) mBoardWidth, (int) mBoardHeight);

        mMatrix = new Matrix();

        mScaleDetector = new ScaleGestureDetector(context, scaleListener);
        mGestureDetector = new GestureDetector(context, listener);
    }

    ScaleGestureDetector.OnScaleGestureListener scaleListener = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
        @Override
        public boolean onScale(ScaleGestureDetector scaleDetector) {
            float factor = scaleDetector.getScaleFactor();
            mMatrix.postScale(factor, factor, getWidth() / 2f, getHeight() / 2f);
            ViewCompat.postInvalidateOnAnimation(MyView.this);
            return true;
        }
    };

    GestureDetector.OnGestureListener listener = new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float dX, float dY) {
            mMatrix.postTranslate(-dX, -dY);
            ViewCompat.postInvalidateOnAnimation(MyView.this);
            return true;
        }
    };

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        float scale = Math.max(w / mBoardWidth, h / mBoardHeight);
        mMatrix.setScale(scale, scale);
        mMatrix.postTranslate((w - scale * mBoardWidth) / 2f, (h - scale * mBoardHeight) / 2f);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.save();
        canvas.concat(mMatrix);
        mBoard.draw(canvas);
        canvas.restore();
    }

    @Override
    @SuppressLint("ClickableViewAccessibility")
    public boolean onTouchEvent(MotionEvent e) {
        mGestureDetector.onTouchEvent(e);
        mScaleDetector.onTouchEvent(e);
        return true;
    }
}
...