支持单指滑动、双指缩放/旋转、边界超出判定、缩放过度判定、旋转自动回正,下面直接贴完整代码
目前无法做到放大过度回弹时,图片的位置保持在两指之间的中心位置,如果有好的想法,欢迎提出。
Android 自定义PhotoView演示视频
package com.example.myapplication.photoView;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ScaleGestureDetector.OnScaleGestureListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.example.myapplication.LogUtil;
public class MyPhotoView2 extends androidx.appcompat.widget.AppCompatImageView {
private final GestureDetector mGestureDetector;
private final ScaleGestureDetector mScaleGestureDetector;
private final RotateGestureDetector mRotateGestureDetector;
private Matrix mMatrix;
private float mPreScaleFactor = 1.0f;
private float mBaseScale;
private float mMaxScale;
private float viewCenterX;
private float viewCenterY;
private float frameViewLeft;
private float frameViewTop;
private float frameViewRight;
private float frameViewBottom;
private static final int MAX_SCROLL_FACTOR = 3;
private static final float DAMP_FACTOR = 9.0f; // 阻尼因子
private boolean isFirstLayout = true;
private int mState;
private static final int STATE_NONE = 0;
private static final int STATE_DRAG = 1;
private static final int STATE_ZOOM = 2;
private float currentImageRotate = 0f; // 图片当前的旋转角度
public MyPhotoView2(@NonNull Context context) {
this(context, null);
}
public MyPhotoView2(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyPhotoView2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setScaleType(ScaleType.MATRIX);
mMatrix = new Matrix();
mGestureDetector = new GestureDetector(context, mSimpleOnGestureListener);
mScaleGestureDetector = new ScaleGestureDetector(context, mOnScaleGestureListener);
mRotateGestureDetector = new RotateGestureDetector(context, mSimpleRotateGestureListener);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!isFirstLayout) return ;
isFirstLayout = false;
viewCenterX = (float) getWidth() / 2;
viewCenterY = (float) getHeight() / 2;
autoFillView();
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (mState == STATE_DRAG) {
checkBound();
}
break;
}
if (mGestureDetector.onTouchEvent(event)) {
return true;
}
mScaleGestureDetector.onTouchEvent(event);
mRotateGestureDetector.onTouchEvent(event);
return true;
}
private void autoFillView() {
Drawable drawable = getDrawable();
if (drawable == null) return ;
int viewWidth = getWidth();
int viewHeight = getHeight();
int drawableWidth = drawable.getIntrinsicWidth();
int drawableHeight = drawable.getIntrinsicHeight();
float scale = Math.min((float) viewWidth / drawableWidth, (float) viewHeight / drawableHeight);
mBaseScale = scale;
mMaxScale = mBaseScale + 1f;
float dy = (viewHeight - drawableHeight * scale) / 2;
Matrix newMatrix = new Matrix();
newMatrix.setScale(scale, scale);
newMatrix.postTranslate(0, dy);
setImageMatrix(newMatrix);
mMatrix = newMatrix;
setFrameView();
}
private final GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (e1.getPointerCount() == e2.getPointerCount() && e1.getPointerCount() == 1) {
mState = STATE_DRAG;
RectF rectF = getMatrixRectF();
float leftEdgeDistanceLeft = rectF.left - frameViewLeft;
float topEdgeDistanceTop = rectF.top - frameViewTop;
float rightEdgeDistanceRight = rectF.right - frameViewRight;
float bottomEdgeDistanceBottom = rectF.bottom - frameViewBottom;
int maxOffsetX = getWidth() / MAX_SCROLL_FACTOR;
int maxOffsetY = getHeight() / MAX_SCROLL_FACTOR;
if (leftEdgeDistanceLeft > 0) {
if (distanceX < 0) {
if (leftEdgeDistanceLeft < maxOffsetX) {
int ration= (int) (DAMP_FACTOR / maxOffsetX * leftEdgeDistanceLeft) + 1;
distanceX /= ration;
} else {
distanceX = 0;
}
}
} else if (rightEdgeDistanceRight < 0) {
if (distanceX > 0) {
if (rightEdgeDistanceRight > -maxOffsetX) {
int ration = (int) (DAMP_FACTOR / maxOffsetX * -rightEdgeDistanceRight) + 1;
distanceX /= ration;
} else {
distanceX = 0;
}
}
}
if (topEdgeDistanceTop > 0) {
if (distanceY < 0) {
int ration = (int) (DAMP_FACTOR / maxOffsetY * topEdgeDistanceTop) + 1;
distanceY /= ration;
} else {
distanceY = 0;
}
} else if (bottomEdgeDistanceBottom < 0) {
if (distanceY > 0) {
int ration = (int) (DAMP_FACTOR / maxOffsetY * -bottomEdgeDistanceBottom) + 1;
distanceY /= ration;
} else {
distanceY = 0;
}
}
mMatrix.postTranslate(-distanceX, -distanceY);
setImageMatrix(mMatrix);
return true;
}
mState = STATE_NONE;
return super.onScroll(e1, e2, distanceX, distanceY);
}
@Override
public boolean onDoubleTap(MotionEvent e) {
return super.onDoubleTap(e);
}
};
private final OnScaleGestureListener mOnScaleGestureListener = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
LogUtil.e("缩放开始: " + System.currentTimeMillis());
return true;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
mState = STATE_NONE;
if (getDrawable() == null || mMatrix == null) return true;
mState = STATE_ZOOM;
float scaleFactor = detector.getScaleFactor();
float deltaFactor = scaleFactor - mPreScaleFactor;
if (scaleFactor != 1.0f && deltaFactor != 0f) {
mMatrix.postScale(deltaFactor + 1.0f, deltaFactor + 1.0f, detector.getFocusX(), detector.getFocusY());
setImageMatrix(mMatrix);
}
mPreScaleFactor = scaleFactor;
LogUtil.e("原始缩放比: " + getMatrixScale());
return false;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
super.onScaleEnd(detector);
LogUtil.e("缩放结束: " + System.currentTimeMillis());
setFrameView();
}
};
private final RotateGestureDetector.SimpleRotateGestureListener mSimpleRotateGestureListener = new RotateGestureDetector.SimpleRotateGestureListener() {
float totalRotate = 0f;
@Override
public boolean onRotateBegin(RotateGestureDetector detector) {
LogUtil.e("旋转开始: " + System.currentTimeMillis());
return true;
}
@Override
public boolean onRotate(RotateGestureDetector detector) {
totalRotate += detector.getDegree();
mMatrix.postRotate(detector.getDegree(), viewCenterX, viewCenterY);
setImageMatrix(mMatrix);
return true;
}
@Override
public void onRotateEnd(RotateGestureDetector detector) {
LogUtil.e("旋转结束: " + System.currentTimeMillis());
fixRotate(totalRotate);
fixScale();
totalRotate = 0f;
}
};
private void setFrameView() {
RectF rectF = getMatrixRectF();
frameViewLeft = Math.max(rectF.left, 0f);
frameViewTop = Math.max(rectF.top, 0f);
frameViewRight = Math.min(rectF.right, getWidth());
frameViewBottom = Math.min(rectF.bottom, getHeight());
}
private void checkBound() {
RectF rectF = getMatrixRectF();
float leftEdgeDistanceLeft = rectF.left - frameViewLeft;
float topEdgeDistanceTop = rectF.top - frameViewTop;
float rightEdgeDistanceRight = rectF.right - frameViewRight;
float bottomEdgeDistanceBottom = rectF.bottom - frameViewBottom;
float[] values = new float[9];
mMatrix.getValues(values);
if (leftEdgeDistanceLeft > 0) {
values[Matrix.MTRANS_X] -= leftEdgeDistanceLeft;
} else if (rightEdgeDistanceRight < 0) {
values[Matrix.MTRANS_X] -= rightEdgeDistanceRight;
}
if (topEdgeDistanceTop > 0) {
values[Matrix.MTRANS_Y] -= topEdgeDistanceTop;
} else if (bottomEdgeDistanceBottom < 0) {
values[Matrix.MTRANS_Y] -= bottomEdgeDistanceBottom;
}
mMatrix.setValues(values);
setImageMatrix(mMatrix);
}
private void fixRotate(float totalRotate) {
// 右旋为正, 左旋为负
LogUtil.e("旋转角度: " + totalRotate);
totalRotate %= 360;
float fixDegree = 0f;
if (totalRotate >= 0) {
if (totalRotate < 45) {
fixDegree = 0 - totalRotate;
} else if (totalRotate >= 45 && totalRotate < 135) {
fixDegree = 90 - totalRotate;
} else if (totalRotate >= 135 && totalRotate < 225) {
fixDegree = 180 - totalRotate;
} else if (totalRotate >= 225 && totalRotate < 315) {
fixDegree = 270 - totalRotate;
} else if (totalRotate >= 315 && totalRotate < 360) {
fixDegree = 360 - totalRotate;
}
} else {
if (totalRotate > -45) {
fixDegree = 0 -totalRotate;
} else if (totalRotate <= -45 && totalRotate > -135) {
fixDegree = -90 - totalRotate;
} else if (totalRotate <= -135 && totalRotate > -225) {
fixDegree = -180 - totalRotate;
} else if (totalRotate <= -225 && totalRotate > -315) {
fixDegree = -270 - totalRotate;
} else if (totalRotate <= -315 && totalRotate > -360) {
fixDegree = -360 - totalRotate;
}
}
mMatrix.postRotate(fixDegree, viewCenterX, viewCenterY);
setImageMatrix(mMatrix);
currentImageRotate += (fixDegree + totalRotate);
LogUtil.e("修正后角度: " + (fixDegree + totalRotate));
LogUtil.e("当前角度: " + currentImageRotate);
}
/**
* event执行顺序
* 旋转开始->缩放开始->缩放结束->旋转结束
* 所以选择在旋转结束时,去处理缩放过度问题
*/
private void fixScale() {
int viewWidth = getWidth();
int viewHeight = getHeight();
int drawableWidth = getDrawable().getIntrinsicWidth();
int drawableHeight = getDrawable().getIntrinsicHeight();
// 手动计算缩放比
// 不能使用Matrix.MSCALE_X,它无法得出图片偏转时的准确缩放比
RectF rectF = getMatrixRectF();
LogUtil.e("图片相对宽度: " + rectF.width());
LogUtil.e("图片相对高度: " + rectF.height());
float currentImageScale;
float widthScale, heightScale;
if (currentImageRotate % 180 != 0) {
// 图片竖直时的缩放比
widthScale = rectF.width() / drawableWidth;
heightScale = rectF.height() / drawableHeight;
} else {
// 图片横置时的缩放比
widthScale = rectF.width() / drawableHeight;
heightScale = rectF.height() / drawableWidth;
}
currentImageScale = (widthScale + heightScale) / 2; // 使用平均值,会更加精确
LogUtil.e("计算缩放比: " + currentImageScale);
if (currentImageScale < mBaseScale) {
// 图片小于最小比例,修正至屏幕中心位置
float scale;
if (currentImageRotate % 180 != 0) {
scale = (float) viewWidth / drawableHeight;
} else {
scale = (float) viewWidth / drawableWidth;
}
float dy = (viewHeight - drawableHeight * scale) / 2;
Matrix newMatrix = new Matrix();
newMatrix.setScale(scale, scale);
newMatrix.postTranslate(0, dy);
newMatrix.postRotate(currentImageRotate, viewCenterX, viewCenterY);
setImageMatrix(newMatrix);
mMatrix = newMatrix;
} else if (currentImageScale > mMaxScale) {
// 太拉了,放大过度修正中心位置
float dx = (viewWidth - drawableWidth * mMaxScale) / 2;
float dy = (viewHeight - drawableHeight * mMaxScale) / 2;
LogUtil.e("水平距离: " + dx + " 垂直距离: " + dy);
Matrix newMatrix = new Matrix();
newMatrix.setScale(mMaxScale, mMaxScale);
newMatrix.postTranslate(dx, dy);
newMatrix.postRotate(currentImageRotate, viewCenterX, viewCenterY);
setImageMatrix(newMatrix);
mMatrix = newMatrix;
} else {
// 算了,摆烂了,缩放全部修正至屏幕中心位置
float dx = (viewWidth - drawableWidth * currentImageScale) / 2;
float dy = (viewHeight - drawableHeight * currentImageScale) / 2;
Matrix newMatrix = new Matrix();
newMatrix.setScale(currentImageScale, currentImageScale);
newMatrix.postTranslate(dx, dy);
newMatrix.postRotate(currentImageRotate, viewCenterX, viewCenterY);
setImageMatrix(newMatrix);
mMatrix = newMatrix;
}
setFrameView();
}
private RectF getMatrixRectF() {
RectF rectF = new RectF();
Drawable drawable = getDrawable();
if (drawable != null) {
rectF.set(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
mMatrix.mapRect(rectF);
}
return rectF;
}
private float getMatrixScale() {
float[] values = new float[9];
mMatrix.getValues(values);
return values[Matrix.MSCALE_X];
}
}
if (mInProgress && configChange) { // 取消两指距离判定
// if (mInProgress && (span < mMinSpan || configChange)) {
<dimen name="config_minScalingSpan">27mm</dimen>