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 пальца. В этом случае вы всегда будете прыгать.