Android手势识别GestureDetector和ScaleGestureDetector介绍与使用,以自定义一个可拖拽拉伸的ImageView为例
一、GestureDetector
1. 简介
GestureDetector主要用于检测单指手势,例如单击、长按、滑动等,不支持多指手势。
2. SimpleOnGestureListener 内部类
GestureDetector.SimpleOnGestureListener 是用于处理手势事件的辅助类,它包含了一系列回调方法,用于处理不同类型的单指手势事件。下面是对每个回调方法的简要介绍:
-
onDown(MotionEvent e): 当用户按下(Down)手指时触发。这个方法返回 true 表示事件被消费了,false 表示未被消费。
-
onShowPress(MotionEvent e): 当用户按下并保持按压一段时间时触发。它表示按下动作已被识别,但尚未发生其它任何行为。
-
onSingleTapUp(MotionEvent e): 当用户轻击屏幕时触发。这个方法返回 true 表示事件被消费了,false 表示未被消费。
-
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY): 当用户在屏幕上滚动时触发。它提供了滚动开始和结束时的事件信息,以及在X和Y方向上的距离差。
-
onLongPress(MotionEvent e): 当用户长按屏幕时触发。
-
onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY): 当用户迅速滑动手指并松开时触发。它提供了滑动开始和结束时的事件信息,以及在X和Y方向上的速度。
-
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY): 当用户在屏幕上滚动时触发。与第四个回调方法不同,这个方法在滚动过程中持续触发,而不仅仅是在滚动结束时触发。
-
onDoubleTap(MotionEvent e): 当用户双击屏幕时触发。
-
onDoubleTapEvent(MotionEvent e): 当双击事件包含按下、移动和抬起动作时触发。通常与 onDoubleTap() 结合使用,以处理更复杂的双击手势。
-
onSingleTapConfirmed(MotionEvent e): 当确认发生了单击事件时触发。与 onSingleTapUp() 不同的是,这个方法确保了事件是单击事件而不是双击事件。
这些回调方法提供了处理各种类型手势事件的灵活性,可以根据需求选择实现相应的方法来处理手势事件。
3. 示例
import android.content.Context; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; public class MyView extends View { private GestureDetector gestureDetector; public MyView(Context context, AttributeSet attrs) { super(context, attrs); // 实例化 GestureDetector,并传入 SimpleOnGestureListener 对象 gestureDetector = new GestureDetector(context, new MyGestureListener()); } @Override public boolean onTouchEvent(MotionEvent event) { // 将触摸事件传递给 GestureDetector return gestureDetector.onTouchEvent(event) || super.onTouchEvent(event); } private class MyGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDown(MotionEvent e) { // 用户按下屏幕时触发 return true; // 返回 true 表示事件被消费 } @Override public boolean onSingleTapConfirmed(MotionEvent e) { // 单击事件确认时触发 return true; } @Override public boolean onDoubleTap(MotionEvent e) { // 双击事件时触发 return true; } @Override public void onLongPress(MotionEvent e) { // 长按事件时触发 } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // 滚动事件时触发 return true; } // 其他手势事件处理方法... } }
二、ScaleGestureDetector
1. 简介
用于检测缩放手势,即双指捏合或者扩张的手势。它提供了 onScale() 和 onScaleBegin() 等回调方法来处理缩放手势的开始、进行中和结束时的事件。
2. SimpleOnGestureListener 内部类
ScaleGestureDetector.SimpleOnGestureListener 用于处理手势事件的辅助类,它包含了一系列回调方法,用于处理不同类型的双指手势事件。下面是对每个回调方法的简要介绍:
-
onScale(ScaleGestureDetector detector): 当缩放手势进行中时调用。这个方法会在缩放手势进行过程中持续调用,每次缩放都会触发。参数 detector 提供了有关缩放手势的信息,如当前的缩放因子等。
-
onScaleBegin(ScaleGestureDetector detector): 当缩放手势开始时调用。这个方法在缩放手势的第一次触发时调用,可以用来初始化缩放相关的状态。参数 detector 提供了有关缩放手势的信息。
-
onScaleEnd(ScaleGestureDetector detector): 当缩放手势结束时调用。这个方法在缩放手势结束后调用,可以用来清理缩放相关的状态。参数 detector 提供了有关缩放手势的信息。
3. 示例
import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; public class MyScaleView extends View { private ScaleGestureDetector scaleGestureDetector; private float scaleFactor = 1.0f; public MyScaleView(Context context, AttributeSet attrs) { super(context, attrs); scaleGestureDetector = new ScaleGestureDetector(context, new ScaleListener()); } @Override public boolean onTouchEvent(MotionEvent event) { // 将触摸事件传递给 ScaleGestureDetector scaleGestureDetector.onTouchEvent(event); return true; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 在画布上绘制内容,并根据 scaleFactor 进行缩放 canvas.scale(scaleFactor, scaleFactor, getWidth() / 2f, getHeight() / 2f); // 绘制内容... } private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @Override public boolean onScale(ScaleGestureDetector detector) { // 缩放因子的变化 scaleFactor *= detector.getScaleFactor(); // 限制缩放因子的范围(可选) scaleFactor = Math.max(0.1f, Math.min(scaleFactor, 5.0f)); // 重绘 View invalidate(); return true; } } }
三、自定义一个可拖拽和拉伸的ImageView
1. 思路整理
- 首先,我们可以直接继承 ImageView,并通过 Matrix 来控制图片的移动和拉伸。
- 其次,使用 GestureDetector 监听移动的相关事件,使用 ScaleGestureDetector 监听拉伸的相关事件。
- 最后,我们可能需要控制图片最大和最小缩放的比例。实际应用中还会考虑一些图片边界、双击放大、动画等,可根据需求自行添加。
2. 示例
import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; public class DragZoomImageView extends AppCompatImageView { private static final int NONE = 0; private static final int DRAG = 1; private static final int ZOOM = 2; private int mMode = NONE; private Matrix mFinalMatrix = new Matrix(); private Matrix mSavedMatrix = new Matrix(); // 图像以FitXY显示时使用的Scale大小 private float mOriginScale = 1.0f; // 图像的最小、最大缩放比例 private float mMinScale = 0.5f; private float mMaxScale = 5.0f; private float mCurrentScale = 1.0f; private Bitmap mBitmap; private GestureDetector mGestureDetector; private ScaleGestureDetector mScaleGestureDetector; public DragZoomImageView(@NonNull Context context) { super(context); init(); } public DragZoomImageView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public DragZoomImageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { setScaleType(ScaleType.MATRIX); mGestureDetector = new GestureDetector(getContext(), new GestureListener()); mScaleGestureDetector = new ScaleGestureDetector(getContext(), new ScaleListener()); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); Drawable drawable = getDrawable(); Bitmap bitmap = drawableToBitmap(drawable); if (bitmap != null) { Matrix matrix = getFitCenterMatrix(bitmap.getWidth(), bitmap.getHeight(), w, h); setImageMatrix(matrix); mOriginScale = getMatrixScaleX(matrix); mBitmap = bitmap; } } @Override public void setImageMatrix(Matrix matrix) { super.setImageMatrix(matrix); mFinalMatrix.set(matrix); } @Override public void setImageBitmap(Bitmap bm) { if (bm == null) { return; } super.setImageBitmap(bm); Matrix matrix = getFitCenterMatrix(bm.getWidth(), bm.getHeight(), getWidth(), getHeight()); setImageMatrix(matrix); mOriginScale = getMatrixScaleX(matrix); mBitmap = bm; } @Override public boolean onTouchEvent(MotionEvent event) { if (mBitmap == null) { return false; } switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: // 单指 mMode = DRAG; break; case MotionEvent.ACTION_POINTER_DOWN: // 多指 mMode = ZOOM; break; } if (mMode == DRAG) { mGestureDetector.onTouchEvent(event); } if (mMode == ZOOM) { mScaleGestureDetector.onTouchEvent(event); } return true; } public void handleScale(float scale) { handleScale(scale, getWidth() / 2, getHeight() / 2); } public void handleScale(float scale, float px, float py) { if (scale mMaxScale) { scale = mMaxScale; } if (mCurrentScale == scale) { return; } mCurrentScale = scale; // record scale float newScale = scale * mOriginScale; float oldScale = getMatrixScaleX(mFinalMatrix); float postScale = newScale / oldScale; mFinalMatrix.postScale(postScale, postScale, px, py); super.setImageMatrix(mFinalMatrix); } private float getMatrixScaleX(Matrix matrix) { float[] values = new float[9]; matrix.getValues(values); return values[Matrix.MSCALE_X]; } private Matrix getFitCenterMatrix(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight) { Matrix matrix = new Matrix(); matrix.reset(); float scale; float dx; float dy; scale = Math.min((float) viewWidth / (float) bitmapWidth, (float) viewHeight / (float) bitmapHeight); dx = Math.round((viewWidth - bitmapWidth * scale) * 0.5f); dy = Math.round((viewHeight - bitmapHeight * scale) * 0.5f); matrix.setScale(scale, scale); matrix.postTranslate(dx, dy); return matrix; } private Bitmap drawableToBitmap(Drawable drawable) { if (drawable == null) { return null; } if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } else if (drawable instanceof ColorDrawable) { // 如果 Drawable 是 ColorDrawable,则创建一个相同大小的 Bitmap ColorDrawable colorDrawable = (ColorDrawable) drawable; int width = colorDrawable.getIntrinsicWidth(); int height = colorDrawable.getIntrinsicHeight(); Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); colorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); colorDrawable.draw(canvas); return bitmap; } else { // 如果 Drawable 不是 BitmapDrawable 或 ColorDrawable,则返回空 return null; } } private class GestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDown(MotionEvent e) { mSavedMatrix.set(mFinalMatrix); return super.onDown(e); } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { Matrix matrix = new Matrix(mSavedMatrix); float dx = e2.getX() - e1.getX(); float dy = e2.getY() - e1.getY(); matrix.postTranslate(dx, dy); setImageMatrix(matrix); return true; } } private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { float px; float py; @Override public boolean onScaleBegin(ScaleGestureDetector detector) { px = detector.getFocusX(); py = detector.getFocusY(); return true; } @Override public boolean onScale(ScaleGestureDetector detector) { float scale = detector.getScaleFactor() * mCurrentScale; handleScale(scale, px, py); return true; } } }
-