Android 两指旋转
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/10682019/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Android Two finger rotation
提问by pretobomba
I am trying to implement two finger rotation in android however, it is not quite working as expected. The goal is to implement rotation like Google Earth does (two-finger rotating the image around the focal point). Currently my rotation listener looks like this:
我正在尝试在 android 中实现两指旋转,但是它并没有像预期的那样工作。目标是像 Google Earth 一样实现旋转(两指围绕焦点旋转图像)。目前我的旋转侦听器如下所示:
private class RotationGestureListener {
private static final int INVALID_POINTER_ID = -1;
private float fX, fY, sX, sY, focalX, focalY;
private int ptrID1, ptrID2;
public RotationGestureListener(){
ptrID1 = INVALID_POINTER_ID;
ptrID2 = INVALID_POINTER_ID;
}
public boolean onTouchEvent(MotionEvent event){
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
sX = event.getX();
sY = event.getY();
ptrID1 = event.getPointerId(0);
break;
case MotionEvent.ACTION_POINTER_DOWN:
fX = event.getX();
fY = event.getY();
focalX = getMidpoint(fX, sX);
focalY = getMidpoint(fY, sY);
ptrID2 = event.getPointerId(event.getActionIndex());
break;
case MotionEvent.ACTION_MOVE:
if(ptrID1 != INVALID_POINTER_ID && ptrID2 != INVALID_POINTER_ID){
float nfX, nfY, nsX, nsY;
nfX = event.getX(event.findPointerIndex(ptrID1));
nfY = event.getY(event.findPointerIndex(ptrID1));
nsX = event.getX(event.findPointerIndex(ptrID2));
nsY = event.getY(event.findPointerIndex(ptrID2));
float angle = angleBtwLines(fX, fY, nfX, nfY, sX, sY, nsX, nsY);
rotateImage(angle, focalX, focalY);
fX = nfX;
fY = nfY;
sX = nfX;
sY = nfY;
}
break;
case MotionEvent.ACTION_UP:
ptrID1 = INVALID_POINTER_ID;
break;
case MotionEvent.ACTION_POINTER_UP:
ptrID2 = INVALID_POINTER_ID;
break;
}
return false;
}
private float getMidpoint(float a, float b){
return (a + b) / 2;
}
private float angleBtwLines (float fx1, float fy1, float fx2, float fy2, float sx1, float sy1, float sx2, float sy2){
float angle1 = (float) Math.atan2(fy1 - fy2, fx1 - fx2);
float angle2 = (float) Math.atan2(sy1 - sy2, sx1 - sx2);
return (float) Math.toDegrees((angle1-angle2));
}
}
However whenever I rotate the angle of rotation is much larger and it sometimes it rotates to the wrong side. Any ideas on how to fix this?
但是,每当我旋转时,旋转角度都会大得多,有时它会旋转到错误的一侧。有想法该怎么解决这个吗?
By the way I am testing it on a Motorola Atrix, so it does not have the touchscreen bug.
顺便说一下,我在摩托罗拉 Atrix 上测试它,所以它没有触摸屏错误。
Thanks
谢谢
回答by leszek.hanusz
Improvements of the class:
类的改进:
- angle returned is total since rotation has begun
- removing unnecessary functions
- simplification
- get position of first pointer only after second pointer is down
- 旋转开始后返回的角度是总角度
- 去除不必要的功能
- 简化
- 仅在第二个指针按下后获取第一个指针的位置
public class RotationGestureDetector {
private static final int INVALID_POINTER_ID = -1;
private float fX, fY, sX, sY;
private int ptrID1, ptrID2;
private float mAngle;
private OnRotationGestureListener mListener;
public float getAngle() {
return mAngle;
}
public RotationGestureDetector(OnRotationGestureListener listener){
mListener = listener;
ptrID1 = INVALID_POINTER_ID;
ptrID2 = INVALID_POINTER_ID;
}
public boolean onTouchEvent(MotionEvent event){
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
ptrID1 = event.getPointerId(event.getActionIndex());
break;
case MotionEvent.ACTION_POINTER_DOWN:
ptrID2 = event.getPointerId(event.getActionIndex());
sX = event.getX(event.findPointerIndex(ptrID1));
sY = event.getY(event.findPointerIndex(ptrID1));
fX = event.getX(event.findPointerIndex(ptrID2));
fY = event.getY(event.findPointerIndex(ptrID2));
break;
case MotionEvent.ACTION_MOVE:
if(ptrID1 != INVALID_POINTER_ID && ptrID2 != INVALID_POINTER_ID){
float nfX, nfY, nsX, nsY;
nsX = event.getX(event.findPointerIndex(ptrID1));
nsY = event.getY(event.findPointerIndex(ptrID1));
nfX = event.getX(event.findPointerIndex(ptrID2));
nfY = event.getY(event.findPointerIndex(ptrID2));
mAngle = angleBetweenLines(fX, fY, sX, sY, nfX, nfY, nsX, nsY);
if (mListener != null) {
mListener.OnRotation(this);
}
}
break;
case MotionEvent.ACTION_UP:
ptrID1 = INVALID_POINTER_ID;
break;
case MotionEvent.ACTION_POINTER_UP:
ptrID2 = INVALID_POINTER_ID;
break;
case MotionEvent.ACTION_CANCEL:
ptrID1 = INVALID_POINTER_ID;
ptrID2 = INVALID_POINTER_ID;
break;
}
return true;
}
private float angleBetweenLines (float fX, float fY, float sX, float sY, float nfX, float nfY, float nsX, float nsY)
{
float angle1 = (float) Math.atan2( (fY - sY), (fX - sX) );
float angle2 = (float) Math.atan2( (nfY - nsY), (nfX - nsX) );
float angle = ((float)Math.toDegrees(angle1 - angle2)) % 360;
if (angle < -180.f) angle += 360.0f;
if (angle > 180.f) angle -= 360.0f;
return angle;
}
public static interface OnRotationGestureListener {
public void OnRotation(RotationGestureDetector rotationDetector);
}
}
How to use it:
如何使用它:
- Put the above class in a separate file
RotationGestureDetector.java
- create a private field
mRotationDetector
of typeRotationGestureDetector
in your activity class and create a new instance of the detector during the initialization (onCreate
method for example) and give as parameter a class implementing theonRotation
method (here theactivity = this
). - In the method
onTouchEvent
, send the touch events received to the gesture detector with 'mRotationDetector.onTouchEvent(event);
' - Implements
RotationGestureDetector.OnRotationGestureListener
in your activity and add the method 'public void OnRotation(RotationGestureDetector rotationDetector)
' in the activity. In this method, get the angle withrotationDetector.getAngle()
- 将上面的类放在一个单独的文件中
RotationGestureDetector.java
- 在您的活动类中创建一个私有
mRotationDetector
类型的字段,RotationGestureDetector
并在初始化期间创建一个新的检测器实例(onCreate
例如方法),并提供一个实现该onRotation
方法的类作为参数(这里是activity = this
)。 - 在该方法中
onTouchEvent
,使用 'mRotationDetector.onTouchEvent(event);
' 将接收到的触摸事件发送到手势检测器。 RotationGestureDetector.OnRotationGestureListener
在您的活动中实施并在活动中添加方法“public void OnRotation(RotationGestureDetector rotationDetector)
”。在这种方法中,获得角度rotationDetector.getAngle()
Example:
例子:
public class MyActivity extends Activity implements RotationGestureDetector.OnRotationGestureListener {
private RotationGestureDetector mRotationDetector;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mRotationDetector = new RotationGestureDetector(this);
}
@Override
public boolean onTouchEvent(MotionEvent event){
mRotationDetector.onTouchEvent(event);
return super.onTouchEvent(event);
}
@Override
public void OnRotation(RotationGestureDetector rotationDetector) {
float angle = rotationDetector.getAngle();
Log.d("RotationGestureDetector", "Rotation: " + Float.toString(angle));
}
}
Note:
笔记:
You can also use the RotationGestureDetector
class in a View
instead of an Activity
.
您还可以RotationGestureDetector
在 aView
而不是 中使用该类Activity
。
回答by aaronmarino
Here's my improvement on Leszek's answer. I found that his didn't work for small views as when a touch went outside the view the angle calculation was wrong. The solution is to get the raw location instead of just getX/Y.
这是我对 Leszek 回答的改进。我发现他对小视图不起作用,因为当触摸超出视图时,角度计算是错误的。解决方案是获取原始位置,而不仅仅是 getX/Y。
Credit to this threadfor getting the raw points on a rotatable view.
信贷这个线程为获得在旋转查看原始点。
public class RotationGestureDetector {
private static final int INVALID_POINTER_ID = -1;
private PointF mFPoint = new PointF();
private PointF mSPoint = new PointF();
private int mPtrID1, mPtrID2;
private float mAngle;
private View mView;
private OnRotationGestureListener mListener;
public float getAngle() {
return mAngle;
}
public RotationGestureDetector(OnRotationGestureListener listener, View v) {
mListener = listener;
mView = v;
mPtrID1 = INVALID_POINTER_ID;
mPtrID2 = INVALID_POINTER_ID;
}
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_OUTSIDE:
Log.d(this, "ACTION_OUTSIDE");
break;
case MotionEvent.ACTION_DOWN:
Log.v(this, "ACTION_DOWN");
mPtrID1 = event.getPointerId(event.getActionIndex());
break;
case MotionEvent.ACTION_POINTER_DOWN:
Log.v(this, "ACTION_POINTER_DOWN");
mPtrID2 = event.getPointerId(event.getActionIndex());
getRawPoint(event, mPtrID1, mSPoint);
getRawPoint(event, mPtrID2, mFPoint);
break;
case MotionEvent.ACTION_MOVE:
if (mPtrID1 != INVALID_POINTER_ID && mPtrID2 != INVALID_POINTER_ID) {
PointF nfPoint = new PointF();
PointF nsPoint = new PointF();
getRawPoint(event, mPtrID1, nsPoint);
getRawPoint(event, mPtrID2, nfPoint);
mAngle = angleBetweenLines(mFPoint, mSPoint, nfPoint, nsPoint);
if (mListener != null) {
mListener.onRotation(this);
}
}
break;
case MotionEvent.ACTION_UP:
mPtrID1 = INVALID_POINTER_ID;
break;
case MotionEvent.ACTION_POINTER_UP:
mPtrID2 = INVALID_POINTER_ID;
break;
case MotionEvent.ACTION_CANCEL:
mPtrID1 = INVALID_POINTER_ID;
mPtrID2 = INVALID_POINTER_ID;
break;
default:
break;
}
return true;
}
void getRawPoint(MotionEvent ev, int index, PointF point) {
final int[] location = { 0, 0 };
mView.getLocationOnScreen(location);
float x = ev.getX(index);
float y = ev.getY(index);
double angle = Math.toDegrees(Math.atan2(y, x));
angle += mView.getRotation();
final float length = PointF.length(x, y);
x = (float) (length * Math.cos(Math.toRadians(angle))) + location[0];
y = (float) (length * Math.sin(Math.toRadians(angle))) + location[1];
point.set(x, y);
}
private float angleBetweenLines(PointF fPoint, PointF sPoint, PointF nFpoint, PointF nSpoint) {
float angle1 = (float) Math.atan2((fPoint.y - sPoint.y), (fPoint.x - sPoint.x));
float angle2 = (float) Math.atan2((nFpoint.y - nSpoint.y), (nFpoint.x - nSpoint.x));
float angle = ((float) Math.toDegrees(angle1 - angle2)) % 360;
if (angle < -180.f) angle += 360.0f;
if (angle > 180.f) angle -= 360.0f;
return -angle;
}
public interface OnRotationGestureListener {
void onRotation(RotationGestureDetector rotationDetector);
}
}
回答by Jorge Garcia
I tried a combination of answers that are here but it still didn't work perfectly so I had to modify it a little bit.
我尝试了这里的答案组合,但它仍然不能完美地工作,所以我不得不稍微修改它。
This code gives you the delta angle on each rotation, it works perfectly to me, I'm using it to rotate an object in OpenGL.
这段代码为您提供了每次旋转的 delta 角度,它对我来说效果很好,我用它来旋转 OpenGL 中的对象。
public class RotationGestureDetector {
private static final int INVALID_POINTER_ID = -1;
private float fX, fY, sX, sY, focalX, focalY;
private int ptrID1, ptrID2;
private float mAngle;
private boolean firstTouch;
private OnRotationGestureListener mListener;
public float getAngle() {
return mAngle;
}
public RotationGestureDetector(OnRotationGestureListener listener){
mListener = listener;
ptrID1 = INVALID_POINTER_ID;
ptrID2 = INVALID_POINTER_ID;
}
public boolean onTouchEvent(MotionEvent event){
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
sX = event.getX();
sY = event.getY();
ptrID1 = event.getPointerId(0);
mAngle = 0;
firstTouch = true;
break;
case MotionEvent.ACTION_POINTER_DOWN:
fX = event.getX();
fY = event.getY();
focalX = getMidpoint(fX, sX);
focalY = getMidpoint(fY, sY);
ptrID2 = event.getPointerId(event.getActionIndex());
mAngle = 0;
firstTouch = true;
break;
case MotionEvent.ACTION_MOVE:
if(ptrID1 != INVALID_POINTER_ID && ptrID2 != INVALID_POINTER_ID){
float nfX, nfY, nsX, nsY;
nsX = event.getX(event.findPointerIndex(ptrID1));
nsY = event.getY(event.findPointerIndex(ptrID1));
nfX = event.getX(event.findPointerIndex(ptrID2));
nfY = event.getY(event.findPointerIndex(ptrID2));
if (firstTouch) {
mAngle = 0;
firstTouch = false;
} else {
mAngle = angleBetweenLines(fX, fY, sX, sY, nfX, nfY, nsX, nsY);
}
if (mListener != null) {
mListener.OnRotation(this);
}
fX = nfX;
fY = nfY;
sX = nsX;
sY = nsY;
}
break;
case MotionEvent.ACTION_UP:
ptrID1 = INVALID_POINTER_ID;
break;
case MotionEvent.ACTION_POINTER_UP:
ptrID2 = INVALID_POINTER_ID;
break;
}
return true;
}
private float getMidpoint(float a, float b){
return (a + b) / 2;
}
float findAngleDelta( float angle1, float angle2 )
{
float From = ClipAngleTo0_360( angle2 );
float To = ClipAngleTo0_360( angle1 );
float Dist = To - From;
if ( Dist < -180.0f )
{
Dist += 360.0f;
}
else if ( Dist > 180.0f )
{
Dist -= 360.0f;
}
return Dist;
}
float ClipAngleTo0_360( float Angle ) {
return Angle % 360.0f;
}
private float angleBetweenLines (float fx1, float fy1, float fx2, float fy2, float sx1, float sy1, float sx2, float sy2)
{
float angle1 = (float) Math.atan2( (fy1 - fy2), (fx1 - fx2) );
float angle2 = (float) Math.atan2( (sy1 - sy2), (sx1 - sx2) );
return findAngleDelta((float)Math.toDegrees(angle1),(float)Math.toDegrees(angle2));
}
public static interface OnRotationGestureListener {
public boolean OnRotation(RotationGestureDetector rotationDetector);
}
}
回答by Nir Hartmann
There are still some mistakes, here is the solution that worked perfect for me...
仍然存在一些错误,这是对我来说完美的解决方案......
instead of
代替
float angle = angleBtwLines(fX, fY, nfX, nfY, sX, sY, nsX, nsY);
you need to write
你需要写
float angle = angleBtwLines(fX, fY, sX, sY, nfX, nfY, nsX, nsY);
And angleBetweenLines should be
和angleBetweenLines 应该是
private float angleBetweenLines (float fx1, float fy1, float fx2, float fy2, float sx1, float sy1, float sx2, float sy2)
{
float angle1 = (float) Math.atan2( (fy1 - fy2), (fx1 - fx2) );
float angle2 = (float) Math.atan2( (sy1 - sy2), (sx1 - sx2) );
return findAngleDelta((float)Math.toDegrees(angle1),(float)Math.toDegrees(angle2));
}
Then the angle you get is the angle you should rotate the image by...
那么你得到的角度就是你应该旋转图像的角度......
ImageAngle += angle...
回答by Viktor Latypov
You have a problem here:
你在这里有问题:
private float angleBtwLines (float fx1, float fy1, float fx2, float fy2, float sx1, float sy1, float sx2, float sy2){
float angle1 = (float) Math.atan2(fy1 - fy2, fx1 - fx2);
float angle2 = (float) Math.atan2(sy1 - sy2, sx1 - sx2);
return (float) Math.toDegrees((angle1-angle2));
}
You must clip the angles to the [0..2*Pi] range and than carefully calculate the angular difference in the (-Pi..+Pi) range.
您必须将角度剪裁到 [0..2*Pi] 范围内,然后仔细计算 (-Pi..+Pi) 范围内的角度差。
Here's the code for 0..360 angle range
这是 0..360 角度范围的代码
float FindAngleDelta( float angle1, float angle2 )
{
float From = ClipAngleTo0_360( angle2 );
float To = ClipAngleTo0_360( angle1 );
float Dist = To - From;
if ( Dist < -180.0f )
{
Dist += 360.0f;
}
else if ( Dist > 180.0f )
{
Dist -= 360.0f;
}
return Dist;
}
In C++ I would code the ClipAngleTo0_360 as
在 C++ 中,我将 ClipAngleTo0_360 编码为
float ClipAngleTo0_360( float Angle ) { return std::fmod( Angle, 360.0f ); }
where the std::fmod return the floating-point remainder.
其中 std::fmod 返回浮点余数。
In java you may use something like
在java中你可以使用类似的东西
float ClipAngleTo0_360( float Angle )
{
float Res = Angle;
while(Angle < 0) { Angle += 360.0; }
while(Angle >= 360.0) { Angle -= 360.0; }
return Res;
}
Yeah, careful floating-point arithmetics is much better than the obvious while() loop.
是的,仔细的浮点运算比明显的 while() 循环要好得多。
As MeTTeO mentioned (java reference, 15.17.3), you can use the '%' operator instead of C++'s std::fmod:
正如 MeTTeO 提到的(java 参考,15.17.3),您可以使用 '%' 运算符代替 C++ 的 std::fmod:
float ClipAngleTo0_360( float Angle ) { return Angle % 360.0; }
回答by vihkat
I tried a lot of examples. But only this one works good.:
我尝试了很多例子。但只有这个效果很好。:
public class RotationGestureDetector {
public interface RotationListener {
public void onRotate(float deltaAngle);
}
protected float mRotation;
private RotationListener mListener;
public RotationGestureDetector(RotationListener listener) {
mListener = listener;
}
private float rotation(MotionEvent event) {
double delta_x = (event.getX(0) - event.getX(1));
double delta_y = (event.getY(0) - event.getY(1));
double radians = Math.atan2(delta_y, delta_x);
return (float) Math.toDegrees(radians);
}
public void onTouch(MotionEvent e) {
if (e.getPointerCount() != 2) {
return;
}
if (e.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
mRotation = rotation(e);
}
float rotation = rotation(e);
float delta = rotation - mRotation;
mRotation += delta;
if (mListener != null) {
mListener.onRotate(delta);}
}
}
}
In your callback:
在您的回调中:
view.setRotation(view.getRotation() + deltaAngle));