How to fix pinch zoom focal point in a custom view?
Using Matrix
is a really better idea - the code is much more simple and you dont have to prove your math skills ;-), see how Matrix#postTranslate
and Matrix#postScale
methods are used:
class MyView extends View {
private static final String TAG = "MyView";
private final ScaleGestureDetector mScaleDetector;
private final GestureDetector mGestureDetector;
private final Drawable mBoard;
private final float mBoardWidth;
private final float mBoardHeight;
private Matrix mMatrix;
public MyView(Context context) {
this(context, null, 0);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mBoard = ResourcesCompat.getDrawable(context.getResources(), R.drawable.chrome, null);
mBoardWidth = mBoard.getIntrinsicWidth();
mBoardHeight = mBoard.getIntrinsicHeight();
mBoard.setBounds(0, 0, (int) mBoardWidth, (int) mBoardHeight);
mMatrix = new Matrix();
mScaleDetector = new ScaleGestureDetector(context, scaleListener);
mGestureDetector = new GestureDetector(context, listener);
}
ScaleGestureDetector.OnScaleGestureListener scaleListener = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector scaleDetector) {
float factor = scaleDetector.getScaleFactor();
mMatrix.postScale(factor, factor, getWidth() / 2f, getHeight() / 2f);
ViewCompat.postInvalidateOnAnimation(MyView.this);
return true;
}
};
GestureDetector.OnGestureListener listener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dX, float dY) {
mMatrix.postTranslate(-dX, -dY);
ViewCompat.postInvalidateOnAnimation(MyView.this);
return true;
}
};
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
float scale = Math.max(w / mBoardWidth, h / mBoardHeight);
mMatrix.setScale(scale, scale);
mMatrix.postTranslate((w - scale * mBoardWidth) / 2f, (h - scale * mBoardHeight) / 2f);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.concat(mMatrix);
mBoard.draw(canvas);
canvas.restore();
}
@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouchEvent(MotionEvent e) {
mGestureDetector.onTouchEvent(e);
mScaleDetector.onTouchEvent(e);
return true;
}
}
Android: Custom view Zoom/Pinch
Add a pinch zoom capability to the graph:
How to implement pinch zoom refer : android pinch zoom
Android: Custom view Zoom/Pinch to display zooming indicators
I finally used the built-in ZoomButtonsController
Canvas pinch zoom in android
I have done this way:
Here you can add multiple images inside parent view, for sample I have added 3 images.
Screenshot:
ZoomableViewGroup.java:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
/**
* Created by hiren.patel on 25-04-2016.
*/
public class ZoomableViewGroup extends ViewGroup {
// these matrices will be used to move and zoom image
private Matrix matrix = new Matrix();
private Matrix matrixInverse = new Matrix();
private Matrix savedMatrix = new Matrix();
// we can be in one of these 3 states
private static final int NONE = 0;
private static final int DRAG = 1;
private static final int ZOOM = 2;
private int mode = NONE;
// remember some things for zooming
private PointF start = new PointF();
private PointF mid = new PointF();
private float oldDist = 1f;
private float[] lastEvent = null;
private boolean initZoomApplied = false;
private float[] mDispatchTouchEventWorkingArray = new float[2];
private float[] mOnTouchEventWorkingArray = new float[2];
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
mDispatchTouchEventWorkingArray[0] = ev.getX();
mDispatchTouchEventWorkingArray[1] = ev.getY();
mDispatchTouchEventWorkingArray = screenPointsToScaledPoints(mDispatchTouchEventWorkingArray);
ev.setLocation(mDispatchTouchEventWorkingArray[0],
mDispatchTouchEventWorkingArray[1]);
return super.dispatchTouchEvent(ev);
}
private float[] scaledPointsToScreenPoints(float[] a) {
matrix.mapPoints(a);
return a;
}
private float[] screenPointsToScaledPoints(float[] a) {
matrixInverse.mapPoints(a);
return a;
}
public ZoomableViewGroup(Context context) {
super(context);
init(context);
}
public ZoomableViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ZoomableViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
/**
* Determine the space between the first two fingers
*/
private float spacing(MotionEvent event) {
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float)Math.sqrt(x * x + y * y);
}
/**
* Calculate the mid point of the first two fingers
*/
private void midPoint(PointF point, MotionEvent event) {
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
point.set(x / 2, y / 2);
}
private void init(Context context) {
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
child.layout(l, t, l + child.getMeasuredWidth(), t + child.getMeasuredHeight());
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
float[] values = new float[9];
matrix.getValues(values);
float container_width = values[Matrix.MSCALE_X] * widthSize;
float container_height = values[Matrix.MSCALE_Y] * heightSize;
//Log.d("zoomToFit", "m width: "+container_width+" m height: "+container_height);
//Log.d("zoomToFit", "m x: "+pan_x+" m y: "+pan_y);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
if (i == 0 && !initZoomApplied && child.getWidth() > 0) {
int c_w = child.getWidth();
int c_h = child.getHeight();
//zoomToFit(c_w, c_h, container_width, container_height);
}
}
}
}
private void zoomToFit(int c_w, int c_h, float container_width, float container_height) {
float proportion_firstChild = (float) c_w / (float) c_h;
float proportion_container = container_width / container_height;
//Log.d("zoomToFit", "firstChildW: "+c_w+" firstChildH: "+c_h);
//Log.d("zoomToFit", "proportion-container: "+proportion_container);
//Log.d("zoomToFit", "proportion_firstChild: "+proportion_firstChild);
if (proportion_container < proportion_firstChild) {
float initZoom = container_height / c_h;
//Log.d("zoomToFit", "adjust height with initZoom: "+initZoom);
matrix.postScale(initZoom, initZoom);
matrix.postTranslate(-1 * (c_w * initZoom - container_width) / 2, 0);
matrix.invert(matrixInverse);
} else {
float initZoom = container_width / c_w;
//Log.d("zoomToFit", "adjust width with initZoom: "+initZoom);
matrix.postScale(initZoom, initZoom);
matrix.postTranslate(0, -1 * (c_h * initZoom - container_height) / 2);
matrix.invert(matrixInverse);
}
initZoomApplied = true;
invalidate();
}
@Override
protected void dispatchDraw(Canvas canvas) {
canvas.save();
canvas.setMatrix(matrix);
super.dispatchDraw(canvas);
canvas.restore();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// handle touch events here
mOnTouchEventWorkingArray[0] = event.getX();
mOnTouchEventWorkingArray[1] = event.getY();
mOnTouchEventWorkingArray = scaledPointsToScreenPoints(mOnTouchEventWorkingArray);
event.setLocation(mOnTouchEventWorkingArray[0], mOnTouchEventWorkingArray[1]);
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
savedMatrix.set(matrix);
start.set(event.getX(), event.getY());
mode = DRAG;
lastEvent = null;
break;
case MotionEvent.ACTION_POINTER_DOWN:
oldDist = spacing(event);
if (oldDist > 10f) {
savedMatrix.set(matrix);
midPoint(mid, event);
mode = ZOOM;
}
lastEvent = new float[4];
lastEvent[0] = event.getX(0);
lastEvent[1] = event.getX(1);
lastEvent[2] = event.getY(0);
lastEvent[3] = event.getY(1);
//d = rotation(event);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
lastEvent = null;
break;
case MotionEvent.ACTION_MOVE:
if (mode == DRAG) {
matrix.set(savedMatrix);
float dx = event.getX() - start.x;
float dy = event.getY() - start.y;
matrix.postTranslate(dx, dy);
matrix.invert(matrixInverse);
} else if (mode == ZOOM) {
float newDist = spacing(event);
if (newDist > 10f) {
matrix.set(savedMatrix);
float scale = (newDist / oldDist);
matrix.postScale(scale, scale, mid.x, mid.y);
matrix.invert(matrixInverse);
}
}
break;
}
invalidate();
return true;
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.zoomcontroldemo.ZoomableViewGroup
android:id="@+id/zoomControl"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<FrameLayout
android:id="@+id/frameLayoutParent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="center" />
</FrameLayout>
</com.zoomcontroldemo.ZoomableViewGroup>
</RelativeLayout>
MainActivity.java:
public class MainActivity extends Activity {
private FrameLayout frameLayoutParent;
private ImageView imageView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
frameLayoutParent = (FrameLayout)findViewById(R.id.frameLayoutParent);
imageView = (ImageView) findViewById(R.id.imageView);
Bitmap originalBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.desert);
imageView.setImageBitmap(originalBitmap);
createAndAddLocator(50, 50, 100, 100);
createAndAddLocator(50, 50, 200, 200);
createAndAddLocator(50, 50, 300, 300);
}
private void createAndAddLocator(int width, int height, int x, int y){
ImageView imageView = new ImageView(MainActivity.this);
imageView.setImageDrawable(getResources().getDrawable(R.drawable.ic_launcher));
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(width, height);
params.leftMargin = x;
params.topMargin = y;
frameLayoutParent.addView(imageView, params);
}
}
Hope this would help you.
Related Topics
Difference Between Android-Support-V7-Appcompat and Android-Support-V4
Low-Latency Audio Playback on Android
Android Background Image Memory Usage
Actionbar Logo Centered and Action Items on Sides
How to Create Custom UI for Android Mediacontroller
Navigation Drawer Semi-Transparent Over Status Bar Not Working
Android Persistence Room: "Cannot Figure Out How to Read This Field from a Cursor"
Jni and Gradle in Android Studio
How to Hide a Button Programmatically
How to Add a Fast-Scroller to the Recyclerview
How to Attach the Android Support Library Source in Eclipse
How to Change Background Color in Android App
How to Use Getsharedpreferences in Android
Version Conflict Updating to 8.4.0
Fragment Oncreateview and Onactivitycreated Called Twice