【问题标题】:Android Dragging and Scaling in FrameLayoutFrameLayout中的Android拖动和缩放
【发布时间】:2016-07-12 11:23:42
【问题描述】:

最近我一直在尝试在我放置在 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 Developerwebsite进行拖拽缩放,有一个设计原则是这样说的:

在拖动(或滚动)操作中,应用程序必须跟踪原始指针(手指),即使其他手指放置在屏幕上也是如此。例如,假设在拖动图像时,用户将第二根手指放在触摸屏上并抬起第一根手指。如果您的应用只是跟踪单个指针,它会将第二个指针视为默认指针并将图像移动到该位置。

在我开始使用 ev.getRawX()ev.getRawY() 之后,在屏幕上添加第二根手指就可以解决上述问题。但是 MotionEventCompat.getX(ev, pointerIndex)MotionEventCompat.getY(ev, pointerIndex) 没有。

有人可以帮我解释一下为什么会这样吗?我知道 MotionEventCompat.getX(ev, pointerIndex) 在某种调整后返回坐标,而 ev.getRawX() 返回绝对坐标。但我不明白调整究竟是如何工作的(有公式或图形解释吗?)。我也想知道为什么使用 MotionEventCompat.getX(...) 会阻止图片跳到屏幕上的第二根手指(在第一根手指抬起之后)。

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

【问题讨论】:

  • this
  • 您好,很抱歉我没有看到这个。我不太了解您的代码背后的机制...(对不起,我是 Android 新手)如果可能的话,您能解释一下您的 Layer 类是什么吗?
  • 运行代码,你会看到三层

标签: android android-framelayout


【解决方案1】:

这个问题很长,所以我将它分成更小的部分。另外,英语不是我的母语,所以我在写答案时遇到了一些困难。如果部分不清楚,请评论。

谁能帮我解释一下为什么会这样?

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

然后,进行了改进,可以跟踪手指 ID。MotionEventCompat.getX(ev, pointerIndex)MotionEventCompat.getY(ev, pointerIndex) 函数允许在创建 onTouch() 侦听器时更加精细。

有公式或图形解释吗?

基本上,您需要考虑该设备的“屏幕密度”。如:

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

我也想知道为什么使用 MotionEventCompat.getX(...) 会阻止图片跳到屏幕上的第二根手指(在第一根手指抬起后)

如果考虑到第一个“手指”被抬起,那么新的“手指”具有不同的“初始点”和不同的“历史”,因此,它可以发送与运动相关的事件制作,而不是屏幕上的最终位置。这样它就不会“跳到手指所在的位置”,而是会根据遍历的“x 个单位”和“y 个单位”的数量移动。

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

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

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

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.

【讨论】:

  • 您好,感谢您的回复,我学到了很多!虽然我了解您提出的关于 1,2 和 3+ 手指的解决方案,但我对如何在我的代码中实现它一无所知...另外,我应该实际使用 onTouch() 还是可以继续使用 onTouchEvent()?
  • 覆盖 onTouchEvent() 是“创建一个新的 onTouch() 侦听器对象”并改用该对象,因此,在实践中,它没有任何区别。我更喜欢 onTouch,因为如果我要覆盖每个对象,我可以多次使用同一个对象(我可以有超过 80 多个视图没有问题,超过 300 个,在大多数设备中存在一些问题)都使用相同的 onTouch 对象,我会在我的活动中降至 120~ 观看次数上限。如果您使用的视图少于 40 个,则没关系。今天的大多数设备平均剩余 512 个 RAM,因此内存管理不再那么必要。
  • 我尝试在我的图像上使用 SCREEN_DENSITY,但是一旦我点击它,它就会飞出屏幕。在您的示例中,您将 LayoutParam 应用于 FrameLayout,我发现这很混乱。你要移动拿着所有照片的父母吗?那不是将所有图片全部移动,而不是一张吗?
  • @ThomasWchen 不,LayoutParams 用于子视图...它是一个 FrameLayout,其中包含 FrameLayout 子级。它的参数从父级的 0x 和 0y 变化,因此您可以移动父级(这将移动所有子级),或者您可以移动子级(只会在父级内部移动,我已经简化了显示的代码,但在我的函数中 x/y/h/w 不能大于父级,或者放置一个不适合的视图。此外,如果您的 SCREEN_DENSITY 没有正确移动到屏幕上,则有些设备会“伪造”@987654321 @他们在
  • 这可以解决问题。 stackoverflow.com/questions/6578320/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-12
  • 2016-03-17
  • 1970-01-01
  • 2011-08-18
  • 2016-01-10
相关资源
最近更新 更多