正确设置图像缩放的焦点

发布于 2025-01-05 05:50:36 字数 4705 浏览 6 评论 0原文

我有一个 SurfaceView ,它负责绘制一个位图作为背景,另一个将用作叠加层。因此,我决定使用可用于两个位图的 Matrix 进行所有转换,因为(我认为)这是不使用 OpenGL 的最快方法之一。

我已经能够实现平移和缩放,但我遇到了一些问题:

  • 我无法找到一种方法来关注两者的中心 缩放时手指移动,图像始终重置为初始状态 (即,没有平移或缩放)在新比例之前 应用。除了看起来错误之外,这还不允许用户缩放 向外查看整个图像,然后放大显示的部分 重要的。
  • 缩放操作后图像将不再是 新抽奖通道后的同一位置,因为平移值将 有所不同。

有没有办法使用矩阵来实现这一点,或者还有其他解决方案?

代码如下(我在单独的线程中使用 SurfaceHolder 来锁定 SurfaceView 画布并调用其 doDraw 方法):

public class MapSurfaceView extends SurfaceView implements SurfaceHolder.Callback {

    public void doDraw(Canvas canvas) {
        canvas.drawColor(Color.BLACK);
        canvas.drawBitmap(mBitmap, mTransformationMatrix, mPaintAA);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_POINTER_DOWN: {
                if (event.getPointerCount() == 2) {
                    mOriginalDistance = MathUtils.distanceBetween(event.getX(0), event.getX(1), event.getY(0), event.getY(1));
                    mScreenMidpoint = MathUtils.midpoint(event.getX(0), event.getX(1), event.getY(0), event.getY(1));
                    mImageMidpoint = MathUtils.midpoint((mXPosition+event.getX(0))/mScale, (mXPosition+event.getX(1))/mScale, (mYPosition+event.getY(0))/mScale, (mYPosition+event.getY(1))/mScale);
                    mOriginalScale = mScale;
                }
            }
            case MotionEvent.ACTION_DOWN: {
                mOriginalTouchPoint = new Point((int)event.getX(), (int)event.getY());
                mOriginalPosition = new Point(mXPosition, mYPosition);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if (event.getPointerCount() == 2) {
                    final double currentDistance = MathUtils.distanceBetween(event.getX(0), event.getX(1), event.getY(0), event.getY(1));
                    if (mIsZooming || currentDistance - mOriginalDistance > mPinchToZoomTolerance || mOriginalDistance - currentDistance > mPinchToZoomTolerance) {
                        final float distanceRatio = (float) (currentDistance / mOriginalDistance);
                        float tempZoom = mOriginalScale * distanceRatio;

                        mScale = Math.min(10, Math.max(Math.min((float)getHeight()/(float)mBitmap.getHeight(), (float)getWidth()/(float)mBitmap.getWidth()), tempZoom));
                        mScale = (float) MathUtils.roundToDecimals(mScale, 1);
                        mIsZooming = true;
                        mTransformationMatrix = new Matrix();
                        mTransformationMatrix.setScale(mScale, mScale);//, mImageMidpoint.x, mImageMidpoint.y);
                    } else {
                        System.out.println("Dragging");
                        mIsZooming = false;
                        final int deltaX = (int) ((int) (mOriginalTouchPoint.x - event.getX()));
                        final int deltaY = (int) ((int) (mOriginalTouchPoint.y - event.getY()));
                        mXPosition = mOriginalPosition.x + deltaX;  
                        mYPosition = mOriginalPosition.y + deltaY;
                        validatePositions();
                        mTransformationMatrix = new Matrix();
                        mTransformationMatrix.setScale(mScale, mScale);
                        mTransformationMatrix.postTranslate(-mXPosition, -mYPosition);
                    }
                }
                break;
            }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP: {
                mIsZooming = false;
                validatePositions();
                mTransformationMatrix = new Matrix();
                mTransformationMatrix.setScale(mScale, mScale);
                mTransformationMatrix.postTranslate(-mXPosition, -mYPosition);
            }
        }
        return true;
    }

    private void validatePositions() {
        // Lower right corner
        mXPosition = Math.min(mXPosition, (int)((mBitmap.getWidth() * mScale)-getWidth()));
        mYPosition = Math.min(mYPosition, (int)((mBitmap.getHeight() * mScale)-getHeight()));
        // Upper left corner
        mXPosition = Math.max(mXPosition, 0);
        mYPosition = Math.max(mYPosition, 0);
        // Image smaller than the container, should center it
        if (mBitmap.getWidth() * mScale <= getWidth()) {
            mXPosition = (int) -((getWidth() - (mBitmap.getWidth() * mScale))/2);
        }
        if (mBitmap.getHeight() * mScale <= getHeight()) { 
            mYPosition = (int) -((getHeight() - (mBitmap.getHeight() * mScale))/2);
        }
    }
}

I have a SurfaceView that is resposible for drawing a Bitmap as a background and another one that will be used as an overlay. So I've decided to do all transformations using a Matrix that can be used for both bitmaps as it is (I think) one of the fastest ways to do it without using OpenGL.

I've been able to implement panning around and zooming but I have some problems with what I've came with:

  • I wasn't able to find a way how to focus on the center of the two
    fingers while zooming, the image always resets to its initial state
    (that is, without panning nor scalling) before the new scale being
    applied. Besides looking wrong, that doesn't allow the user to zoom
    out to see the whole image and then zoom in on the part that is
    important.
  • After the scalling operation the image won't be at the
    same place after the new draw pass because the translation value will
    be different.

Is there a way to achieve that using a Matrix or is there another solution?

Code is below (I use a SurfaceHolder in a separate thread do lock the SurfaceView canvas and call its doDraw method):

public class MapSurfaceView extends SurfaceView implements SurfaceHolder.Callback {

    public void doDraw(Canvas canvas) {
        canvas.drawColor(Color.BLACK);
        canvas.drawBitmap(mBitmap, mTransformationMatrix, mPaintAA);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_POINTER_DOWN: {
                if (event.getPointerCount() == 2) {
                    mOriginalDistance = MathUtils.distanceBetween(event.getX(0), event.getX(1), event.getY(0), event.getY(1));
                    mScreenMidpoint = MathUtils.midpoint(event.getX(0), event.getX(1), event.getY(0), event.getY(1));
                    mImageMidpoint = MathUtils.midpoint((mXPosition+event.getX(0))/mScale, (mXPosition+event.getX(1))/mScale, (mYPosition+event.getY(0))/mScale, (mYPosition+event.getY(1))/mScale);
                    mOriginalScale = mScale;
                }
            }
            case MotionEvent.ACTION_DOWN: {
                mOriginalTouchPoint = new Point((int)event.getX(), (int)event.getY());
                mOriginalPosition = new Point(mXPosition, mYPosition);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if (event.getPointerCount() == 2) {
                    final double currentDistance = MathUtils.distanceBetween(event.getX(0), event.getX(1), event.getY(0), event.getY(1));
                    if (mIsZooming || currentDistance - mOriginalDistance > mPinchToZoomTolerance || mOriginalDistance - currentDistance > mPinchToZoomTolerance) {
                        final float distanceRatio = (float) (currentDistance / mOriginalDistance);
                        float tempZoom = mOriginalScale * distanceRatio;

                        mScale = Math.min(10, Math.max(Math.min((float)getHeight()/(float)mBitmap.getHeight(), (float)getWidth()/(float)mBitmap.getWidth()), tempZoom));
                        mScale = (float) MathUtils.roundToDecimals(mScale, 1);
                        mIsZooming = true;
                        mTransformationMatrix = new Matrix();
                        mTransformationMatrix.setScale(mScale, mScale);//, mImageMidpoint.x, mImageMidpoint.y);
                    } else {
                        System.out.println("Dragging");
                        mIsZooming = false;
                        final int deltaX = (int) ((int) (mOriginalTouchPoint.x - event.getX()));
                        final int deltaY = (int) ((int) (mOriginalTouchPoint.y - event.getY()));
                        mXPosition = mOriginalPosition.x + deltaX;  
                        mYPosition = mOriginalPosition.y + deltaY;
                        validatePositions();
                        mTransformationMatrix = new Matrix();
                        mTransformationMatrix.setScale(mScale, mScale);
                        mTransformationMatrix.postTranslate(-mXPosition, -mYPosition);
                    }
                }
                break;
            }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP: {
                mIsZooming = false;
                validatePositions();
                mTransformationMatrix = new Matrix();
                mTransformationMatrix.setScale(mScale, mScale);
                mTransformationMatrix.postTranslate(-mXPosition, -mYPosition);
            }
        }
        return true;
    }

    private void validatePositions() {
        // Lower right corner
        mXPosition = Math.min(mXPosition, (int)((mBitmap.getWidth() * mScale)-getWidth()));
        mYPosition = Math.min(mYPosition, (int)((mBitmap.getHeight() * mScale)-getHeight()));
        // Upper left corner
        mXPosition = Math.max(mXPosition, 0);
        mYPosition = Math.max(mYPosition, 0);
        // Image smaller than the container, should center it
        if (mBitmap.getWidth() * mScale <= getWidth()) {
            mXPosition = (int) -((getWidth() - (mBitmap.getWidth() * mScale))/2);
        }
        if (mBitmap.getHeight() * mScale <= getHeight()) { 
            mYPosition = (int) -((getHeight() - (mBitmap.getHeight() * mScale))/2);
        }
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

坐在坟头思考人生 2025-01-12 05:50:36

不要每次使用 new Matrix() 重置变换矩阵,而是尝试使用 post*() 更新它。这样,您只需执行与屏幕相关的操作。用以下术语思考更容易:“缩放到屏幕上的这一点”。

现在一些代码。在缩放部分计算了 mScale:

...
mScale = (float) MathUtils.roundToDecimals(mScale, 1);
float ratio = mScale / mOriginalScale;
mTransformationMatrix.postScale(ratio, ratio, mScreenMidpoint.x, mScreenMidpoint.y);

在每个缩放触摸事件上重新计算 mScreenMidpoint 可能会更好。这将允许用户在缩放时稍微改变焦点。对我来说,这比在第一次两根手指触摸后冻结焦点更自然。

在拖动过程中,您使用 deltaX 和 deltaY 而不是绝对点进行平移:

mTransformationMatrix.postTranslate(-deltaX, -deltaY);

当然,现在您必须更改 validatePositions() 方法以:

  • 确保 deltaX 和 deltaY 不会使图像移动太多,或者
  • 使用变换矩阵来检查图像是否关闭屏幕,然后移动它以对抗

我将描述第二种方法,因为它更灵活并且还允许验证缩放。

我们计算有多少图像离开屏幕,然后使用这些值移动它:

void validate() {

    mTransformationMatrix.mapRect(new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()));

    float height = rect.height();
    float width = rect.width();

    float deltaX = 0, deltaY = 0;

    // Vertical delta
    if (height < mScreenHeight) {
        deltaY = (mScreenHeight - height) / 2 - rect.top;
    } else if (rect.top > 0) {
        deltaY = -rect.top;
    } else if (rect.bottom < mScreenHeight) {
        deltaY = mScreenHeight - rect.bottom;
    }

    // Horziontal delta
    if (width < mScreenWidth) {
        deltaX = (mScreenWidth - width) / 2 - rect.left;
    } else if (rect.left > 0) {
        deltaX = -rect.left;
    } else if (rect.right < mScreenWidth) {
        deltaX = mScreenWidth - rect.right;
    }

    mTransformationMatrix.postTranslate(deltaX, deltaY)
}

Instead of resetting the transformation matrix every time using new Matrix(), try updating it using post*(). This way, you do only operations relative to the screen. It is easier to think in terms: "zoom to this point on the screen".

Now some code. Having calculated mScale in zooming part:

...
mScale = (float) MathUtils.roundToDecimals(mScale, 1);
float ratio = mScale / mOriginalScale;
mTransformationMatrix.postScale(ratio, ratio, mScreenMidpoint.x, mScreenMidpoint.y);

It might be even better to recalculate mScreenMidpoint on each zooming touch event. This would allow user to change the focus point a bit while zooming. For me, it is more natural than having the focus point frozen after first two finger touch.

During dragging, you translate using deltaX and deltaY instead of absolute points:

mTransformationMatrix.postTranslate(-deltaX, -deltaY);

Of course now you have to change your validatePositions() method to:

  • ensure deltaX and deltaY do not make image move too much, or
  • use transformation matrix to check if image is off screen and then move it to counter that

I will describe the second method, as it is more flexible and allows to validate zooming as well.

We calculate how much image is off screen and then move it using those values:

void validate() {

    mTransformationMatrix.mapRect(new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()));

    float height = rect.height();
    float width = rect.width();

    float deltaX = 0, deltaY = 0;

    // Vertical delta
    if (height < mScreenHeight) {
        deltaY = (mScreenHeight - height) / 2 - rect.top;
    } else if (rect.top > 0) {
        deltaY = -rect.top;
    } else if (rect.bottom < mScreenHeight) {
        deltaY = mScreenHeight - rect.bottom;
    }

    // Horziontal delta
    if (width < mScreenWidth) {
        deltaX = (mScreenWidth - width) / 2 - rect.left;
    } else if (rect.left > 0) {
        deltaX = -rect.left;
    } else if (rect.right < mScreenWidth) {
        deltaX = mScreenWidth - rect.right;
    }

    mTransformationMatrix.postTranslate(deltaX, deltaY)
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文