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. 示例

java 复制代码
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. 示例

java 复制代码
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. 示例

java 复制代码
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 < mMinScale) {
            scale = mMinScale;
        }
        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;
        }

    }
}
相关推荐
阿巴斯甜9 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker9 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952710 小时前
Andorid Google 登录接入文档
android
黄林晴11 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android