手势识别-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) {
}

你可能感兴趣的:(手势识别-PhotoView)