I'm trying to implement pinch zoom on a SurfaceView
. I've done a lot of research regarding this and I found this class to implement pinch zoom. Here is how I modified it:
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);
}
and I implement it into my layout like this:
<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>
The problem I'm having is that the zoom is not 'smooth' and the behavior of the zoom is different in different devices (Samsung S4 (Lollipop) and J7Pro (Nougat)).
I'm not sure why pinch zoom is "glitchy" and why it doesn't zoom the same way on the two different devices.
Edit 1: Memory and CPU consumption image added below -
EDIT 2: After trying something else I'm facing a new issue -
I changed my ZoomLayout
completely to the following:
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();
}
}
}
The problem I'm facing now is that it zooms to X - 0 and Y - 0 meaning that it zooms to the top-left of the screen instead of zooming to the point between my fingers.. I think it could be related to the following:
@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;
}
I have an idea that I should detect the centre point of the 2 fingers and add that to OnScale
. How should I proceed?
EDIT 3:
Ok I finally got the zoom working perfectly on my J7 Pro running Nougat. But the problem now, is that my SurfaceView
in my S4 running Lollipop doesn't get zoomed in, instead my SurfaceView
gets moved to the top left corner. I have tested placing a ImageView
inside my custom zoomview and the image gets zoomed perfectly like expected. This is how my custom zoom class looks like now:
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);
}
Edit 4: Added video of behaviour:
Please see how the current zoom looks like after updating my zoomLayout class to edit 3 -