This article has been published exclusively by guolin_blog, an official wechat account

PhotoView goes from 0 to 1, 👎 to 👍

⚠️: Considering that some Java developers are not familiar with KT, this article uses the Java language to write! Kotlin/Java version of the source code attached at the bottom

Take a look at today’s renderings:

Transverse images Longitudinal image

Requirements:

  • The picture
    • Horizontal pictures default left and right side up and down white
    • Always want pictures up and down the default side left or so
  • Double click to zoom in/out and move with one finger after zoom in
  • Double refers to enlarge
  • The minimum size should not be smaller than the original image, and the maximum size should not be 1.5 times larger than the image

Most basic, draw a picture!

public class PhotoView2 extends View {
	 // The image needs to be manipulated
    private Bitmap mBitMap;

    / / brush
    Paint mPaint = new Paint();	

    public PhotoView2(Context context) {
        this(context, null);
    }

   public PhotoView2(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    @SuppressLint("CustomViewStyleable")
    public PhotoView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
		 TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PhotoView);

        Drawable drawable = typedArray.getDrawable(R.styleable.PhotoView_android_src);
        if (drawable == null)
            mBitMap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.error);
        else
            mBitMap = toBitMap(drawable, 800.800);

        // Recycle to avoid memory leaks
        typedArray.recycle();
	}
	
	 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // draw a picture at position 0,0
        canvas.drawBitmap(mBitMap, 0.0, mPaint);
    }


	// drawable -> bitmap
	private Bitmap toBitMap(Drawable drawable, int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0.0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);
        returnbitmap; }}Copy the code

This part of the code is relatively simple, over a matter of time!

Image center

You know, when you’re customizing a View

The View is executed as follows: -> Constructor -> onMeasure() -> onSizeChanged() -> onDraw()

Get the offset before onDraw

#PhotoView2.java

	 // Move the image to the center of View
    float offsetWidth = 0f;
    float offsetHeight = 0f;
    
	@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
 		offsetWidth = getWidth() / 2f - mBitMap.getWidth() / 2f;
        offsetHeight = getHeight() / 2f - mBitMap.getHeight() / 2f;
}

	 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // Parameter 1: image
        // Image x position
        // The position of the image y
        // Parameter 4: brush
        canvas.drawBitmap(mBitMap, offsetWidth, offsetHeight, mPaint);
    }
Copy the code

It doesn’t matter if you don’t understand it, come to a picture at a glance!

Current effect




This piece is still relatively basic thing! Next to improve the difficulty…..

zoom

To meet requirement one, enlarge the image to the right place

Requirements:

  • The picture
- Default left/right margin for landscape images - Default left/right margin for portrait imagesCopy the code

Requirement 1 Auxiliary diagram:

Longitudinal image Transverse images

Let’s start with the code:

#PhotoView2.java

// Scale the image before scaling
    float smallScale = 0f;
    
    // Zoom the image
    float bigScale = 0f;

    // The current ratio
    float currentScale = 0f;

    // Scale multiple
    private static final float ZOOM_SCALE = 1.5 f;
    
 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

     	/ / the view
        float viewScale = (float) getWidth() / (float) getHeight();
        
        // Image scale
        float bitScale = (float) mBitMap.getWidth() / (float) mBitMap.getHeight();

        // If the image scale is larger than the view scale
        if (bitScale > viewScale) {
            // Horizontal image
            smallScale = (float) getWidth() / (float) mBitMap.getWidth();
            bigScale = (float) getHeight() / (float) mBitMap.getHeight() * ZOOM_SCALE;
        } else {
            // Vertical image
            smallScale = (float) getHeight() / (float) mBitMap.getHeight();
            bigScale = (float) getWidth() / (float) mBitMap.getWidth() * ZOOM_SCALE;
        }

        // Current scale = scale before scaling
        currentScale = smallScale;
    }

	 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        /* * The x and y scales are the same [currentScale] */ for simplicity
        canvas.scale(currentScale, currentScale, getWidth() / 2f, getHeight() / 2f);

        canvas.drawBitmap(mBitMap, offsetWidth, offsetHeight, mPaint);
    }
Copy the code

SmallScale /bigScale Take the horizontal picture as an example, put in the parameters, a picture to understand!

  • SmallScale Scales the original image by 1.5 times
  • BigScale scales 2.4 times

Take the horizontal image key code as an example:

// Horizontal image
smallScale = (float) getWidth() / (float) mBitMap.getWidth();
bigScale = (float) getHeight() / (float) mBitMap.getHeight() * ZOOM_SCALE;
Copy the code

If the image is landscape, prove height > width

So the smallScale scale is width/bitmap.width, leaving the left and right white, and the top and bottom white

Height * 1.5 is used here to prevent images from being too small to cover the entire screen

Current effect




Double-click the amplification

When it comes to double click amplification, we have to mention android’s own classes that listen for double clicks

#PhotoView2.java

	// Double-click the gesture to listen
    static class PhotoGestureListener extends GestureDetector.SimpleOnGestureListener {
        // Click condition: Triggered when [ACTION_UP] is lifted
        // Double click: triggered when [ACTION_POINTER_UP] is lifted the second time
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            Log.i("szjPhotoGestureListener"."Lifted onSingleTapUp");
            return super.onSingleTapUp(e);
        }

        // Long time trigger [300ms]
        @Override
        public void onLongPress(MotionEvent e) {
            Log.i("szjPhotoGestureListener"."Long press onLongPress");
            super.onLongPress(e);
        }

        // Trigger an action_move-like event while sliding
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            Log.i("szjPhotoGestureListener"."I slid onScroll");
            return super.onScroll(e1, e2, distanceX, distanceY);
        }

        // glide/fly
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            Log.i("szjPhotoGestureListener"."Inertia slide onFling");
            return super.onFling(e1, e2, velocityX, velocityY);
        }

        // Delay trigger [100ms] - commonly used with water ripple effect
        @Override
        public void onShowPress(MotionEvent e) {
            super.onShowPress(e);
            Log.i("szjPhotoGestureListener"."Delay trigger onShowPress");
        }

        // The press must return true because all events are triggered by the press
        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }

        // Double click -- triggered on the second press (40ms-300ms) [less than 40ms to prevent jitter]
        @Override
        public boolean onDoubleTap(MotionEvent e) {
            Log.i("szjPhotoGestureListener"."Double click onDoubleTap");
            return super.onDoubleTap(e);
        }

        // Double click the second event processing DOWN MOVE UP will be executed here
        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            Log.i("szjPhotoGestureListener"."Double click executes onDoubleTapEvent");
            return super.onDoubleTapEvent(e);
        }

        // Triggered when a click is triggered When a double-click is not triggered
        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            Log.i("szjPhotoGestureListener"."Click onSingleTapConfirmed");
            return super.onSingleTapConfirmed(e); }}Copy the code

So I’m logging all of this, so it’s easy to do it yourself, but the most important thing for magnification is of course the double click event onDoubleTap()

Look directly at the code

#PhotoGestureListener.java

    // Whether to double-click [default first click is zoom]
    boolean isDoubleClick = false;
    
	    // Double click -- triggered on the second press (40ms-300ms) [less than 40ms to prevent jitter]
        @Override
        public boolean onDoubleTap(MotionEvent e) {
        	// The first click is to enlarge the effectisDoubleClick = ! isDoubleClick;if (isDoubleClick) {
                // Zoom in to maximum scale
                currentScale = bigScale;
            } else {
                // Zoom out to the ratio of left and right white space
                currentScale = smallScale;
            }
            / / refresh ontouch
            invalidate();
          
            return super.onDoubleTap(e);
        }
Copy the code

Remember to initialize PhotoGestureListener

We all know that a click event (DOWN)/a touch event (MOVE)/a lift event (UP) can be heard by onTouchEvent(), so the same is true for a double click event!!

Note that ⚠️⚠️ :onDown() must return true because the DOWN event is the starting point for all events

#PhotoView2.java

	// Double click
private final GestureDetector mPhotoGestureListener;

public PhotoView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {... Constructor to initialize... mPhotoGestureListener =new GestureDetector(context, new PhotoGestureListener());
}

// Double-click the event to pass
@Override
public boolean onTouchEvent(MotionEvent event) {
    return mPhotoGestureListener.onTouchEvent(event);
}
Copy the code

Current effect




Double click to enlarge and add animation

Now it’s a bit rough, so I’m going to add a zoom animation

#PhotoGestureListener.java

        @Override
        public boolean onDoubleTap(MotionEvent e) { isDoubleClick = ! isDoubleClick;if (isDoubleClick) {
                Enlarge / /
// currentScale = bigScale;
                scaleAnimation(currentScale, bigScale).start();
            } else {
                / / to narrow
// currentScale = smallScale;
                scaleAnimation(bigScale, smallScale).start();
            }
            // No need to refresh, setCurrentScale() is already refreshed when the property animation calls it
// invalidate();
           
            return super.onDoubleTap(e);
        }

// Zoom animation
public ObjectAnimator scaleAnimation(float start, float end) {
        ObjectAnimator animator = ObjectAnimator.ofFloat(this."currentScale", start, end);
        // Animation time
        animator.setDuration(500);
        return animator;
    }

    // Attribute animation key!! Internally, the set method is called by reflection to assign values
    public void setCurrentScale(float currentScale) {
        this.currentScale = currentScale;
        invalidate();
    }
Copy the code

Current effect

Zoom in and slide the image

So for the sake of code specification, line, I’m going to write the y coordinate as an OffSet class

data class OffSet(var x: Float, var y: Float)
Copy the code

Once again in the double-click gesture class,onScroll() is similar to the ACTION_MOVE event, so listening to this is the same.

#PohtoView2.java

  // Move the finger after zooming in
    private OffSet moveOffset = new OffSet(0f.0f);

// Double-click the gesture to listen
class PhotoGestureListener extends GestureDetector.SimpleOnGestureListener {
// Trigger an action_move-like event while sliding
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {

			// It can only be moved if it is zoomed in
            if (isDoubleClick) {
                moveOffset.setX(moveOffset.getX() - distanceX);
                moveOffset.setY(moveOffset.getY() - distanceY);
                // Kotlin:
                // moveOffset.x -= distanceX
                // moveOffset.y -= distanceY
                invalidate();
            }
            return super.onScroll(e1, e2, distanceX, distanceY); }}Copy the code

Now, some of you might want to ask, why is this subtraction equal to, first of all, what are distanceX and distanceY

Because onScroll() is kind of a MOVE event, so it’s going to output anything that’s touched

Let’s take the X-axis as an example:

Draw a conclusion, take the pressing point as the center point

  • distanceX
    • Swipe positive to the left
    • Slide negative numbers to the right
  • distanceY
    • Slide up positive
    • Negative sliding down

DistanceX = new x – old x distanceY = new y – old y

Let’s look at the moving canvas API:

#PhotoView2.java

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        /* * Author: Android super pawn * created time: 10/15/21 5:17pm * TODO translation canvas * Parameter 1 :x axis translation distance * Parameter 2: Y axis translation distance */
        canvas.translate(-300.0);
}
Copy the code

Effect:




Conclusion:

To move the image left, set Canvas.translate (); The X-axis (parameter 1) is negative, and the reverse is positive if you move to the right

Now that you know distanceX and distanceY, and now that you know the API for canvas movement, the question is, why is it minus or equal when moving?

#PhotoGestureListener.java

// It can only be moved if it is zoomed in
if (isDoubleClick) {
	 / / Java
     moveOffset.setX(moveOffset.getX() - distanceX);
     moveOffset.setY(moveOffset.getY() - distanceY);
     // Kotlin:
     // moveOffset.x -= distanceX
     // moveOffset.y -= distanceY
     invalidate();
}
Copy the code

Because when you swipe left, the picture should be moving right

Because the distanceX is positive when you swipe left and is triggered by a MOVE event, it will fire multiple times

So this is going to be minus or equal to, you need to add up the distanceX coordinates

Finally, remember to draw offsets in onDraw

PohtoView2.java

  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // Move the canvascanvas.translate(moveOffset.getX(), moveOffset.getY()); . The rest of the code... }Copy the code

Current effect




Image enlargement state operation

First let’s see what I mean by “picture enlargement state operation”In fact, it is the state of magnification, prohibit white edge, make the user experience higher!

Take a look at the code:

#PohtoView2.java

 public void fixOffset(a) {
        // The width of the enlarged image
        float currentWidth = mBitMap.getWidth() * bigScale;
        // The height of the enlarged image
        float currentHeight = mBitMap.getHeight() * bigScale;

        // Right side restriction
        moveOffset.setX(Math.max(moveOffset.getX(), -(currentWidth - getWidth()) / 2));

        // left limit [left moveoffset.getx () is negative]
        moveOffset.setX(Math.min(moveOffset.getX(), (currentWidth - getWidth()) / 2));

        // Lower limit
        moveOffset.setY(Math.max(moveOffset.getY(), -(currentHeight - getHeight()) / 2));

        // Upper limit [upper moveoffset.gety () is negative]
        moveOffset.setY(Math.min(moveOffset.getY(), (currentHeight - getHeight()) / 2));
    }
Copy the code

[onScroll()] will do just that!

#PhotoGestureListener.java

 		// Trigger an action_move-like event while sliding
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {

            if (isDoubleClick) {

                moveOffset.setX(moveOffset.getX() - distanceX);
                moveOffset.setY(moveOffset.getY() - distanceY);
				// moveOffset.x -= distanceX;
				// moveOffset.y -= distanceY;
				
                // Prevents images from sliding out of the screen
                fixOffset();

                invalidate();
            }

            return super.onScroll(e1, e2, distanceX, distanceY);
        }
Copy the code

This code needs to be savoured!

Current effect




Double click to zoom in

The name is very abstract, so let’s take a look at the effect:

Auxiliary graph:

Implementation idea:

When it is a small picture, you need to double click to enlarge it, just ask the distance between the double click position and the corresponding position after double click to enlarge it, and then move it over

= LLDB etX() -getwidth () / 2

Due to the width of the large image = getWidth() * bigScale

(LLDB etX() -getwidth () / 2) * bigSale

= LLDB etX() -getwidth () / 2 – (LLDB etX() -getwidth () / 2) * bigSale

Double-click to zoom in and move:

#PhotoGestureListener.java

        @Override
        public boolean onDoubleTap(MotionEvent e) { isDoubleClick = ! isDoubleClick;if (isDoubleClick) {
                float currentX = e.getX() - (float) getWidth() / 2f;
                float currentY = e.getY() - (float) getHeight() / 2f;
                
                moveOffset.setX(currentX - currentX * bigScale);
                moveOffset.setY(currentY - currentY * bigScale);

                // Recalculate, forbid white edge after magnification.
                fixOffset();

                scaleAnimation(currentScale, bigScale).start();
            } else {
                scaleAnimation(bigScale, smallScale).start();
            }
            return super.onDoubleTap(e);
        }
Copy the code

Take a look at the results:




Shit, this… What is the… It seems to be right in a sense, at least when you click, the translation is correct, calm analysis, see what the problem is…

After 30 minutes of thinking, I finally know why.

The problem is that when you double-click directly, you calculate the distance between the smaller image and the larger one, and then there is a zoom animation underneath, so this can happen. Just make the moveOffset follow the zoom animation!

At present, the conditions for double-clicking to zoom in and out are as follows:

  • Double click to zoom in from currentScale -> bigScale
  • Double-click to zoom out from bigScale -> smallScale

Here comes a little algorithm:

 float a = (currentScale - smallScale) / (bigScale - smallScale);
Copy the code

Let’s say we’re currently scaling from small to big, so currentScale -> bigScale

When currentScale = bigScale, we’re already at our maximum

So (currentScale-SmallScale)/(Bigscale-SmallScale) = 1

otherwise

  • Double click to enlarge:

(CurrentScale-SmallScale)/(Bigscale-SmallScale) is a change from 0-1

  • Double-click to zoom out:

(CurrentScale-SmallScale)/(Bigscale-SmallScale) is the state changed from 1-0

Let’s see how the code is written:

#PhotoView2.java

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        /* * Author: Android super pawn * created time: 10/15/21 5:17pm * TODO translation canvas * Parameter 1 :x axis translation distance * Parameter 2: Y axis translation distance */
        floata = (currentScale - smallScale) / (bigScale - smallScale); canvas.translate(moveOffset.getX() * a, moveOffset.getY() * a); . A lot of code is omitted.... }Copy the code

This code needs fine Detail

Current effect




Attach the image to the Fling effect

Let’s look at what we want to achieve:




The Android Fling class Is called OverScroller

Use very simple, pure tune API code

#PhotoView2.java

	// Inertial sliding
    private final OverScroller mOverScroller;
	@SuppressLint("CustomViewStyleable")
    public PhotoView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
		
		// Inertial sliding
        mOverScroller = new OverScroller(context);
	}
Copy the code

Call from an onFling event:

#PhotoGestureListener.java

		 // glide/fly
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            /* * int startX slide x * int startY, slide y * int velocityX, x * int velocityY, y * int minX, Max * int Max * int minY, Max * int maxY, Max * int overX, Moverscroll.fling () */
            mOverScroller.fling(
                    (int) moveOffset.getX(),
                    (int) moveOffset.getY(),
                    (int) velocityX,
                    (int) velocityY,
                    (int) (-(mBitMap.getWidth() * bigScale - getWidth()) / 2),
                    (int) ((mBitMap.getHeight() * bigScale - getWidth()) / 2),
                    (int) (-(mBitMap.getHeight() * bigScale - getHeight()) / 2),
                    (int) ((mBitMap.getHeight() * bigScale - getHeight()) / 2),
                    300.300
            );
            return super.onFling(e1, e2, velocityX, velocityY);
        }
Copy the code

Take a look at the results:

This is… It seems to be a bit of a drag, and does not achieve the desired effect.. By printing the log he knows that onFling() is executed only once

So you need to pull out the values stored in moverscroll.Fling ()

#PhotoView2.java

	// Inertial slide assist
    class FlingRunner implements Runnable {

        @Override
        public void run(a) {
            // Check whether the current execution
            if (mOverScroller.computeScrollOffset()) {
                // Set the fling value
                moveOffset.setX(mOverScroller.getCurrX());
                moveOffset.setY(mOverScroller.getCurrY());
                Log.i("szjFlingRunner"."X:" + mOverScroller.getCurrX() + "\tY:" + mOverScroller.getCurrY());

                // Continue executing flingrunner.run
                postOnAnimation(this);
                / / refreshinvalidate(); }}}Copy the code

It’s still initialized in a construct

#PhotoView2.java

	// Auxiliary inertial sliding class
    private final FlingRunner mFlingRunner;
@SuppressLint("CustomViewStyleable")
    public PhotoView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr); . Omit the...// Inertial slide assist class
        mFlingRunner = new FlingRunner();

	}
Copy the code
#PhotoGestureListener.java

 		// glide/fly
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            Log.i("szjPhotoGestureListener"."Inertia slide onFling");

            Log.i("szjOnFling"."velocityX:" + velocityX + "\tvelocityY" + velocityY);

            /* * int startX slide x * int startY, slide y * int velocityX, x * int velocityY, y * int minX, Width min * int maxX, width Max * int minY, height min * int maxY, height Max * int overX, overflow x distance * int overY overflow y distance */mOverScroller.fling( .... It's too long, it's been omitted... ;// Set the fling effect
           mFlingRunner.run();

            return super.onFling(e1, e2, velocityX, velocityY);
        }
Copy the code

Current effect




The double refers to the operation

The two-finger operation will continue to be built in android

class PhotoDoubleScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener {
        // Get the current scale value at the start of the two-finger operation
        private float scaleFactor = 0f;


        // Double finger operation
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            // Detector. GetScaleFactor Scale factor
            currentScale = scaleFactor * detector.getScaleFactor();
            
			/ / refresh
            invalidate();
            return false;
        }

        // Start double finger operation
        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            scaleFactor = currentScale;
            // Note that the value is true to indicate the start of the two-finger operation
            return true;
        }

        // The operation is complete
        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {}}Copy the code

The two-finger operation is relatively simple, that is, simple tuning API

Initialization of a two-finger operation

It’s still initialized in a construct

#PohtoView2.java

    // Double finger operation
    private final ScaleGestureDetector scaleGestureDetector;
    
	@SuppressLint("CustomViewStyleable")
    public PhotoView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

		 // Double finger operation
        scaleGestureDetector = new ScaleGestureDetector(context, new PhotoDoubleScaleGestureListener());
}
Copy the code

The two-finger operation also needs to be initialized in onTouchEvent()

Because a two-finger operation is the same as a double-click operation, both are an event

 	@Override
    public boolean onTouchEvent(MotionEvent event) {

        // Double finger operation
        boolean scaleTouchEvent = scaleGestureDetector.onTouchEvent(event);

        // Whether the operation is two-fingered
        if (scaleGestureDetector.isInProgress()) {

            return scaleTouchEvent;
        }

        // Double click
        return mPhotoGestureListener.onTouchEvent(event);
    }
Copy the code

The default event is now a two-finger action event, followed by a double-click action

Current effect




Finally optimize double – click double – finger operation

As you can see, the basics are already in place; now you just need to finally restrict it!

This code doesn’t have any gold in it, so let’s go straight to the code:

#PhotoDoubleScaleGestureListener.java

 		// The operation is complete
        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            // The current image width
            float currentWidth = mBitMap.getWidth() * currentScale;
            // The width of the image before scaling
            float smallWidth = mBitMap.getWidth() * smallScale;
            // Zoom the width of the image
            float bigWidth = mBitMap.getWidth() * bigScale;

            // If the current image < the image before scaling
            if (currentWidth < smallWidth) {
                // Zoom out the image
                isDoubleClick = false;

                scaleAnimation(currentScale, smallScale).start();
            } else if (currentWidth > smallWidth) {
                // Zoom out the image
                isDoubleClick = false;
            }

            // If current state > zoomed image then change it to the maximum state
            if (currentWidth > bigWidth) {

                // Double click to zoom out
                scaleAnimation(currentScale, bigScale).start();
                // Double click to enlarge the image
                isDoubleClick = true; }}Copy the code

The final result




The complete code

Original is not easy, your praise is the biggest support for me!