如何在自定义视图中修复捏合缩放的焦点?

9

对于我的问题,我在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值来平移它,以便焦点在游戏板坐标中保持相同的位置。因此,我的计算是 -
该点的位置为:
-mBoardScrollX + focusX * mBoardScale
-mBoardScrollY + focusY * mBoardScale

新位置将会在此处:

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

通过解这两个线性方程,我得到:
mBoardScrollX = mBoardScrollX + focusX * (1 - factor) * mBoardScale;
mBoardScrollY = mBoardScrollY + focusY * (1 - factor) * mBoardScale;

然而,那并不起作用!

为了消除任何错误,我甚至尝试将焦点硬编码到我的自定义视图的中心 - 但仍然在缩放时游戏板的中心晃动:

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

我认为我错过了一些微小的东西,请帮助我。

我希望找到一个不使用 Matrix 的解决方案,因为我相信上述计算中确实缺少了一些微小的东西。是的,我已经学习了很多类似的代码,包括 Chris Banes 的 PhotoView 和 Google 的 InteractiveChart 示例。

双击更新:

pskink 提供的基于 Matrix 的解决方案非常好,但是我仍然有一个问题,那就是聚焦点错误 -

我尝试向自定义视图添加代码,在双击手势上将缩放增加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
在我看来,数学太多了。Matrix 会更好,可以看看 Canvas.concat 方法,它展示了如何轻松地做一些漂亮的事情,而不需要那么多的数学计算和 Canvas.scale / Canvas.translate / Canvas.rotate 调用。顺便说一句,界面看起来很不错;-) - pskink
1
例如,这个代码片段是一个不错的例子 - 从getMainView方法中返回一些ImageView以查看它的效果。 - pskink
感谢您的建议(+1),我会在周末再次尝试这个方法(之前尝试失败了)。此外,我有一种感觉,在非“Matrix”方法中只缺少了一个小东西,我很好奇我的简单测试应用程序出了什么问题。 - Alexander Farber
是的,你的代码更简单且完美地运行着。你想提交它作为答案,这样我就可以授予你赏金吗?不过我仍有一个开放式问题:如何限制某些缩放和滚动限制?我应该使用mMatrix.getValues()吗?我想知道在onRestoreInstanceState中是否应该保存所有9个浮点值,或者更少的值是否足够? - Alexander Farber
1
不,不,不,ValueAnimator 很好用,只需使用 scaleFactor = newScale / oldScale 就可以了,但是好吧,也许我会使用 ObjectAnimator - 这样可以减少样板代码。 - pskink
显示剩余4条评论
1个回答

7

使用Matrix是一个更好的想法 - 代码更简单,你不必证明你的数学技能 ;-),看看如何使用Matrix#postTranslateMatrix#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;
    }
}

谢谢,这个完美地运行了。请问您有关于强制缩放/滚动限制的建议吗?是否有一些矩阵技巧可以实现呢?(编辑:感谢mapRect的建议)另外,您如何在双击时实现缩放?我应该使用类似Zoomer这样的东西还是有更简单的方法? - Alexander Farber
1
简单的 ValueAnimator 就可以实现这个功能(如果你想要平滑的缩放效果),只需要双击即可。 - pskink
早上好@pskink,你能否再看一下我的更新问题?当我尝试实现双击缩放并在ValueAnimator监听器中重复调用mMatrix.setScale(scale, scale, e.getX(), e.getY());时,焦点再次出现错误。 - Alexander Farber
1
setScale 清除了翻译,应使用 postScale 代替。 - pskink
1
是的:如果你想在10个步骤内从1扩展到2,那么第一次需要使用1.1/1.0,第二次需要使用1.2/1.1,第三次需要使用1.3/1.2等等——或者至少看起来是这样... 这是你可以发挥你的数学技能的地方;-) - pskink
显示剩余2条评论

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