参考文章
1. Android 手势检测实战 打造支持缩放平移的图片预览效果(上)
2. Android 手势检测实战 打造支持缩放平移的图片预览效果(下)
3. 我的Android进阶之旅——>android Matrix图片随意的放大缩小,拖动
整体思路:
1. 实现缩放功能:
(1) 创建ScaleGestureDetector对象,实现ScaleGestureDetector.OnScaleGestureListener接口;
(2) 在onScale方法中实现缩放逻辑 , 相关逻辑包括获取缩放比例的初始值、定义放大的上限比例;
(3) setOnTouchListener(this),实现OnTouchListener接口,接收触摸事件。
2. 实现拖动功能:
(1) 计算、修正拖动距离
(2) 实现拖动
更多细节呈现在代码中。
代码实现
public class ScalableImageView extends ImageView implements ScaleGestureDetector.OnScaleGestureListener, ViewTreeObserver.OnGlobalLayoutListener {
private static final String TAG = "ScalableImageView";
private int gesture;
private static final int GESTURE_DRAG = 1;
private static final int GESTURE_ZOOM = 2;
// 最大的缩放比例
private static final float MAX_SCALE = 4.0f;
// 初始化时的缩放比例,如果图片宽或高大于屏幕,此值将小于1(HongYang大神的博客上有笔误)
private float initScale = 1.0f;
private Matrix mMatrix = new Matrix();
private ScaleGestureDetector mScaleGestureDetector;
private int viewWidth;
private int viewHeight;
private Drawable mDrawable;
public ScalableImageView(Context context) {
this(context, null);
}
public ScalableImageView(Context context, AttributeSet attrs) {
super(context, attrs);
// 这一步很关键
super.setScaleType(ScaleType.MATRIX);
mScaleGestureDetector = new ScaleGestureDetector(context, this);
}
@Override
public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
// 全局缩放比例
float scale = getScale();
// 上一次缩放事件到当前事件的缩放比例(微分缩放比例)
float factor = scaleGestureDetector.getScaleFactor();
// 第一次获取的factor偏小,会引起缩放手势触屏的一瞬间图片缩小
if (resetFactor) {
factor = 1.0f;
resetFactor = false;
}
// drawable为空或者缩放比例超出范围,拒执行
if (getDrawable() == null || scale * factor > MAX_SCALE || scale * factor < initScale) {
return false;
} else {
mMatrix.postScale(factor, factor, getWidth() / 2, getHeight() / 2);
setImageMatrix(mMatrix);
return true;
}
// 以下是HongYang大神的思路
// 在INIT_VALUE到MAX_SCALE范围内缩放(放大不超过MAX_SCALE,缩小不小于INIT_SCALE)
// if ((scale < MAX_SCALE && factor >= 1.0f) || (scale > initScale && factor <= 1.0f)) {
// if (scale * factor > MAX_SCALE) {//放大超过MAX_SCALE的处理
// factor = MAX_SCALE / scale;
// } else if (scale * factor < initScale) {//缩小超过INIT_SCALE的处理
// factor = initScale / scale;
// }
// mMatrix.postScale(factor, factor, getWidth() / 2, getHeight() / 2);
// setImageMatrix(mMatrix);
// }
// return true;
}
private final float[] matrixValues = new float[9];
// 获取全局缩放比例(相对于未缩放时的缩放比例),和直接用getScaleX()有区别!
private float getScale() {
mMatrix.getValues(matrixValues);
return matrixValues[Matrix.MSCALE_X];
}
// 缩放开始时:可用于过滤一些手势,比如从有效区以外的区域划进来的手势
@Override
public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
return true;
}
// 缩放结束时
@Override
public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) {
}
private float mLastX;
private float mLastY;
private boolean resetFactor;
private boolean onZoomFinished;
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
gesture = GESTURE_DRAG;
break;
case MotionEvent.ACTION_POINTER_DOWN:
gesture = GESTURE_ZOOM;
resetFactor = true;
break;
case MotionEvent.ACTION_POINTER_UP:
gesture = GESTURE_DRAG;
onZoomFinished = true;
break;
case MotionEvent.ACTION_MOVE:
switch (gesture) {
case GESTURE_DRAG:
// 屏蔽缩放转换为拖动时的跳动,此时dx、dy偏大。
if (onZoomFinished) {
onZoomFinished = false;
break;
}
float dx = x - mLastX;
float dy = y - mLastY;
// 对偏移值做适当修正,避免图片与视图边间产生空白区域
PointF dragDelta = amendDelta(dx, dy);
mMatrix.postTranslate(dragDelta.x, dragDelta.y);
setImageMatrix(mMatrix);
break;
case GESTURE_ZOOM:
mScaleGestureDetector.onTouchEvent(event);
break;
}
break;
case MotionEvent.ACTION_UP:
// 消除缩放造成的图片距视图边的空白
PointF zoomDelta = amendDelta(0, 0);
mMatrix.postTranslate(zoomDelta.x, zoomDelta.y);
setImageMatrix(mMatrix);
break;
}
mLastX = x;
mLastY = y;
return true;
}
private PointF amendDelta(float dx, float dy) {
RectF rectF = getRectF();
if (rectF.width() > viewWidth) {// 图片宽度超过视图宽度
if (rectF.left + dx > 0) {// 拖动会引起图片左边出现空白
dx = -rectF.left;
} else if (rectF.right + dx < viewWidth) {// 拖动会引起图片右边出现空白
dx = viewWidth - rectF.right;
}
} else {// 图片宽度不及视图宽度,不允许图片宽度方向可视区域离开边界
if (rectF.left + dx < 0) {
dx = -rectF.left;
} else if (rectF.right + dx > viewWidth) {
dx = viewWidth - rectF.right;
}
}
if (rectF.height() > viewHeight) {
if (rectF.top + dy > 0) {
dy = -rectF.top;
} else if (rectF.bottom + dy < viewHeight) {
dy = viewHeight - rectF.bottom;
}
} else {
if (rectF.top + dy < 0) {
dy = -rectF.top;
} else if (rectF.bottom + dy > viewHeight) {
dy = viewHeight - rectF.bottom;
}
}
return new PointF(dx, dy);
}
private RectF mRectF;
public RectF getRectF() {
if (mDrawable == null) {
mDrawable = getDrawable();
}
if (mRectF == null) {
mRectF = new RectF();
}
if (mDrawable != null) {
mRectF.set(0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
mMatrix.mapRect(mRectF);
}
return mRectF;
}
// onGlobalLayoutListener可能会多次触发
private boolean isFirstTime = true;
// 观察布局变化,目的是获取View的尺寸,在测量之前执行onMeasure getWidth()和getHeight()可能为0
@Override
public void onGlobalLayout() {
if (isFirstTime) {
Drawable drawable = getDrawable();
if (drawable == null) {
return;
}
isFirstTime = false;
int drawableWidth = drawable.getIntrinsicWidth();
int drawableHeight = drawable.getIntrinsicHeight();
viewWidth = getWidth();
viewHeight = getHeight();
// 图片长宽超出View的可见范围的处理
if (drawableWidth > viewWidth || drawableHeight > viewHeight) {
initScale = Math.min(viewWidth * 1.0f / drawableWidth, viewHeight * 1.0f / drawableHeight);
}
// 将图片偏移到中心位置
mMatrix.postTranslate((viewWidth - drawableWidth) / 2, (viewHeight - drawableHeight) / 2);
// 初始化缩放
mMatrix.postScale(initScale, initScale, viewWidth / 2, viewHeight / 2);
setImageMatrix(mMatrix);
}
}
// 当View附加到Window上时调用,这时候它的绘画面板已存在,即将开始绘制。注意,该方法能保证发生在onDraw之前,但可能发生在onMeasure之前或之后。
// HongYang大神写在此方法中,考虑到上述原因,这里写在onMeasure中,见仁见智。
// @Override
// protected void onAttachedToWindow() {
// super.onAttachedToWindow();
// getViewTreeObserver().addOnGlobalLayoutListener(this);
// }
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
getViewTreeObserver().addOnGlobalLayoutListener(this);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
}
关于偏移量修正的示意图如下,以宽度方向为例,当图片宽度小于View的宽度时,无论如何,图片都会与View都某一边产生距离的,这时候要求伸出View的图片部分缩回来;当图片宽度大于View的宽度时,如果与View边缘产生距离,要求将该空白填充满。总之一个原则,充分利用View的区域,尽可能显示更多的图片内容。
这时候控件中还没有加入边界回弹功能,要添加边界回弹功能,请看下一篇:自定义ImageView实现图片的拖动、缩放和边界回弹