手势识别-PhotoView

Android知识总结

一、GestureDetector单指点击手势

GestureDetector的工作原理是,当我们接收到用户触摸消息时,将这个消息交给GestureDetector去加工,我们通过设置侦听器获得GestureDetector处理后的手势。

GestureDetector提供了两个侦听器接口,OnGestureListener处理单击类消息,OnDoubleTapListener处理双击类消息。

  • OnGestureListener 的接口有这几个:
// 单击,触摸屏按下时立刻触发  
abstract boolean onDown(MotionEvent e);  

// 单击抬起,手指离开触摸屏时触发(长按、滚动、滑动时,不会触发这个手势)  
//单击抬起时触发,且只在双击的第一次抬起时触发。(连续点击三次,则会触发两次)
abstract boolean onSingleTapUp(MotionEvent e);  

// 短按(触摸反馈),触摸屏按下后片刻后抬起,会触发这个手势,如果迅速抬起则不会  
//它是在 View 被点击(按下)时调用,其作用是给用户一个视觉反馈,让用户知道我这个控件被点击了,
//这样的效果我们也可以用 Material design 的 ripple 实现,或者直接 drawable 写个背景也行。
//它是一种延时回调,延迟时间是 100 ms。也就是说用户手指按下后,如果立即抬起或者事件立即被拦截,
//时间没有超过 100 ms的话,这条消息会被 remove 掉,也就不会触发这个回调。
abstract void onShowPress(MotionEvent e);  

// 长按,触摸屏按下后既不抬起也不移动,过一段时间后触发  
abstract void onLongPress(MotionEvent e);  

// 滚动,触摸屏按下后移动 
// e1:手指按下时的 MotionEvent
// e2:手指当前的 MotionEvent
// distanceX:在X轴上划过的距离 --- 旧位置 减去 新位置
// distanceY:在Y轴上划过的距离
abstract boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);  

//处理惯性滑动。最小滑动速度50dip/s(dp=dip)。最大8000dp/s。
// 抛掷(惯性滑动)
// e1:手指按下时的 MotionEvent,可以知道按下位置等
// e2:手指当前的 MotionEvent
// velocityX:在X轴上的运动速度(像素/秒)
// velocityY:在Y轴上的运动速度(像素/秒) 
abstract boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);  
  • OnDoubleTapListener 的接口有这几个:
    300ms内点击两次才算双击。
// 双击,手指在触摸屏上迅速点击第二下时触发  
abstract boolean onDoubleTap(MotionEvent e);  

// 在双击手势中发生事件时通知,包括按下、移动和抬起事件
//双击第二次点击时的按下,移动和抬起事件都会回调。
abstract boolean onDoubleTapEvent(MotionEvent e);  

// 单击确认,即很快的按下并抬起,但并不连续点击第二下  
abstract boolean onSingleTapConfirmed(MotionEvent e);  

有时候我们并不需要处理上面所有手势,方便起见,Android提供了另外一个类SimpleOnGestureListener,实现了OnGestureListener, OnDoubleTapListener, OnContextClickListener这三个接口,并重写了接口的方法。所以我们可以 new 一个 SimpleOnGestureListener 对象,这样就不用重写接口的所有方法,而只写自己需要的方法即可

1.1、双击缩放

// 创建 GestureDetector对象
gestureDetector = new GestureDetector(context, new PhotoGestureListener());

class PhotoGestureListener extends GestureDetector.SimpleOnGestureListener {

    // 必须为true才表示消费事件
    @Override
    public boolean onDown(MotionEvent e) {
        return true;
    }

    // 双击处理缩放
    @Override
    public boolean onDoubleTap(MotionEvent e) {
        isEnlarge = !isEnlarge;
        if (isEnlarge) {
            currentScale = bigScale;
        } else {
            currentScale = smallScale;
        }
        invalidate();
        return super.onDoubleTap(e);
    }
}

//必须重写onTouchEvent,因为GestureDetector里面自己重写了事件处理。
@Override
public boolean onTouchEvent(MotionEvent event) {
    return gestureDetector.onTouchEvent(event);
}

1.2、缩放效果

public boolean onDoubleTap(MotionEvent e) {
    isEnlarge = !isEnlarge;
    if (isEnlarge) {
        getScaleAnimator().start();
    } else {
        getScaleAnimator().reverse();
    }
    return super.onDoubleTap(e);
}

private ObjectAnimator getScaleAnimator() {
    if (scaleAnimator == null) {
        // values必须要有,否则运行时报错
        scaleAnimator = ObjectAnimator.ofFloat(this,
                "currentScale", 0);
    }
    scaleAnimator.setFloatValues(smallScale, bigScale);
    return scaleAnimator;
}

public float getCurrentScale() {
    return currentScale;
}

public void setCurrentScale(float currentScale) {
    this.currentScale = currentScale;
    // 属性动画变化是刷新界面
    invalidate();
}

1.3、手指滑动

public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    if (isEnlarge) {
        offsetX -= distanceX;
        offsetY -= distanceY;
        // 处理边界问题
        fixOffsets();
        invalidate();
    }
    return super.onScroll(e1, e2, distanceX, distanceY);
}

private void fixOffsets() {
    offsetX = Math.min(offsetX, (bitmap.getWidth() * bigScale - getWidth()) / 2);
    offsetX = Math.max(offsetX, -(bitmap.getWidth() * bigScale - getWidth()) / 2);
    offsetY = Math.min(offsetY, (bitmap.getHeight() * bigScale - getHeight()) / 2);
    offsetY = Math.max(offsetY, -(bitmap.getHeight() * bigScale - getHeight()) / 2);
}

1.4、惯性滑动

public boolean onFling(MotionEvent down, MotionEvent event, float velocityX, float velocityY) {
    if (isEnlarge) {
        overScroller.fling((int) offsetX, (int) offsetY, (int) velocityX, (int) velocityY,
                -(int) (bitmap.getWidth() * bigScale - getWidth()) / 2,
                (int) (bitmap.getWidth() * bigScale - getWidth()) / 2,
                -(int) (bitmap.getHeight() * bigScale - getHeight()) / 2,
                (int) (bitmap.getHeight() * bigScale - getHeight()) / 2);
        postOnAnimation(flingRunner);
    }
    return false;
}

class FlingRunner implements Runnable {

        @Override
        public void run() {
            // 动画还在执行,返回true
            if (overScroller.computeScrollOffset()) {
                offsetX = overScroller.getCurrX();
                offsetY = overScroller.getCurrY();
                invalidate();
                postOnAnimation(this);
            }
        }
    }

二、ScaleGestureDetector 双指点击手势

//缩放时。返回值代表本次缩放事件是否已被处理。如果已被处理,那么detector就会重置缩 
//放事件;如果未被处理,detector会继续进行计算,修改getScaleFactor()的返回值,直到被
//处理为止。因此,它常用在判断只有缩放值达到一定数值时才进行缩放。
public boolean onScale(ScaleGestureDetector detector);

// 缩放开始。该detector是否处理后继的缩放事件。返回false时,不会执行onScale()。
public boolean onScaleBegin(ScaleGestureDetector detector);

//缩放结束时
public void onScaleEnd(ScaleGestureDetector detector);

2.1、实现

scaleGestureListener = new PhotoScaleGestureListener();
scaleGestureDetector = new ScaleGestureDetector(context, scaleGestureListener);

public boolean onTouchEvent(MotionEvent event) {
    // 双指缩放操作优先处理事件
    boolean result = scaleGestureDetector.onTouchEvent(event);
    // 如果不是双指缩放才处理手势事件
    if (!scaleGestureDetector.isInProgress()) {
        result = gestureDetector.onTouchEvent(event);
    }

    return result;
}

class PhotoScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener {

    float initialScale;

    // 缩放中回调 -- 倍数,焦点
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        // getScaleFactor:将比例因子从上一个缩放事件返回到当前事件
        currentScale = initialScale * detector.getScaleFactor();
        invalidate();
        return false;
    }

    // 缩放前回调,返回true 消费这个缩放事件
    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        initialScale = currentScale;
        return true;
    }

    // 缩放后回调
    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {

    }
}

三、实例代码

实现 图片的放大、缩小、平移、惯性处理和单双指操作。

public class PhotoView extends View {
    private Bitmap mBitmap;
    private Paint mPaint;

    // 偏移值
    private float mOriginalOffsetX;
    private float mOriginalOffsetY;
    // 一边全屏,一边留白
    private float mSmallScale;
    // 一边全屏,一边超出屏幕
    private float mBigScale;
    private float OVER_SCALE_FACTOR = 1.5f;
    private float mCurrentScale;
    private FlingRunner flingRunner;
    //用来实现抛出后的回弹效果
    private OverScroller overScroller;
    //是否放大
    private boolean isEnlarge;
    //偏移量
    private float offsetX;
    private float offsetY;
    //判断是否进行了双指缩放
    private boolean isScale;

    private GestureDetector mGestureDetector;
    private ScaleGestureDetector mScaleGestureDetector;

    public PhotoView(Context context) {
        this(context, null);
    }

    public PhotoView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PhotoView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        // 获取bitmap对象
        mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.photo);
        mPaint = new Paint();

        mGestureDetector = new GestureDetector(context, new PhotoGestureListener());
        overScroller = new OverScroller(context);
        flingRunner = new FlingRunner();

        mScaleGestureDetector = new ScaleGestureDetector(context, new PhotoScaleGestureListener());
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 双指操作优先
        boolean result = mScaleGestureDetector.onTouchEvent(event);
        if (!mScaleGestureDetector.isInProgress()) {
            result = mGestureDetector.onTouchEvent(event);
        }
        return result;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 需要得到 浮点数,否则会留条小缝
        mOriginalOffsetX = (getWidth() - mBitmap.getWidth()) / 2f;
        mOriginalOffsetY = (getHeight() - mBitmap.getHeight()) / 2f;

        if ((float) mBitmap.getWidth() / mBitmap.getHeight() > (float) getWidth() / getHeight()) {
            // 图片是横向的
            mSmallScale = (float) getWidth() / mBitmap.getWidth();
            mBigScale = (float) getHeight() / mBitmap.getHeight();
        } else {
            // 纵向的图片
            mSmallScale = (float) getHeight() / mBitmap.getHeight();
            mBigScale = (float) getWidth() / mBitmap.getWidth();
        }
        mCurrentScale = mSmallScale;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //TODO 通过系数解决,放大小小后图片位置仍然居中
        float scaleFraction = (mCurrentScale - mSmallScale) / (mBigScale - mSmallScale);
        //平移处理
        canvas.translate(offsetX * scaleFraction, offsetY * scaleFraction);
        // smallScale --》 bigScale (实际上是缩放坐标系)
        canvas.scale(mCurrentScale, mCurrentScale, getWidth() / 2f, getHeight() / 2f);
        // 绘制bitmap
        canvas.drawBitmap(mBitmap, mOriginalOffsetX, mOriginalOffsetY, mPaint);
    }


    class PhotoGestureListener extends GestureDetector.SimpleOnGestureListener {
        public PhotoGestureListener() {
            super();
        }

        // Up时触发  双击的时候,触发两次??第二次抬起时触发
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return super.onSingleTapUp(e);
        }

        // 长按 -- 300ms
        @Override
        public void onLongPress(MotionEvent e) {
            super.onLongPress(e);
        }

        /**
         * 类似move事件
         *
         * @param e1
         * @param e2
         * @param distanceX 在 X 轴上滑过的距离(单位时间) 旧位置 - 新位置
         * @param distanceY
         * @return
         */
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            // 只有在放大的情况下,才能进行移动
            if (isEnlarge) {
                offsetX -= distanceX;
                offsetY -= distanceY;
                fixOffsets();
                invalidate();
            }
            return super.onScroll(e1, e2, distanceX, distanceY);
        }

        // 抛掷
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            if (isEnlarge) {
                // 只会处理一次,抛出后的回弹效果实现
                overScroller.fling((int) offsetX, (int) offsetY, (int) velocityX, (int) velocityY,
                        -(int) (mBitmap.getWidth() * mBigScale - getWidth()) / 2,
                        (int) (mBitmap.getWidth() * mBigScale - getWidth()) / 2,
                        -(int) (mBitmap.getHeight() * mBigScale - getHeight()) / 2,
                        (int) (mBitmap.getHeight() * mBigScale - getHeight()) / 2, 600, 600);
                postOnAnimation(flingRunner); //启动动画事务
            }
            return super.onFling(e1, e2, velocityX, velocityY);
        }

        // 延时触发 100ms -- 点击效果,水波纹
        @Override
        public void onShowPress(MotionEvent e) {
            super.onShowPress(e);
        }

        // 按下 -- 注意:直接返回true
        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }

        // 双击 -- 第二次点击按下的时候 -- 40ms(小于表示:防抖动) -- 300ms
        @Override
        public boolean onDoubleTap(MotionEvent e) {
            isEnlarge = !isEnlarge;
            if (isEnlarge) {
                //TODO  解决,以手指点击的位置放大缩小问题
                offsetX = (e.getX() - getWidth() / 2f) - (e.getX() - getWidth() / 2f) * mBigScale / mSmallScale;
                offsetY = (e.getY() - getHeight() / 2f) - (e.getY() - getHeight() / 2f) * mBigScale / mSmallScale;
                fixOffsets();
                // 启动属性动画
                getScaleAnimator().start();
            } else {
                getScaleAnimator().reverse();
            }
            return super.onDoubleTap(e);
        }

        // 双击第二次 down、move、up都会触发
        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            return super.onDoubleTapEvent(e);
        }

        // 单击按下时触发,双击时不触发,
        // 延时300ms触发TAP事件
        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            return super.onSingleTapConfirmed(e);
        }

        @Override
        public boolean onContextClick(MotionEvent e) {
            return super.onContextClick(e);
        }
    }

    //实现一直循环执行动画
    class FlingRunner implements Runnable {

        @Override
        public void run() {
            // 动画还在执行 则返回true
            if (overScroller.computeScrollOffset()) {
                //拿到当前偏移量,进行刷新
                offsetX = overScroller.getCurrX();
                offsetY = overScroller.getCurrY();
                invalidate();
                // 没帧动画执行一次,性能更好
                postOnAnimation(this);
            }
        }
    }


    private void fixOffsets() {
        offsetX = Math.min(offsetX, (mBitmap.getWidth() * mBigScale - getWidth()) / 2);
        offsetX = Math.max(offsetX, -(mBitmap.getWidth() * mBigScale - getWidth()) / 2);
        offsetY = Math.min(offsetY, (mBitmap.getHeight() * mBigScale - getHeight()) / 2);
        offsetY = Math.max(offsetY, -(mBitmap.getHeight() * mBigScale - getHeight()) / 2);
    }

    /**
     * 属性动画,设置放大缩小的效果
     */
    private ObjectAnimator mScaleAnimator;

    private ObjectAnimator getScaleAnimator() {
        if (mScaleAnimator == null) {
            mScaleAnimator = ObjectAnimator.ofFloat(this, "currentScale", 0);
        }
        //TODO 解决双指缩放后,点击放大缩小时的动画闪烁问题
        if (isScale) {
            isScale = false;
            mScaleAnimator.setFloatValues(mSmallScale, mCurrentScale);
        } else {
            //放大缩小的范围
            mScaleAnimator.setFloatValues(mSmallScale, mBigScale);
        }
        return mScaleAnimator;
    }

    // 属性动画,值会不断地从 smallScale 慢慢 加到 bigScale, 通过反射调用改方法
    public void setCurrentScale(float currentScale) {
        mCurrentScale = currentScale;
        invalidate();
    }

    //实现双指放大和缩小功能
    class PhotoScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener{
        float initialScale;
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            if ((mCurrentScale > mSmallScale && !isEnlarge)
                    || (mCurrentScale == mSmallScale && isEnlarge)) {
                isEnlarge = !isEnlarge;
            }
            //detector.getScaleFactor() 获取缩放因子
            mCurrentScale = initialScale * detector.getScaleFactor();
            isScale = true;
            invalidate();
            return false;
        }

        // 注意:返回true,消费事件
        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            initialScale = mCurrentScale;
            return true;
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            super.onScaleEnd(detector);
        }
    }
}
  • OverScroller 里的 fling 方法参数讲解
/**
 * 
 * @param startX    x轴,起始位置
 * @param startY    Y轴,起始位置
 * @param velocityX     x轴,速度(可以用 onFling 中的系统速度)
 * @param velocityY     Y轴,速度(可以用 onFling 中的系统速度)
 * @param minX      x最小限制条件
 * @param maxX      x最大限制条件
 * @param minY      y最小限制条件
 * @param maxY      y最大限制条件
 * @param overX     x轴,允许的最大滑动距离(就是可留白的大小)
 * @param overY     Y轴,允许的最大滑动距离(就是可留白的大小)
 */
public void fling(int startX, int startY, int velocityX, int velocityY,
                  int minX, int maxX, int minY, int maxY, int overX, int overY) {
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容