在 Android 開發中,圖片查看功能是常見需求。本文將詳細介紹如何通過繼承 ImageView 並結合 Matrix、ScaleGestureDetector、GestureDetector 以及 Scroller 實現一個功能完整的圖片查看控件,支持雙指縮放、雙擊縮放、邊界檢查、自動回彈、慣性滑動等核心交互。
自定義 ZoomImageView 實現以下接口:
ScaleGestureDetector.OnScaleGestureListener:處理雙指縮放手勢View.OnTouchListener:處理觸摸事件(拖動、雙擊、單指滑動)ViewTreeObserver.OnGlobalLayoutListener:在佈局完成後初始化圖片矩陣public class ZoomImageView extends ImageView implements ... {
private ScaleGestureDetector mScaleGestureDetector;
private GestureDetector mGestureDetector;
private Matrix mScaleMatrix;
private boolean mFirst = true;
private float mInitScale, mMaxScale, mMidScale, mMinScale, mMaxOverScale;
private boolean isAutoScale;
// 觸摸相關
private int mLastPointerCount;
private float mLastX, mLastY;
private int mTouchSlop;
private boolean isCanDrag, isCheckLeftAndRight, isCheckTopAndBottom;
// 慣性滑動
private VelocityTracker mVelocityTracker;
private FlingRunnable mFlingRunnable;
...
}
在 onGlobalLayout() 中完成圖片的初始縮放與居中:
@Override
public void onGlobalLayout() {
if (!mFirst) return;
mFirst = true;
int width = getWidth(), height = getHeight();
Drawable d = getDrawable();
if (d == null) return;
int dw = d.getIntrinsicWidth(), dh = d.getIntrinsicHeight();
float scale = 1.0f;
// 根據圖片與控件尺寸比例計算初始縮放比
if (dw > width && dh < height) scale = width * 1.0f / dw;
else if (dw < width && dh > height) scale = height * 1.0f / dh;
else scale = Math.min(width * 1.0f / dw, height * 1.0f / dh);
// 平移至中心
int dx = width/2 - dw/2, dy = height/2 - dh/2;
mScaleMatrix.postTranslate(dx, dy);
mScaleMatrix.postScale(scale, scale, width/2, height/2);
setImageMatrix(mScaleMatrix);
// 設置縮放邊界值
mInitScale = scale;
mMaxScale = mInitScale * 4;
mMidScale = mInitScale * 2;
mMinScale = mInitScale / 4;
mMaxOverScale = mMaxScale * 5;
}
在 onScale() 中根據 scaleFactor 縮放,並限制在 [mMinScale, mMaxOverScale] 範圍內,縮放中心為兩指中點:
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
float scale = getScale();
if (getDrawable() == null) return true;
if ((scaleFactor > 1.0f && scale * scaleFactor < mMaxOverScale) ||
(scaleFactor < 1.0f && scale * scaleFactor > mMinScale)) {
// 邊界修正
if (scale * scaleFactor > mMaxScale) scaleFactor = mMaxScale / scale;
if (scale * scaleFactor < mInitScale) scaleFactor = mInitScale / scale;
mScaleMatrix.postScale(scaleFactor, scaleFactor,
detector.getFocusX(), detector.getFocusY());
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
}
return true;
}
雙擊時根據當前縮放比決定放大至 mMidScale 或縮小至 mInitScale,通過 AutoScaleRunnable 實現漸變動畫:
private class AutoScaleRunnable implements Runnable {
private float mTargetScale, tempScale, x, y;
private final float BIGGER = 1.07f, SMALLER = 0.93f;
public AutoScaleRunnable(float targetScale, float x, float y) { ... }
@Override public void run() {
mScaleMatrix.postScale(tempScale, tempScale, x, y);
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
float currentScale = getScale();
if ((tempScale > 1.0f && currentScale < mTargetScale) ||
(tempScale < 1.0f && currentScale > mTargetScale)) {
postDelayed(this, 16);
} else {
// 修正最終比例
float scale = mTargetScale / currentScale;
mScaleMatrix.postScale(scale, scale, x, y);
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
isAutoScale = false;
}
}
}
在 onTouch() 的 ACTION_MOVE 中計算位移 dx, dy,並根據圖片是否超出控件邊界決定是否允許水平/垂直拖動:
if (isCanDrag) {
if (rectF.width() < getWidth()) { dx = 0; isCheckLeftAndRight = false; }
if (rectF.height() < getHeight()) { dy = 0; isCheckTopAndBottom = false; }
mScaleMatrix.postTranslate(dx, dy);
checkBorderWhenTranslate();
setImageMatrix(mScaleMatrix);
}
在 ACTION_UP 中檢測當前縮放比,若小於 mInitScale 則彈回初始比例,大於 mMaxScale 則彈回最大比例:
case MotionEvent.ACTION_UP:
mLastPointerCount = 0;
if (getScale() < mInitScale) {
post(new AutoScaleRunnable(mInitScale, getWidth()/2, getHeight()/2));
} else if (getScale() > mMaxScale) {
post(new AutoScaleRunnable(mMaxScale, getWidth()/2, getHeight()/2));
}
// 啟動慣性滑動...
break;
利用 VelocityTracker 獲取手指抬起瞬間的速度,配合 Scroller 在 FlingRunnable 中模擬慣性滑動:
private class FlingRunnable implements Runnable {
private Scroller mScroller;
private int mCurrentX, mCurrentY;
public void fling(int viewWidth, int viewHeight, int velocityX, int velocityY) {
RectF rectF = getMatrixRectF();
// 計算滑動範圍 [minX, maxX], [minY, maxY]
mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
}
@Override public void run() {
if (mScroller.computeScrollOffset()) {
int dx = mScroller.getCurrX() - mCurrentX;
int dy = mScroller.getCurrY() - mCurrentY;
mScaleMatrix.postTranslate(dx, dy);
checkBorderWhenTranslate();
setImageMatrix(mScaleMatrix);
mCurrentX = mScroller.getCurrX();
mCurrentY = mScroller.getCurrY();
post(this);
}
}
}
本文完整展示了一个生產級的圖片查看控件實現,涵蓋了手勢檢測、矩陣變換、邊界約束、動畫插值、慣性滑動等核心技術點。代碼結構清晰,可直接集成到項目中使用或根據業務需求擴展。