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;
        }

    }
}
相关推荐
雨白1 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹3 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空4 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭5 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日6 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安6 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑6 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟10 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡11 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0011 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体