Android Image View Pinch Zooming
AndroidAndroid LayoutAndroid IntentAndroid WidgetAndroid Problem Overview
I am using code sample from [Making Sense of Multitouch][1]
[1]: http://android-developers.blogspot.in/2010/06/making-sense-of-multitouch.html
for zooming image view. On ScaleListener I added ScaleGestureDetector.getFocusX() and getFocusY()
for content to zoom about the focal point of the gesture. It is working fine.
The problem is, on first multitouch the entire Image drawing position is changing to the current touch point and zooming it from there. Could you help me to resolve this issue?
Here is My Code Sample For TouchImageView.
public class TouchImageViewSample extends ImageView {
private Paint borderPaint = null;
private Paint backgroundPaint = null;
private float mPosX = 0f;
private float mPosY = 0f;
private float mLastTouchX;
private float mLastTouchY;
private static final int INVALID_POINTER_ID = -1;
private static final String LOG_TAG = "TouchImageView";
// The ‘active pointer’ is the one currently moving our object.
private int mActivePointerId = INVALID_POINTER_ID;
public TouchImageViewSample(Context context) {
this(context, null, 0);
}
public TouchImageViewSample(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
private ScaleGestureDetector mScaleDetector;
private float mScaleFactor = 1.f;
// Existing code ...
public TouchImageViewSample(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// Create our ScaleGestureDetector
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
borderPaint = new Paint();
borderPaint.setARGB(255, 255, 128, 0);
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(4);
backgroundPaint = new Paint();
backgroundPaint.setARGB(32, 255, 255, 255);
backgroundPaint.setStyle(Paint.Style.FILL);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
// Let the ScaleGestureDetector inspect all events.
mScaleDetector.onTouchEvent(ev);
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
mLastTouchX = x;
mLastTouchY = y;
mActivePointerId = ev.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE: {
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(pointerIndex);
final float y = ev.getY(pointerIndex);
// Only move if the ScaleGestureDetector isn't processing a gesture.
if (!mScaleDetector.isInProgress()) {
final float dx = x - mLastTouchX;
final float dy = y - mLastTouchY;
mPosX += dx;
mPosY += dy;
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 = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(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 = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
break;
}
}
return true;
}
/*
* (non-Javadoc)
*
* @see android.view.View#draw(android.graphics.Canvas)
*/
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
canvas.drawRect(0, 0, getWidth() - 1, getHeight() - 1, borderPaint);
}
@Override
public void onDraw(Canvas canvas) {
canvas.drawRect(0, 0, getWidth() - 1, getHeight() - 1, backgroundPaint);
if (this.getDrawable() != null) {
canvas.save();
canvas.translate(mPosX, mPosY);
Matrix matrix = new Matrix();
matrix.postScale(mScaleFactor, mScaleFactor, pivotPointX,
pivotPointY);
// canvas.setMatrix(matrix);
canvas.drawBitmap(
((BitmapDrawable) this.getDrawable()).getBitmap(), matrix,
null);
// this.getDrawable().draw(canvas);
canvas.restore();
}
}
/*
* (non-Javadoc)
*
* @see
* android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable
* )
*/
@Override
public void setImageDrawable(Drawable drawable) {
// Constrain to given size but keep aspect ratio
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
mLastTouchX = mPosX = 0;
mLastTouchY = mPosY = 0;
int borderWidth = (int) borderPaint.getStrokeWidth();
mScaleFactor = Math.min(((float) getLayoutParams().width - borderWidth)
/ width, ((float) getLayoutParams().height - borderWidth)
/ height);
pivotPointX = (((float) getLayoutParams().width - borderWidth) - (int) (width * mScaleFactor)) / 2;
pivotPointY = (((float) getLayoutParams().height - borderWidth) - (int) (height * mScaleFactor)) / 2;
super.setImageDrawable(drawable);
}
float pivotPointX = 0f;
float pivotPointY = 0f;
private class ScaleListener extends
ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(ScaleGestureDetector detector) {
mScaleFactor *= detector.getScaleFactor();
pivotPointX = detector.getFocusX();
pivotPointY = detector.getFocusY();
Log.d(LOG_TAG, "mScaleFactor " + mScaleFactor);
Log.d(LOG_TAG, "pivotPointY " + pivotPointY + ", pivotPointX= "
+ pivotPointX);
mScaleFactor = Math.max(0.05f, mScaleFactor);
invalidate();
return true;
}
}
And here how I used it within my activity.
ImageView imageView = (ImageView) findViewById(R.id.imgView);
int hMargin = (int) (displayMetrics.widthPixels * .10);
int vMargin = (int) (displayMetrics.heightPixels * .10);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(displayMetrics.widthPixels - (hMargin * 2), (int)(displayMetrics.heightPixels - btnCamera.getHeight()) - (vMargin * 2));
params.leftMargin = hMargin;
params.topMargin = vMargin;
imageView.setLayoutParams(params);
imageView.setImageDrawable(drawable);
Android Solutions
Solution 1 - Android
You can use this class : TouchImageView
Solution 2 - Android
##Using a ScaleGestureDetector
When learning a new concept I don't like using libraries or code dumps. I found a good description here and in the documentation of how to resize an image by pinching. This answer is a slightly modified summary. You will probably want to add more functionality later, but it will help you get started.
https://i.stack.imgur.com/RfPGy.gif" width="200" alt="Animated gif: Scale image example">
##Layout
The ImageView
just uses the app logo since it is already available. You can replace it with any image you like, though.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
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:src="@mipmap/ic_launcher"
android:layout_centerInParent="true"/>
</RelativeLayout>
##Activity
We use a ScaleGestureDetector
on the activity to listen to touch events. When a scale (ie, pinch) gesture is detected, then the scale factor is used to resize the ImageView
.
public class MainActivity extends AppCompatActivity {
private ScaleGestureDetector mScaleGestureDetector;
private float mScaleFactor = 1.0f;
private ImageView mImageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// initialize the view and the gesture detector
mImageView = findViewById(R.id.imageView);
mScaleGestureDetector = new ScaleGestureDetector(this, new ScaleListener());
}
// this redirects all touch events in the activity to the gesture detector
@Override
public boolean onTouchEvent(MotionEvent event) {
return mScaleGestureDetector.onTouchEvent(event);
}
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
// when a scale gesture is detected, use it to resize the image
@Override
public boolean onScale(ScaleGestureDetector scaleGestureDetector){
mScaleFactor *= scaleGestureDetector.getScaleFactor();
mImageView.setScaleX(mScaleFactor);
mImageView.setScaleY(mScaleFactor);
return true;
}
}
}
##Notes
-
Although the activity had the gesture detector in the example above, it could have also been set on the image view itself.
-
You can limit the size of the scaling with something like
mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));
-
Thanks again to Pinch-to-zoom with multi-touch gestures In Android
-
Use Ctrl + mouse drag to simulate a pinch gesture in the emulator.
Going on
You will probably want to do other things like panning and scaling to some focus point. You can develop these things yourself, but if you would like to use a pre-made custom view, copy TouchImageView.java
into your project and use it like a normal ImageView
. It worked well for me and I only ran into one bug. I plan to further edit the code to remove the warning and the parts that I don't need. You can do the same.
Solution 3 - Android
I made my own custom imageview with pinch to zoom. There is no limits/borders on Chirag Ravals code, so user can drag the image off the screen. This will fix it.
Here is the CustomImageView class:
public class CustomImageVIew extends ImageView implements OnTouchListener {
private Matrix matrix = new Matrix();
private Matrix savedMatrix = new Matrix();
static final int NONE = 0;
static final int DRAG = 1;
static final int ZOOM = 2;
private int mode = NONE;
private PointF mStartPoint = new PointF();
private PointF mMiddlePoint = new PointF();
private Point mBitmapMiddlePoint = new Point();
private float oldDist = 1f;
private float matrixValues[] = {0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f};
private float scale;
private float oldEventX = 0;
private float oldEventY = 0;
private float oldStartPointX = 0;
private float oldStartPointY = 0;
private int mViewWidth = -1;
private int mViewHeight = -1;
private int mBitmapWidth = -1;
private int mBitmapHeight = -1;
private boolean mDraggable = false;
public CustomImageVIew(Context context) {
this(context, null, 0);
}
public CustomImageVIew(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomImageVIew(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.setOnTouchListener(this);
}
@Override
public void onSizeChanged (int w, int h, int oldw, int oldh){
super.onSizeChanged(w, h, oldw, oldh);
mViewWidth = w;
mViewHeight = h;
}
public void setBitmap(Bitmap bitmap){
if(bitmap != null){
setImageBitmap(bitmap);
mBitmapWidth = bitmap.getWidth();
mBitmapHeight = bitmap.getHeight();
mBitmapMiddlePoint.x = (mViewWidth / 2) - (mBitmapWidth / 2);
mBitmapMiddlePoint.y = (mViewHeight / 2) - (mBitmapHeight / 2);
matrix.postTranslate(mBitmapMiddlePoint.x, mBitmapMiddlePoint.y);
this.setImageMatrix(matrix);
}
}
@Override
public boolean onTouch(View v, MotionEvent event){
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
savedMatrix.set(matrix);
mStartPoint.set(event.getX(), event.getY());
mode = DRAG;
break;
case MotionEvent.ACTION_POINTER_DOWN:
oldDist = spacing(event);
if(oldDist > 10f){
savedMatrix.set(matrix);
midPoint(mMiddlePoint, event);
mode = ZOOM;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
break;
case MotionEvent.ACTION_MOVE:
if(mode == DRAG){
drag(event);
} else if(mode == ZOOM){
zoom(event);
}
break;
}
return true;
}
public void drag(MotionEvent event){
matrix.getValues(matrixValues);
float left = matrixValues[2];
float top = matrixValues[5];
float bottom = (top + (matrixValues[0] * mBitmapHeight)) - mViewHeight;
float right = (left + (matrixValues[0] * mBitmapWidth)) -mViewWidth;
float eventX = event.getX();
float eventY = event.getY();
float spacingX = eventX - mStartPoint.x;
float spacingY = eventY - mStartPoint.y;
float newPositionLeft = (left < 0 ? spacingX : spacingX * -1) + left;
float newPositionRight = (spacingX) + right;
float newPositionTop = (top < 0 ? spacingY : spacingY * -1) + top;
float newPositionBottom = (spacingY) + bottom;
boolean x = true;
boolean y = true;
if(newPositionRight < 0.0f || newPositionLeft > 0.0f){
if(newPositionRight < 0.0f && newPositionLeft > 0.0f){
x = false;
} else{
eventX = oldEventX;
mStartPoint.x = oldStartPointX;
}
}
if(newPositionBottom < 0.0f || newPositionTop > 0.0f){
if(newPositionBottom < 0.0f && newPositionTop > 0.0f){
y = false;
} else{
eventY = oldEventY;
mStartPoint.y = oldStartPointY;
}
}
if(mDraggable){
matrix.set(savedMatrix);
matrix.postTranslate(x? eventX - mStartPoint.x : 0, y? eventY - mStartPoint.y : 0);
this.setImageMatrix(matrix);
if(x)oldEventX = eventX;
if(y)oldEventY = eventY;
if(x)oldStartPointX = mStartPoint.x;
if(y)oldStartPointY = mStartPoint.y;
}
}
public void zoom(MotionEvent event){
matrix.getValues(matrixValues);
float newDist = spacing(event);
float bitmapWidth = matrixValues[0] * mBitmapWidth;
float bimtapHeight = matrixValues[0] * mBitmapHeight;
boolean in = newDist > oldDist;
if(!in && matrixValues[0] < 1){
return;
}
if(bitmapWidth > mViewWidth || bimtapHeight > mViewHeight){
mDraggable = true;
} else{
mDraggable = false;
}
float midX = (mViewWidth / 2);
float midY = (mViewHeight / 2);
matrix.set(savedMatrix);
scale = newDist / oldDist;
matrix.postScale(scale, scale, bitmapWidth > mViewWidth ? mMiddlePoint.x : midX, bimtapHeight > mViewHeight ? mMiddlePoint.y : midY);
this.setImageMatrix(matrix);
}
/** 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);
}
}
This is how you can use it in your activity:
CustomImageVIew mImageView = (CustomImageVIew)findViewById(R.id.customImageVIew1);
mImage.setBitmap(your bitmap);
And layout:
<your.package.name.CustomImageVIew
android:id="@+id/customImageVIew1"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_marginBottom="15dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="15dp"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:scaleType="matrix"/> // important
Solution 4 - Android
Add bellow line in build.gradle:
compile 'com.commit451:PhotoView:1.2.4'
or
compile 'com.github.chrisbanes:PhotoView:1.3.0'
In Java file:
PhotoViewAttacher photoAttacher;
photoAttacher= new PhotoViewAttacher(Your_Image_View);
photoAttacher.update();
Solution 5 - Android
In the TouchImageViewSample
class scaling happens around the pivot point. Pixel belong to the pivot point doesn't get effected by scaling of the image. When you are changing the pivot point, the view get redrawn and scaling happens around the new pivot point. This changes the location of previous pivot point and you see this as the image get shifted every time you touching down on the image. You have to compensate this shifting error by translating the image. See how this is done in my ZoomGestureDetector.updatePivotPoint()
method.
ZoomGestureDetector
I created custom zoom gesture detector class. It can do scaling, translation and rotation at the same time. It also supports fling animation.
https://i.stack.imgur.com/83Zcr.gif" width="400" height="400">
import android.graphics.Canvas
import android.graphics.Matrix
import android.view.MotionEvent
import android.view.VelocityTracker
import androidx.core.math.MathUtils
import androidx.dynamicanimation.animation.FlingAnimation
import androidx.dynamicanimation.animation.FloatValueHolder
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.atan2
class ZoomGestureDetector(private val listener: Listener) {
companion object {
const val MIN_SCALE = 0.01f
const val MAX_SCALE = 100f
const val MIN_FLING_VELOCITY = 50f
const val MAX_FLING_VELOCITY = 8000f
}
// public
var isZoomEnabled: Boolean = true
var isScaleEnabled: Boolean = true
var isRotationEnabled: Boolean = true
var isTranslationEnabled: Boolean = true
var isFlingEnabled: Boolean = true
// local
private val mDrawMatrix: Matrix = Matrix()
private val mTouchMatrix: Matrix = Matrix()
private val mPointerMap: HashMap<Int, Position> = HashMap()
private val mTouchPoint: FloatArray = floatArrayOf(0f, 0f)
private val mPivotPoint: FloatArray = floatArrayOf(0f, 0f)
// transformations
private var mTranslationX: Float = 0f
private var mTranslationY: Float = 0f
private var mScaling: Float = 1f
private var mPivotX: Float = 0f
private var mPivotY: Float = 0f
private var mRotation: Float = 0f
// previous values
private var mPreviousFocusX: Float = 0f
private var mPreviousFocusY: Float = 0f
private var mPreviousTouchSpan: Float = 1f
// fling related
private var mVelocityTracker: VelocityTracker? = null
private var mFlingAnimX: FlingAnimation? = null
private var mFlingAnimY: FlingAnimation? = null
fun updateTouchLocation(event: MotionEvent) {
mTouchPoint[0] = event.x
mTouchPoint[1] = event.y
mTouchMatrix.mapPoints(mTouchPoint)
event.setLocation(mTouchPoint[0], mTouchPoint[1])
}
fun updateCanvasMatrix(canvas: Canvas) {
canvas.setMatrix(mDrawMatrix)
}
fun onTouchEvent(event: MotionEvent): Boolean {
if (isZoomEnabled) {
// update velocity tracker
if (isFlingEnabled) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain()
}
mVelocityTracker?.addMovement(event)
}
// handle touch events
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
// update focus point
mPreviousFocusX = event.x
mPreviousFocusY = event.y
event.savePointers()
// cancel ongoing fling animations
if (isFlingEnabled) {
mFlingAnimX?.cancel()
mFlingAnimY?.cancel()
}
}
MotionEvent.ACTION_POINTER_DOWN -> {
updateTouchParameters(event)
}
MotionEvent.ACTION_POINTER_UP -> {
// Check the dot product of current velocities.
// If the pointer that left was opposing another velocity vector, clear.
if (isFlingEnabled) {
mVelocityTracker?.let { tracker ->
tracker.computeCurrentVelocity(1000, MAX_FLING_VELOCITY)
val upIndex: Int = event.actionIndex
val id1: Int = event.getPointerId(upIndex)
val x1 = tracker.getXVelocity(id1)
val y1 = tracker.getYVelocity(id1)
for (i in 0 until event.pointerCount) {
if (i == upIndex) continue
val id2: Int = event.getPointerId(i)
val x = x1 * tracker.getXVelocity(id2)
val y = y1 * tracker.getYVelocity(id2)
val dot = x + y
if (dot < 0) {
tracker.clear()
break
}
}
}
}
updateTouchParameters(event)
}
MotionEvent.ACTION_UP -> {
// do fling animation
if (isFlingEnabled) {
mVelocityTracker?.let { tracker ->
val pointerId: Int = event.getPointerId(0)
tracker.computeCurrentVelocity(1000, MAX_FLING_VELOCITY)
val velocityY: Float = tracker.getYVelocity(pointerId)
val velocityX: Float = tracker.getXVelocity(pointerId)
if (abs(velocityY) > MIN_FLING_VELOCITY || abs(velocityX) > MIN_FLING_VELOCITY) {
val translateX = mTranslationX
val translateY = mTranslationY
val valueHolder = FloatValueHolder()
mFlingAnimX = FlingAnimation(valueHolder).apply {
setStartVelocity(velocityX)
setStartValue(0f)
addUpdateListener { _, value, _ ->
mTranslationX = translateX + value
updateDrawMatrix()
listener.onZoom(mScaling, mRotation, mTranslationX to mTranslationY, mPivotX to mPivotY)
}
addEndListener { _, _, _, _ ->
updateTouchMatrix()
}
start()
}
mFlingAnimY = FlingAnimation(valueHolder).apply {
setStartVelocity(velocityY)
setStartValue(0f)
addUpdateListener { _, value, _ ->
mTranslationY = translateY + value
updateDrawMatrix()
listener.onZoom(mScaling, mRotation, mTranslationX to mTranslationY, mPivotX to mPivotY)
}
addEndListener { _, _, _, _ ->
updateTouchMatrix()
}
start()
}
}
tracker.recycle()
mVelocityTracker = null
}
}
}
MotionEvent.ACTION_MOVE -> {
val (focusX, focusY) = event.focalPoint()
if (event.pointerCount > 1) {
if (isScaleEnabled) {
val touchSpan = event.touchSpan(focusX, focusY)
mScaling *= scaling(touchSpan)
mScaling = MathUtils.clamp(mScaling, MIN_SCALE, MAX_SCALE)
mPreviousTouchSpan = touchSpan
}
if (isRotationEnabled) {
mRotation += event.rotation(focusX, focusY)
}
if (isTranslationEnabled) {
val (translationX, translationY) = translation(focusX, focusY)
mTranslationX += translationX
mTranslationY += translationY
}
} else {
if (isTranslationEnabled) {
val (translationX, translationY) = translation(focusX, focusY)
mTranslationX += translationX
mTranslationY += translationY
}
}
mPreviousFocusX = focusX
mPreviousFocusY = focusY
updateTouchMatrix()
updateDrawMatrix()
event.savePointers()
listener.onZoom(mScaling, mRotation, mTranslationX to mTranslationY, mPivotX to mPivotY)
}
}
return true
}
return false
}
// update focus point, touch span and pivot point
private fun updateTouchParameters(event: MotionEvent) {
val (focusX, focusY) = event.focalPoint()
mPreviousFocusX = focusX
mPreviousFocusY = focusY
mPreviousTouchSpan = event.touchSpan(focusX, focusY)
updatePivotPoint(focusX, focusY)
updateTouchMatrix()
updateDrawMatrix()
event.savePointers()
listener.onZoom(mScaling, mRotation, mTranslationX to mTranslationY, mPivotX to mPivotY)
}
// touch matrix is used to transform touch points
// on the child view and to find pivot point
private fun updateTouchMatrix() {
mTouchMatrix.reset()
mTouchMatrix.preTranslate(-mTranslationX, -mTranslationY)
mTouchMatrix.postRotate(-mRotation, mPivotX, mPivotY)
mTouchMatrix.postScale(1f / mScaling, 1f / mScaling, mPivotX, mPivotY)
}
// draw matrix is used to transform child view when drawing on the canvas
private fun updateDrawMatrix() {
mDrawMatrix.reset()
mDrawMatrix.preScale(mScaling, mScaling, mPivotX, mPivotY)
mDrawMatrix.preRotate(mRotation, mPivotX, mPivotY)
mDrawMatrix.postTranslate(mTranslationX, mTranslationY)
}
// this updates the pivot point and translation error caused by changing the pivot point
private fun updatePivotPoint(focusX: Float, focusY: Float) {
// update point
mPivotPoint[0] = focusX
mPivotPoint[1] = focusY
mTouchMatrix.mapPoints(mPivotPoint)
mPivotX = mPivotPoint[0]
mPivotY = mPivotPoint[1]
// correct pivot error
mDrawMatrix.mapPoints(mPivotPoint)
mTranslationX -= mTranslationX + mPivotX - mPivotPoint[0]
mTranslationY -= mTranslationY + mPivotY - mPivotPoint[1]
}
private fun MotionEvent.focalPoint(): Pair<Float, Float> {
val upIndex = if (actionMasked == MotionEvent.ACTION_POINTER_UP) actionIndex else -1
var sumX = 0f
var sumY = 0f
var sumCount = 0
for (pointerIndex in 0 until pointerCount) {
if (pointerIndex == upIndex) continue
sumX += getX(pointerIndex)
sumY += getY(pointerIndex)
sumCount++
}
val focusX = sumX / sumCount
val focusY = sumY / sumCount
return focusX to focusY
}
private fun MotionEvent.touchSpan(
currentFocusX: Float,
currentFocusY: Float
): Float {
var spanSumX = 0f
var spanSumY = 0f
var sumCount = 0
val ignoreIndex = if (actionMasked == MotionEvent.ACTION_POINTER_UP) actionIndex else -1
for (pointerIndex in 0 until pointerCount) {
if (pointerIndex == ignoreIndex) continue
spanSumX += abs(currentFocusX - getX(pointerIndex))
spanSumY += abs(currentFocusY - getY(pointerIndex))
sumCount++
}
if (sumCount > 1) {
val spanX = spanSumX / sumCount
val spanY = spanSumY / sumCount
return spanX + spanY
}
return mPreviousTouchSpan
}
private fun scaling(currentTouchSpan: Float): Float {
return currentTouchSpan / mPreviousTouchSpan
}
private fun MotionEvent.rotation(
currentFocusX: Float,
currentFocusY: Float
): Float {
var rotationSum = 0f
var weightSum = 0f
for (pointerIndex in 0 until pointerCount) {
val pointerId = getPointerId(pointerIndex)
val x1 = getX(pointerIndex)
val y1 = getY(pointerIndex)
val (x2, y2) = mPointerMap[pointerId] ?: continue
val dx1 = x1 - currentFocusX
val dy1 = y1 - currentFocusY
val dx2 = x2 - currentFocusX
val dy2 = y2 - currentFocusY
// dot product is proportional to the cosine of the angle
// the determinant is proportional to its sine
// sign of the rotation tells if it is clockwise or counter-clockwise
val dot = dx1 * dx2 + dy1 * dy2
val det = dy1 * dx2 - dx1 * dy2
val rotation = atan2(det, dot)
val weight = abs(dx1) + abs(dy1)
rotationSum += rotation * weight
weightSum += weight
}
if (weightSum > 0f) {
val rotation = rotationSum / weightSum
return rotation * 180f / PI.toFloat()
}
return 0f
}
private fun translation(
currentFocusX: Float,
currentFocusY: Float
): Pair<Float, Float> {
return (currentFocusX - mPreviousFocusX) to (currentFocusY - mPreviousFocusY)
}
private fun MotionEvent.savePointers() {
mPointerMap.clear()
for (pointerIndex in 0 until pointerCount) {
val id = getPointerId(pointerIndex)
val x = getX(pointerIndex)
val y = getY(pointerIndex)
mPointerMap[id] = x to y
}
}
interface Listener {
fun onZoom(scaling: Float, rotation: Float, translation: Position, pivot: Position)
}
}
typealias Position = Pair<Float, Float>
I used ZoomGestureDetector
in a FrameLayout
as below.
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.widget.FrameLayout
class ZoomLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr),
ZoomGestureDetector.Listener {
private val gestureDetector = ZoomGestureDetector(this)
var isZoomEnabled
get() = gestureDetector.isZoomEnabled
set(value) {
gestureDetector.isZoomEnabled = value
}
var isScaleEnabled
get() = gestureDetector.isScaleEnabled
set(value) {
gestureDetector.isScaleEnabled = value
}
var isRotationEnabled
get() = gestureDetector.isRotationEnabled
set(value) {
gestureDetector.isRotationEnabled = value
}
var isTranslationEnabled
get() = gestureDetector.isTranslationEnabled
set(value) {
gestureDetector.isTranslationEnabled = value
}
var isFlingEnabled
get() = gestureDetector.isFlingEnabled
set(value) {
gestureDetector.isFlingEnabled = value
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
if (isZoomEnabled) return true
gestureDetector.updateTouchLocation(event)
return super.onInterceptTouchEvent(event)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
return gestureDetector.onTouchEvent(event)
}
override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
gestureDetector.updateCanvasMatrix(canvas)
return super.drawChild(canvas, child, drawingTime)
}
override fun onZoom(scaling: Float, rotation: Float, translation: Position, pivot: Position) {
invalidate()
}
}
Solution 6 - Android
Custom zoom view in Kotlin
import android.content.Context
import android.graphics.Matrix
import android.graphics.PointF
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener
import androidx.appcompat.widget.AppCompatImageView
class ZoomImageview : AppCompatImageView {
var matri: Matrix? = null
var mode = NONE
// Remember some things for zooming
var last = PointF()
var start = PointF()
var minScale = 1f
var maxScale = 3f
lateinit var m: FloatArray
var viewWidth = 0
var viewHeight = 0
var saveScale = 1f
protected var origWidth = 0f
protected var origHeight = 0f
var oldMeasuredWidth = 0
var oldMeasuredHeight = 0
var mScaleDetector: ScaleGestureDetector? = null
var contex: Context? = null
constructor(context: Context) : super(context) {
sharedConstructing(context)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
sharedConstructing(context)
}
private fun sharedConstructing(context: Context) {
super.setClickable(true)
this.contex= context
mScaleDetector = ScaleGestureDetector(context, ScaleListener())
matri = Matrix()
m = FloatArray(9)
imageMatrix = matri
scaleType = ScaleType.MATRIX
setOnTouchListener { v, event ->
mScaleDetector!!.onTouchEvent(event)
val curr = PointF(event.x, event.y)
when (event.action) {
MotionEvent.ACTION_DOWN -> {
last.set(curr)
start.set(last)
mode = DRAG
}
MotionEvent.ACTION_MOVE -> if (mode == DRAG) {
val deltaX = curr.x - last.x
val deltaY = curr.y - last.y
val fixTransX = getFixDragTrans(deltaX, viewWidth.toFloat(), origWidth * saveScale)
val fixTransY = getFixDragTrans(deltaY, viewHeight.toFloat(), origHeight * saveScale)
matri!!.postTranslate(fixTransX, fixTransY)
fixTrans()
last[curr.x] = curr.y
}
MotionEvent.ACTION_UP -> {
mode = NONE
val xDiff = Math.abs(curr.x - start.x).toInt()
val yDiff = Math.abs(curr.y - start.y).toInt()
if (xDiff < CLICK && yDiff < CLICK) performClick()
}
MotionEvent.ACTION_POINTER_UP -> mode = NONE
}
imageMatrix = matri
invalidate()
true // indicate event was handled
}
}
fun setMaxZoom(x: Float) {
maxScale = x
}
private inner class ScaleListener : SimpleOnScaleGestureListener() {
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
mode = ZOOM
return true
}
override fun onScale(detector: ScaleGestureDetector): Boolean {
var mScaleFactor = detector.scaleFactor
val origScale = saveScale
saveScale *= mScaleFactor
if (saveScale > maxScale) {
saveScale = maxScale
mScaleFactor = maxScale / origScale
} else if (saveScale < minScale) {
saveScale = minScale
mScaleFactor = minScale / origScale
}
if (origWidth * saveScale <= viewWidth || origHeight * saveScale <= viewHeight) matri!!.postScale(mScaleFactor, mScaleFactor, viewWidth / 2.toFloat(), viewHeight / 2.toFloat()) else matri!!.postScale(mScaleFactor, mScaleFactor, detector.focusX, detector.focusY)
fixTrans()
return true
}
}
fun fixTrans() {
matri!!.getValues(m)
val transX = m[Matrix.MTRANS_X]
val transY = m[Matrix.MTRANS_Y]
val fixTransX = getFixTrans(transX, viewWidth.toFloat(), origWidth * saveScale)
val fixTransY = getFixTrans(transY, viewHeight.toFloat(), origHeight * saveScale)
if (fixTransX != 0f || fixTransY != 0f) matri!!.postTranslate(fixTransX, fixTransY)
}
fun getFixTrans(trans: Float, viewSize: Float, contentSize: Float): Float {
val minTrans: Float
val maxTrans: Float
if (contentSize <= viewSize) {
minTrans = 0f
maxTrans = viewSize - contentSize
} else {
minTrans = viewSize - contentSize
maxTrans = 0f
}
if (trans < minTrans) return -trans + minTrans
if (trans > maxTrans) return -trans + maxTrans
return 0f
}
fun getFixDragTrans(delta: Float, viewSize: Float, contentSize: Float): Float {
if (contentSize <= viewSize) {
return 0f
} else {
return delta
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
viewWidth = MeasureSpec.getSize(widthMeasureSpec)
viewHeight = MeasureSpec.getSize(heightMeasureSpec)
//
// Rescales image on rotation
//
if (oldMeasuredHeight == viewWidth && oldMeasuredHeight == viewHeight || viewWidth == 0 || viewHeight == 0) return
oldMeasuredHeight = viewHeight
oldMeasuredWidth = viewWidth
if (saveScale == 1f) {
//Fit to screen.
val scale: Float
val drawable = drawable
if (drawable == null || drawable.intrinsicWidth == 0 || drawable.intrinsicHeight == 0) return
val bmWidth = drawable.intrinsicWidth
val bmHeight = drawable.intrinsicHeight
Log.d("bmSize", "bmWidth: $bmWidth bmHeight : $bmHeight")
val scaleX = viewWidth.toFloat() / bmWidth.toFloat()
val scaleY = viewHeight.toFloat() / bmHeight.toFloat()
scale = Math.min(scaleX, scaleY)
matri!!.setScale(scale, scale)
// Center the image
var redundantYSpace = viewHeight.toFloat() - scale * bmHeight.toFloat()
var redundantXSpace = viewWidth.toFloat() - scale * bmWidth.toFloat()
redundantYSpace /= 2.toFloat()
redundantXSpace /= 2.toFloat()
matri!!.postTranslate(redundantXSpace, redundantYSpace)
origWidth = viewWidth - 2 * redundantXSpace
origHeight = viewHeight - 2 * redundantYSpace
imageMatrix = matri
}
fixTrans()
}
companion object {
// We can be in one of these 3 states
const val NONE = 0
const val DRAG = 1
const val ZOOM = 2
const val CLICK = 3
}
}
Solution 7 - Android
I made code for imageview with pinch to zoom using zoomageview. so user can drag the image off the screen and zoom-In , zoom-out the image.
You can follow this link
to get the Step By Step
Code and also given Output Screenshot.