缩放和平移ImageView Android

14
我已经为ImageView创建了一个缩放和平移的类。
我正试图实现以下功能: - 单指触摸和移动时进行平移 - 双指触摸和移动时进行缩放和平移
大部分情况下这个功能非常好用。但是在以下情况下有一个小bug: - 我使用一只手指拖动图片(状态:没有问题) - 我用第二个手指按下去并进行缩放和平移(状态:没有问题) - 我松开第二个手指(状态:图片会跳动一下)
我希望有人能帮助我解决这个问题。
我认为这可能与“MotionEvent.ACTION_POINTER_UP”中重置mLastTouchX和mLastTouchY有关。
非常感谢您的帮助!
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;
        }
    }
}

嗯,我尝试了您的代码,并观察了一下小图像,当我放第二个手指时,图像会“跳到”它。这是您所提到的错误吗? - sandrstar
是的,跳转是我正在尝试解决的问题。 - Hank
看起来这是由于使用焦点坐标引起的。我会尝试稍后今天调试代码。 - sandrstar
顺便问一下,当您放置第二个手指时,期望的行为是什么?似乎对于缩放图像,应该将其放置在两个手指之间。 - sandrstar
如果你抬起一根手指,它就会返回到单指平移模式,但图像不应该跳动。 - Hank
显示剩余2条评论
4个回答

19
似乎'onDraw'方法中'else'语句中的canvas.scale()需要mLastGestureX和mLastGestureY来停止跳跃。在'MotionEvent.ACTION_POINTER_UP'中返回单指平移时,我还刷新了mLastTouchX和mLastTouchY。
这是最终的代码,可能不适合所有人,因为它没有限制图像边界之外的平移,但应该很容易实现,有很多相关讨论。
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;
        }
    }
}

如何限制它不超出ImageView的范围? - Ciper123
将所有内容移入OpenGL,因为它是一个更强大的图形映射器,但我认为如果你找出图像在任何缩放级别下的边界,你可能可以检查并防止平移。 - Hank
当第二个手指放在屏幕上时,它仍会跳动。 - Özgür
嗨@Hank,我正在尝试使用这个,但是上述问题仍然存在(图片跳动)。你有解决方案吗? - schinj
@Hank - 为什么有两个MyImageView构造函数? - Toniq
没有理由,其实也不需要吧。 - Hank

3
我已经尝试解决这个问题一个星期了,但是仍然有很多问题。虽然我已经大大缩小了问题的范围。但是你上面的解决方案对我并没有起作用,我的解决方案接近于你提供的方案。问题在于当第二根手指按下或抬起时,它会跳动。我发现这是因为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;
        }
    }

再次强调,这段代码并不完美运行,但已经非常接近。我已经在上面解释了具体问题,仍然难以让其正常工作。我不知道这个消息是否会在您的通知中出现,Hank,但希望有人能看到它并帮助我。


谢谢,这是对以前代码的改进,但当第二个手指触摸屏幕时仍会出现跳跃。 - Özgür

0
Hank的方案对我有用。我添加了一个重置函数,这样后续的图片就可以正常显示了。
public void ResetView() {
        mScaleFactor = 1.f;
        mPosX = 0.f;
        mPosY = 0.f;
    }

-1

不看代码,我会认为你正在基于两个手指进行位置计算。在这种情况下,你总会得到一个跳跃。


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