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!