【问题标题】:Android Two finger rotationAndroid 两指旋转
【发布时间】:2012-05-27 18:18:25
【问题描述】:

我正在尝试在 android 中实现两个手指旋转,但是它并没有像预期的那样工作。目标是像谷歌地球一样实现旋转(两指围绕焦点旋转图像)。目前我的旋转监听器看起来像这样:

 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));
    }
}

但是,当我旋转时,旋转角度会大得多,有时它会旋转到错误的一侧。有关如何解决此问题的任何想法?

顺便说一句,我在摩托罗拉 Atrix 上测试它,所以它没有触摸屏错误。

谢谢

【问题讨论】:

  • 我会研究这些类:GestureDetector (source) ScaleGestureDetector (source) 它们包含一些很好的技巧来
  • ScaleGestureDetector 只能用于缩放(我已经使用该类实现了),而 GestureDetector 只能用于单点触摸手势。 Android 没有默认的旋转手势检测器。
  • 您有角度问题,而不是 GestureDection 和处理问题。您报告的症状正是角度差计算错误所预期的症状。
  • pretomba,我只是建议你可以研究那些内置类的实现,看看如何很好地处理来自两个手指的事件。 Viktor,Java 有余数运算符 '%' 所以你不需要使用方法和 while 循环...
  • 谢谢,将您的建议添加到我的回答中。

标签: android rotation gesture-recognition


【解决方案1】:

类的改进:

  • 自旋转开始后返回的总角度
  • 删除不必要的功能
  • 简化
  • 只有在第二个指针向下后才能获取第一个指针的位置
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);
    }
}

使用方法:

  1. 把上面的类放在一个单独的文件里RotationGestureDetector.java
  2. 在您的活动类中创建一个RotationGestureDetector 类型的私有字段mRotationDetector,并在初始化期间创建一个新的检测器实例(例如onCreate 方法),并将实现onRotation 方法的类作为参数(这里是activity = this)。
  3. onTouchEvent方法中,将接收到的触摸事件发送给手势检测器,使用'mRotationDetector.onTouchEvent(event);'
  4. 在您的活动中实现RotationGestureDetector.OnRotationGestureListener 并在活动中添加方法“public void OnRotation(RotationGestureDetector rotationDetector)”。在这个方法中,用rotationDetector.getAngle()获取角度

示例:

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));
    }

}

注意:

您还可以在View 中使用RotationGestureDetector 类,而不是Activity

【讨论】:

  • 这对我很有效。我为自己的特定需求添加了一些自定义过滤,但这为我节省了很多时间。谢谢。
  • 向一个方向拖动我的手指我得到 178.33331, -177.07356, -177.14868, 131.9633 我的意思是根本没有意义的值。假的
  • 泰勒戴维斯,我遇到了同样的问题。请参阅下面的答案以获取解决方案。
  • 只是为了清楚,这实际上并不能处理旋转,对吧?它只是获取对象已旋转的角度,以便可以像 Matrix.postRotate 调用一样使用它来实际旋转视图
  • 值得注意的是,angleBetweenLines() 提供了累积角度。如果你需要增量角度,你应该写(在 onTouchEvent() 中)mDeltaAngle = mAngle - mPreviousAngle 并返回 mDeltaAngle。还要在“if (mListener != null)”块中写入 mPreviousAngle = mAngle。不要忘记将其初始化为零,并在 switch 块中的其他“情况”中使其为零。
【解决方案2】:

这是我对 Leszek 答案的改进。我发现他不适用于小视图,因为当触摸超出视图时,角度计算是错误的。解决方案是获取原始位置,而不仅仅是 getX/Y。

感谢this thread 在可旋转视图上获取原始点。

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);
    }
}

【讨论】:

  • 这太棒了!谢谢!不过,我遇到的唯一问题是,在第一次旋转之后,每次旋转都会首先将角度重置为 0。我已经有一段时间无法解决这个问题了。关于如何实现 delta-anglebetweenlines 的任何想法?
  • 我自己修复了这个问题,方法是将旧角度保存在 ACTOIN_POINTER_DOWN 中,并将其添加到 ACTION_MOVE 中的 mAngle 中(并对该值使用模 360)。我希望这对尝试和我做同样事情的人有用。
【解决方案3】:

我尝试了此处提供的答案组合,但仍然无法完美运行,因此我不得不对其进行一些修改。

此代码为您提供每次旋转的增量角度,它对我来说非常有效,我正在使用它来旋转 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);
}
}

【讨论】:

  • 你能告诉如何在活动中使用这个类吗?
  • @vins,看我的回答。
  • 这在三星 s8+ 上也适用于我,在 opengl 上非常流畅
【解决方案4】:

还有一些错误,这里是最适合我的解决方案...

而不是

float angle = angleBtwLines(fX, fY, nfX, nfY, sX, sY, nsX, nsY);

你需要写

float angle = angleBtwLines(fX, fY, sX, sY, nfX, nfY, nsX, nsY);

并且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));
}

那么你得到的角度就是你应该旋转图像的角度......

ImageAngle += angle...

【讨论】:

    【解决方案5】:

    你这里有问题:

    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));
    }
    

    您必须将角度剪裁到 [0..2*Pi] 范围内,然后仔细计算 (-Pi..+Pi) 范围内的角度差。

    这是 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;
    }
    

    在 C++ 中,我会将 ClipAngleTo0_360 编码为

    float ClipAngleTo0_360( float Angle ) { return std::fmod( Angle, 360.0f ); }
    

    std::fmod 返回浮点余数。

    在java中你可以使用类似的东西

    float ClipAngleTo0_360( float Angle )
    {
        float Res = Angle;
        while(Angle < 0) { Angle += 360.0; }
        while(Angle >= 360.0) { Angle -= 360.0; }
        return Res;
    }
    

    是的,仔细的浮点运算比明显的 while() 循环要好得多。

    正如 MeTTeO 提到的(java 参考,15.17.3),您可以使用 '%' 运算符而不是 C++ 的 std::fmod:

    float ClipAngleTo0_360( float Angle ) { return Angle % 360.0; }
    

    【讨论】:

      【解决方案6】:

      我尝试了很多例子。但只有这一个效果好。:

      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);}
          }
        }
      }
      

      在您的回调中:

      view.setRotation(view.getRotation() + deltaAngle));
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-11-21
        • 1970-01-01
        • 2022-01-08
        • 2013-01-23
        相关资源
        最近更新 更多