android - 在 SurfaceView 上捏合缩放

标签 android surfaceview pinchzoom

我正在尝试在 SurfaceView 上实现缩放。我对此做了很多研究,我发现 this 类可以实现捏合缩放。这是我修改它的方式:

public class ZoomLayout extends FrameLayout implements ScaleGestureDetector.OnScaleGestureListener {

private SurfaceView mSurfaceView;

private enum Mode {
    NONE,
    DRAG,
    ZOOM
}

private static final String TAG = "ZoomLayout";
private static final float MIN_ZOOM = 1.0f;
private static final float MAX_ZOOM = 4.0f;

private Mode mode;
private float scale = 1.0f;
private float lastScaleFactor = 0f;

// Where the finger first  touches the screen
private float startX = 0f;
private float startY = 0f;

// How much to translate the canvas
private float dx = 0f;
private float dy = 0f;
private float prevDx = 0f;
private float prevDy = 0f;

public ZoomLayout(Context context) {
    super(context);
    init(context);
}

public ZoomLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
}

public ZoomLayout(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init(context);
}

private void init(Context context) {
    final ScaleGestureDetector scaleDetector = new ScaleGestureDetector(context, this);
    setOnTouchListener(new OnTouchListener() {
        public boolean onTouch(View view, MotionEvent motionEvent) {
            ZoomLayout.this.mSurfaceView = (SurfaceView) view.findViewById(R.id.mSurfaceView);

            switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    Log.i(TAG, "DOWN");
                    if (scale > MIN_ZOOM) {
                        mode = Mode.DRAG;
                        startX = motionEvent.getX() - prevDx;
                        startY = motionEvent.getY() - prevDy;
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (mode == Mode.DRAG) {
                        dx = motionEvent.getX() - startX;
                        dy = motionEvent.getY() - startY;
                    }
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    mode = Mode.ZOOM;
                    break;
                case MotionEvent.ACTION_POINTER_UP:
                    mode = Mode.NONE;
                    break;
                case MotionEvent.ACTION_UP:
                    Log.i(TAG, "UP");
                    mode = Mode.NONE;
                    prevDx = dx;
                    prevDy = dy;
                    break;
            }
            scaleDetector.onTouchEvent(motionEvent);

            if ((mode == Mode.DRAG && scale >= MIN_ZOOM) || mode == Mode.ZOOM) {
                getParent().requestDisallowInterceptTouchEvent(true);
                float maxDx = (child().getWidth() - (child().getWidth() / scale)) / 2 * scale;
                float maxDy = (child().getHeight() - (child().getHeight() / scale))/ 2 * scale;
                dx = Math.min(Math.max(dx, -maxDx), maxDx);
                dy = Math.min(Math.max(dy, -maxDy), maxDy);
                Log.i(TAG, "Width: " + child().getWidth() + ", scale " + scale + ", dx " + dx
                        + ", max " + maxDx);
                applyScaleAndTranslation();
            }

            return true;
        }
    });
}

// ScaleGestureDetector

public boolean onScaleBegin(ScaleGestureDetector scaleDetector) {
    Log.i(TAG, "onScaleBegin");
    return true;
}

public boolean onScale(ScaleGestureDetector scaleDetector) {
    float scaleFactor = scaleDetector.getScaleFactor();
    Log.i(TAG, "mode:" + this.mode + ", onScale:" + scaleFactor);
    if (this.lastScaleFactor == 0.0f || Math.signum(scaleFactor) == Math.signum(this.lastScaleFactor)) {
        this.scale *= scaleFactor;
        this.scale = Math.max(MIN_ZOOM, Math.min(this.scale, MAX_ZOOM));
        this.lastScaleFactor = scaleFactor;
    } else {
        this.lastScaleFactor = 0.0f;
    }
    if (this.mSurfaceView != null) {
        int orgWidth = getWidth();
        int _width = (int) (((float) orgWidth) * this.scale);
        int _height = (int) (((float) getHeight()) * this.scale);
        LayoutParams params = (LayoutParams) this.mSurfaceView.getLayoutParams();
        params.height = _height;
        params.width = _width;
        this.mSurfaceView.setLayoutParams(params);
        child().setScaleX(this.scale);
        child().setScaleY(this.scale);
    }
    return true;
}

@Override
public void onScaleEnd(ScaleGestureDetector scaleDetector) {
    Log.i(TAG, "onScaleEnd");
}

private void applyScaleAndTranslation() {
    child().setScaleX(scale);
    child().setScaleY(scale);
    child().setTranslationX(dx);
    child().setTranslationY(dy);
}

private View child() {
    return getChildAt(0);
}

然后我将它实现到我的布局中,如下所示:

<pacageName.control.ZoomLayout
    android:id="@+id/mZoomLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:measureAllChildren="true">

        <SurfaceView
            android:id="@+id/mSurfaceView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

</pacageName.control.ZoomLayout>

我遇到的问题是缩放不“平滑”并且缩放的行为在不同设备(三星 S4( Lollipop )和 J7Pro(牛轧糖))中不同。

我不确定为什么双指缩放会出现“故障”,以及为什么它在两种不同设备上的缩放方式不同。


编辑 1: 下面添加了内存和 CPU 消耗图像 -

CPU


编辑 2: 在尝试其他方法后,我遇到了一个新问题 -

我将 ZoomLayout 完全更改为以下内容:

public class ZoomableSurfaceView extends SurfaceView {

private ScaleGestureDetector SGD;
private Context context;
private boolean isSingleTouch;
private float width, height = 0;
private float scale = 1f;
private float minScale = 1f;
private float maxScale = 5f;
int left, top, right, bottom;

public ZoomableSurfaceView(Context context) {
    super(context);
    this.context = context;
    init();
}

public ZoomableSurfaceView(Context context, AttributeSet attrs) {
    super(context, attrs);
    this.context = context;
    init();
}

public ZoomableSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    this.context = context;
    init();
}

private void init() {
    setOnTouchListener(new MyTouchListeners());
    SGD = new ScaleGestureDetector(context, new ScaleListener());
    this.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {

        }
    });
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    if (width == 0 && height == 0) {
        width = ZoomableSurfaceView.this.getWidth();
        height = ZoomableSurfaceView.this.getHeight();
        this.left = left;
        this.right = right;
        this.top = top;
        this.bottom = bottom;
    }

}

private class MyTouchListeners implements View.OnTouchListener {

    float dX, dY;

    MyTouchListeners() {
        super();
    }

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        SGD.onTouchEvent(event);
        if (event.getPointerCount() > 1) {
            isSingleTouch = false;
        } else {
            if (event.getAction() == MotionEvent.ACTION_UP) {
                isSingleTouch = true;
            }
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                dX = ZoomableSurfaceView.this.getX() - event.getRawX();
                dY = ZoomableSurfaceView.this.getY() - event.getRawY();
                break;

            case MotionEvent.ACTION_MOVE:
                if (isSingleTouch) {
                    ZoomableSurfaceView.this.animate()
                            .x(event.getRawX() + dX)
                            .y(event.getRawY() + dY)
                            .setDuration(0)
                            .start();
                    checkDimension(ZoomableSurfaceView.this);
                }
                break;
            default:
                return true;
        }
        return true;
    }
}

private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        //Log.e("onGlobalLayout: ", scale + " " + width + " " + height);
        scale *= detector.getScaleFactor();
        scale = Math.max(minScale, Math.min(scale, maxScale));
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams((int) (width * scale), (int) (height * scale));
        Log.e("onGlobalLayout: ", (int) (width * scale) + " " + (int) (height * scale));
        ZoomableSurfaceView.this.setLayoutParams(params);
        checkDimension(ZoomableSurfaceView.this);
        return true;
    }
}

private void checkDimension(View vi) {
    if (vi.getX() > left) {
        vi.animate()
                .x(left)
                .y(vi.getY())
                .setDuration(0)
                .start();
    }

    if ((vi.getWidth() + vi.getX()) < right) {
        vi.animate()
                .x(right - vi.getWidth())
                .y(vi.getY())
                .setDuration(0)
                .start();
    }

    if (vi.getY() > top) {
        vi.animate()
                .x(vi.getX())
                .y(top)
                .setDuration(0)
                .start();
    }

    if ((vi.getHeight() + vi.getY()) < bottom) {
        vi.animate()
                .x(vi.getX())
                .y(bottom - vi.getHeight())
                .setDuration(0)
                .start();
    }
}
}

我现在面临的问题是它缩放到 X - 0 和 Y - 0 意味着它缩放到屏幕的左上角而不是缩放到我手指之间的点..我认为它可能是与以下内容相关:

@Override
    public boolean onScale(ScaleGestureDetector detector) {
        //Log.e("onGlobalLayout: ", scale + " " + width + " " + height);
        scale *= detector.getScaleFactor();
        scale = Math.max(minScale, Math.min(scale, maxScale));
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams((int) (width * scale), (int) (height * scale));
        Log.e("onGlobalLayout: ", (int) (width * scale) + " " + (int) (height * scale));
        ZoomableSurfaceView.this.setLayoutParams(params);
        checkDimension(ZoomableSurfaceView.this);
        return true;
    }

我有一个想法,我应该检测 2 个手指的中心点并将其添加到 OnScale。我应该如何进行?


编辑 3:

好的,我终于在运行 Nougat 的 J7 Pro 上完美地实现了变焦功能。但现在的问题是,我在运行 Lollipop 的 S4 中的 SurfaceView 没有放大,而是我的 SurfaceView 移到了左上角。我已经测试过在我的自定义缩放 View 中放置一个 ImageView 并且图像得到了预期的完美缩放。这就是我的自定义缩放类现在的样子:

public class ZoomLayout extends FrameLayout implements ScaleGestureDetector.OnScaleGestureListener {

private enum Mode {
    NONE,
    DRAG,
    ZOOM
}

private static final String TAG = "ZoomLayout";
private static final float MIN_ZOOM = 1.0f;
private static final float MAX_ZOOM = 4.0f;

private Mode mode = Mode.NONE;
private float scale = 1.0f;
private float lastScaleFactor = 0f;

// Where the finger first  touches the screen
private float startX = 0f;
private float startY = 0f;

// How much to translate the canvas
private float dx = 0f;
private float dy = 0f;
private float prevDx = 0f;
private float prevDy = 0f;

public ZoomLayout(Context context) {
    super(context);
    init(context);
}

public ZoomLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
}

public ZoomLayout(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init(context);
}

private void init(Context context) {
    final ScaleGestureDetector scaleDetector = new ScaleGestureDetector(context, this);
    setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    Log.i(TAG, "DOWN");
                    if (scale > MIN_ZOOM) {
                        mode = Mode.DRAG;
                        startX = motionEvent.getX() - prevDx;
                        startY = motionEvent.getY() - prevDy;
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (mode == Mode.DRAG) {
                        dx = motionEvent.getX() - startX;
                        dy = motionEvent.getY() - startY;
                    }
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    mode = Mode.ZOOM;
                    break;
                case MotionEvent.ACTION_POINTER_UP:
                    mode = Mode.NONE; // changed from DRAG, was messing up zoom
                    break;
                case MotionEvent.ACTION_UP:
                    Log.i(TAG, "UP");
                    mode = Mode.NONE;
                    prevDx = dx;
                    prevDy = dy;
                    break;
            }
            scaleDetector.onTouchEvent(motionEvent);

            if ((mode == Mode.DRAG && scale >= MIN_ZOOM) || mode == Mode.ZOOM) {
                getParent().requestDisallowInterceptTouchEvent(true);
                float maxDx = child().getWidth() * (scale - 1);  // adjusted for zero pivot
                float maxDy = child().getHeight() * (scale - 1);  // adjusted for zero pivot
                dx = Math.min(Math.max(dx, -maxDx), 0);  // adjusted for zero pivot
                dy = Math.min(Math.max(dy, -maxDy), 0);  // adjusted for zero pivot
                Log.i(TAG, "Width: " + child().getWidth() + ", scale " + scale + ", dx " + dx
                        + ", max " + maxDx);
                applyScaleAndTranslation();
            }

            return true;
        }
    });
}

// ScaleGestureDetector
@Override
public boolean onScaleBegin(ScaleGestureDetector scaleDetector) {
    Log.i(TAG, "onScaleBegin");
    return true;
}

@Override
public boolean onScale(ScaleGestureDetector scaleDetector) {
    float scaleFactor = scaleDetector.getScaleFactor();
    Log.i(TAG, "onScale(), scaleFactor = " + scaleFactor);
    if (lastScaleFactor == 0 || (Math.signum(scaleFactor) == Math.signum(lastScaleFactor))) {
        float prevScale = scale;
        scale *= scaleFactor;
        scale = Math.max(MIN_ZOOM, Math.min(scale, MAX_ZOOM));
        lastScaleFactor = scaleFactor;
        float adjustedScaleFactor = scale / prevScale;
        // added logic to adjust dx and dy for pinch/zoom pivot point
        Log.d(TAG, "onScale, adjustedScaleFactor = " + adjustedScaleFactor);
        Log.d(TAG, "onScale, BEFORE dx/dy = " + dx + "/" + dy);
        float focusX = scaleDetector.getFocusX();
        float focusY = scaleDetector.getFocusY();
        Log.d(TAG, "onScale, focusX/focusy = " + focusX + "/" + focusY);
        dx += (dx - focusX) * (adjustedScaleFactor - 1);
        dy += (dy - focusY) * (adjustedScaleFactor - 1);
        Log.d(TAG, "onScale, dx/dy = " + dx + "/" + dy);
    } else {
        lastScaleFactor = 0;
    }
    return true;
}

@Override
public void onScaleEnd(ScaleGestureDetector scaleDetector) {
    Log.i(TAG, "onScaleEnd");
}

private void applyScaleAndTranslation() {
    child().setScaleX(scale);
    child().setScaleY(scale);
    child().setPivotX(0f);  // default is to pivot at view center
    child().setPivotY(0f);  // default is to pivot at view center
    child().setTranslationX(dx);
    child().setTranslationY(dy);
}

private View child() {
    return getChildAt(0);
}

编辑 4: 添加了行为视频:

更新我的 zoomLayout 类以编辑 3 后,请查看当前缩放的样子 -

Video of SurfaceView zoom behaviour

最佳答案

不使用 SurfaceView,而是使用 TextureView,它具有更多功能并且优于 SurfaceView。参见 TextureView Documentation


你的代码确实是正确的,应该可以工作,但在某些情况下,在 SurfaceView 上绘制的 MediaPlayer 不会调整大小,并且会产生在该位置发生缩放的错觉0,0 的电话。如果为 SurfaceView 设置小于父级的尺寸并为父级设置背景,您会注意到这一点


代码中的解决方案

示例项目:Pinch Zoom

该项目扩展了 TextureView 并实现了触摸以创建缩放效果。

IMAGE: DEMONSTRATION

关于android - 在 SurfaceView 上捏合缩放,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50411590/

相关文章:

java - 我们可以存储一个即使用户清理缓存和数据也无法删除的密码吗

android - Android 中的文本作为图像

Android Jetpack Compose (Composable) 用动画改变图像源

java - Android Studio - 如何使用按钮和 if-elseif 语句循环浏览图像?

Swift:热平移和放大/缩小 imageView 中的图像

java - 防止 OpenGL ES 在方向切换时破坏 Canvas

android - 从自定义 SurfaceView 获取位图

android - 在相机预览上移动叠加图像

android - 有什么方法可以在 Android Emulator 上测试多点触控吗?

ios - 以两个手指之间的点为中心缩放 UIImageView