Android 在 FrameLayout 中拖动和缩放

Android Dragging and Scaling in FrameLayout

最近我一直在尝试对放置在FrameLayout 中的图片进行拖动和缩放。我想要实现的目标很简单:能够拖动图片并对其进行缩放。我去了 Android 开发者网站并关注了 guide there.

然后根据该网站上的代码示例,我编写了 MyCustomView:

public class MyCustomView extends ImageView {
private static final int INVALID_POINTER_ID = 0xDEADBEEF;
private ScaleGestureDetector mScaleDetector;
private float mScaleFactor = 1.f;
private float mLastTouchX, mLastTouchY;
private int mActivePointerId = INVALID_POINTER_ID;
private LayoutParams mLayoutParams;
private int mPosX, mPosY;

public MyCustomView(Context context) {
    super(context);

    mScaleDetector = new ScaleGestureDetector(context, new CustomScaleListener());
    mLayoutParams = (LayoutParams) super.getLayoutParams();
    if (mLayoutParams != null) {
        mPosX = mLayoutParams.leftMargin;
        mPosY = mLayoutParams.topMargin;
    } else {
        mLayoutParams = new LayoutParams(300, 300);
        mLayoutParams.leftMargin = 0;
        mLayoutParams.topMargin = 0;
    }
}

@Override
public void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.save();
    canvas.scale(mScaleFactor, mScaleFactor);

    canvas.restore();
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
    // Let the ScaleGestureDetector inspect all events
    mScaleDetector.onTouchEvent(ev);

    final int action = MotionEventCompat.getActionMasked(ev);

    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            final int pointerIndex = MotionEventCompat.getActionIndex(ev);

            //final float x = MotionEventCompat.getX(ev, pointerIndex);
            //final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float x = ev.getRawX();
            final float y = ev.getRawY();

            // Remember where we started (for dragging)
            mLastTouchX = x;
            mLastTouchY = y;
            // Save the ID of this pointer (for dragging)
            mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
            break;
        }

        case MotionEvent.ACTION_MOVE: {
            // Find the index of the active pointer and fetch its position
            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);

            //final float x = MotionEventCompat.getX(ev, pointerIndex);
            //final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float x = ev.getRawX();
            final float y = ev.getRawY();

            final float dx = x - mLastTouchX;
            final float dy = y - mLastTouchY;

            //TODO: Update the location of this view
            mPosX += dx;
            mPosY += dy;

            mLayoutParams.leftMargin += dx;
            mLayoutParams.topMargin += dy;
            super.setLayoutParams(mLayoutParams);

            invalidate();


            mLastTouchX = x;
            mLastTouchY = y;
            break;
        }

        case MotionEvent.ACTION_UP: {
            mActivePointerId = INVALID_POINTER_ID;
            break;
        }

        case MotionEvent.ACTION_CANCEL: {
            mActivePointerId = INVALID_POINTER_ID;
            break;
        }

        case MotionEvent.ACTION_POINTER_UP: {
            final int pointerIndex = MotionEventCompat.getActionIndex(ev);
            final int pointerID = MotionEventCompat.getPointerId(ev, pointerIndex);

            if (pointerID == mActivePointerId) {
                // This was our active pointer going up. Choose a new active pointer and
                // adjust accordingly
                final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                //mLastTouchX = MotionEventCompat.getX(ev, newPointerIndex);
                //mLastTouchY = MotionEventCompat.getY(ev, newPointerIndex);
                mLastTouchX = ev.getRawX();
                mLastTouchY = ev.getRawY();
                mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
            }

            break;
        }
    }
    return true;
}




private class CustomScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        mScaleFactor *= detector.getScaleFactor();

        mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));

        invalidate();

        return true;
    }
}

MainActivity中我简单的实例化了一个MyCustomView对象,并附加到后台的ViewGroup,也就是一个FrameLayout。 xml 文件只有一个 FrameLayout。

    public class MainActivity extends AppCompatActivity {
    private ViewGroup layoutRoot;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        layoutRoot = (ViewGroup) findViewById(R.id.view_root);

        final MyCustomView ivAndroid = new MyCustomView(this);
        ivAndroid.setImageResource(R.mipmap.ic_launcher);
        ivAndroid.setLayoutParams(new FrameLayout.LayoutParams(300, 300));
        layoutRoot.addView(ivAndroid);
    }
}

困扰我的问题来了: Android开发者网站以此获取触摸图片的手指坐标:

final float x = MotionEventCompat.getX(ev, pointerIndex);
final float y = MotionEventCompat.getY(ev, pointerIndex);

但它的效果很糟糕!图片会动,但不会完全跟着我的手指移动,总是比我的手指移动得少,最重要的是,它会闪烁。

这就是为什么您可以在 MyCustomView 中看到我已注释掉此行并使用此代码的原因:

final float x = ev.getRawX();
final float y = ev.getRawY();

虽然这次画面顺畅地顺着我的手指移动,但这个变化引入了一个新的问题。 Android Developer website 上关于拖动和缩放的设计原则是这样写的:

In a drag (or scroll) operation, the app has to keep track of the original pointer (finger), even if additional fingers get placed on the screen. For example, imagine that while dragging the image around, the user places a second finger on the touch screen and lifts the first finger. If your app is just tracking individual pointers, it will regard the second pointer as the default and move the image to that location.

在我开始使用 ev.getRawX()ev.getRawY() 后,将第二根手指添加到屏幕给了我上面提到的问题。但是 MotionEventCompat.getX(ev, pointerIndex)MotionEventCompat.getY(ev , pointerIndex) 没有.

谁能帮我解释一下为什么会这样?我知道 MotionEventCompat.getX(ev, pointerIndex) returns 经过某种调整后的坐标, ev.getRawX() returns 绝对坐标。但我不明白调整的具体工作原理(是否有公式或图形解释?)。我还想知道为什么使用 MotionEventCompat.getX(...) 会阻止图片跳到屏幕上的第二根手指(在抬起第一根手指后)。

最后但同样重要的是,缩放代码根本不起作用。如果有人教我这方面的知识,我将不胜感激!

这个问题很长,所以我会分成小部分。另外,英语不是我的母语,所以我在写答案时遇到了一些困难。有不明白之处请留言。

Can somebody help me explain why it happens?

getRawX()ev.getRawY() 都会为您提供事件的绝对像素值。这些也将(为了向后兼容,当大多数屏幕一次只能跟踪 1 "region" 时)将始终将手指视为与设备交互的第一个(也是唯一一个)手指。

然后,进行了允许跟踪手指 ID 的改进。MotionEventCompat.getX(ev, pointerIndex)MotionEventCompat.getY(ev, pointerIndex) 函数允许在创建我们的 onTouch() 侦听器时进一步精细化。

Is there a formula or graphical explanation for it?

基本上,您需要考虑该设备的 "Screen Density"。如:

float SCREEN_DENSITY = getResources().getDisplayMetrics().density;

protected void updateFrame(FrameLayout frameLayout, int h, int w, int x, int y) {
    FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
            (int) ((w * SCREEN_DENSITY) + 0.5),
            (int) ((h * SCREEN_DENSITY) + 0.5)
    );
    params.leftMargin = (int) ((x * SCREEN_DENSITY) + 0.5);
    params.topMargin = (int) ((y * SCREEN_DENSITY) + 0.5);
    frameLayout.setLayoutParams(params);
}

I also want to know why using MotionEventCompat.getX(...) would prevent the picture from jumping to the second finger on screen (after the first finger has been lifted)

如果考虑到第一个 "finger" 被解除,那么新的 "initial point" 和 "history" 不同,因此,它可以发送它的事件与所做的运动有关,而不是屏幕上的最终位置。这样它就不会 "jump to where the finger is" 但会根据 "x units" 和 "y units" 遍历的数量移动。

Last but not least, the scaling code simply doesn't work AT ALL. If someone and teach me on that it will also be greatly appreciated!

您正在使用该事件(通过在您的 onTouch 侦听器上返回 true),因此,没有其他侦听器可以继续从事件中读取,您可以触发更多侦听器。

如果需要,可以将这两个功能(移动和调整大小)移到 onTouch 中。我的 onTouch 监听器有超过 1700 行代码(因为它做了很多事情,包括以编程方式创建视图并向其添加监听器),所以我不能在这里 post 它,但基本上:

1 Finger = move the frame. Get raw values, and use the "updateFrame"
2 Fingers = resize the frame. Get raw values, and use the "updateFrame"
3+ Fingers = Drop first finger, suppose 2 Fingers.