如何在自定义视图中修复捏缩放焦点?
How to fix pinch zoom focal point in a custom view?
对于我的问题,我已经在 Github 准备了 a very simple test app。
为简单起见,我删除了投掷、滚动约束和边缘效果(它们在我的真实应用中运行良好):
所以 the custom view 在我的测试应用中只支持滚动:
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;
}
});
并用 2 根手指捏合缩放(虽然焦点被打破了!):
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
并且 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
的解决方案,因为我相信在上述计算中确实遗漏了一些非常小的东西。是的,我已经研究了很多可比较的代码,包括 Google.
的 PhotoView by Chris Banes and the InteractiveChart 示例
双击更新:
pskink 的基于 Matrix
的解决方案效果很好,但是我仍然有一个错误焦点的问题 -
我已尝试将代码添加到 the custom view 以在双击手势时将缩放比例增加 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
调用。
例如,当我在屏幕中间双击时,结果会向右和向下平移太远:
使用 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;
}
}
对于我的问题,我已经在 Github 准备了 a very simple test app。
为简单起见,我删除了投掷、滚动约束和边缘效果(它们在我的真实应用中运行良好):
所以 the custom view 在我的测试应用中只支持滚动:
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;
}
});
并用 2 根手指捏合缩放(虽然焦点被打破了!):
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
并且 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
的解决方案,因为我相信在上述计算中确实遗漏了一些非常小的东西。是的,我已经研究了很多可比较的代码,包括 Google.
双击更新:
pskink 的基于 Matrix
的解决方案效果很好,但是我仍然有一个错误焦点的问题 -
我已尝试将代码添加到 the custom view 以在双击手势时将缩放比例增加 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
调用。
例如,当我在屏幕中间双击时,结果会向右和向下平移太远:
使用 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;
}
}