Android GestureDetector ScaleGestureDetector

       Android代码中给我们提供大量的帮助类来方便我们的使用。今天咱们就来看下手势帮助类GestureDetector、ScaleGestureDetector。

一、GestureDetector

       Android手机屏幕上,当咱们触摸屏幕的时候,会产生许多手势事件,如down,up,scroll,filing等等。咱们可以在onTouchEvent()方法里面完成各种手势识别。但是,咱们自己去识别各种手势就比较麻烦了,而且有些情况可能考虑的不是那么的全面。所以,为了方便咱们的时候Android就给提供了GestureDetector帮助类来方便大家的使用。

       GestureDetector类给我们提供了三个接口,一个外部类。

  • OnGestureListener:接口,用来监听手势事件(6种)。
  • OnDoubleTapListener:接口,用来监听双击事件。
  • OnContextClickListener:接口,外接设备,比如外接鼠标产生的事件(本文中我们不考虑)。
  • SimpleOnGestureListener:外部类,SimpleOnGestureListener其实上面三个接口中所有函数的集成,它包含了这三个接口里所有必须要实现的函数而且都已经重写,但所有方法体都是空的。需要自己根据情况去重写。

OnGestureListener接口方法解释:

    public interface OnGestureListener {

        /**
         * 按下。返回值表示事件是否处理
         */
        boolean onDown(MotionEvent e);

        /**
         * 短按(手指尚未松开也没有达到scroll条件)
         */
        void onShowPress(MotionEvent e);

        /**
         * 轻触(手指松开)
         */
        boolean onSingleTapUp(MotionEvent e);

        /**
         * 滑动(一次完整的事件可能会多次触发该函数)。返回值表示事件是否处理
         */
        boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);

        /**
         * 长按(手指尚未松开也没有达到scroll条件)
         */
        void onLongPress(MotionEvent e);

        /**
         * 滑屏(用户按下触摸屏、快速滑动后松开,返回值表示事件是否处理)
         */
        boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
    }

OnDoubleTapListener接口方法解释:

    public interface OnDoubleTapListener {
        /**
         * 单击事件(onSingleTapConfirmed,onDoubleTap是两个互斥的函数)
         */
        boolean onSingleTapConfirmed(MotionEvent e);

        /**
         * 双击事件
         */
        boolean onDoubleTap(MotionEvent e);

        /**
         * 双击事件产生之后手指还没有抬起的时候的后续事件
         */
        boolean onDoubleTapEvent(MotionEvent e);
    }

SimpleOnGestureListener实现了OnGestureListener、OnDoubleTapListener、OnContextClickListener。SimpleOnGestureListener里面的方法是是三个接口的集合。

1.1、GestureDetector使用

       GestureDetector的使用非常的简单,分为三个步骤:

  • 定义GestureDetector类,
  • 将touch事件交给GestureDetector(onTouchEvent函数里面调用GestureDetector的onTouchEvent函数)。
  • 处理SimpleOnGestureListener或者OnGestureListener、OnDoubleTapListener、OnContextClickListener三者之一的回调。

       我们用一个简单的实例来说明GestureDetector的使用。我就简单的写一个View,然后看看各个事件的触发情况。

public class GestureView extends View {

    //定义GestureDetector类
    private GestureDetector mGestureDetector;

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

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

    public GestureView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mGestureDetector = new GestureDetector(context, mOnGestureListener);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mGestureDetector.onTouchEvent(event);
    }

    private final GestureDetector.OnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            Log.d("tuacy", "onSingleTapUp");
            return true;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            Log.d("tuacy", "onLongPress");
            super.onLongPress(e);
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            Log.d("tuacy", "onScroll");
            return true;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            Log.d("tuacy", "onFling");
            return true;
        }

        @Override
        public void onShowPress(MotionEvent e) {
            Log.d("tuacy", "onShowPress");
            super.onShowPress(e);
        }

        @Override
        public boolean onDown(MotionEvent e) {
            Log.d("tuacy", "onDown");
            return true;
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            Log.d("tuacy", "onDoubleTap");
            return true;
        }

        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            Log.d("tuacy", "onDoubleTapEvent");
            return true;
        }

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            Log.d("tuacy", "onSingleTapConfirmed");
            return true;
        }
    };
}

我们总结下各个动作对应的回调

  • 快速点击下View:onDow() -> onSingleTapUp() -> onSingleTapConfirmed()。
  • 短按View不滑动:onDown() -> onShowPress() -> onSingleTapUp() -> onSingleTapConfirmed()。
  • 长按View不滑动:onDown() -> onShowPress() -> onLongPress()。
  • 滑动:onDown() -> onScroll() -> onScroll()....。
  • 快速滑动:onDown() -> onScroll() -> onScroll().... -> onFling()。
  • 快速点击两下:onDown() -> onSingleTapUp() -> onDoubleTap() -> onDoubleTapEvent() -> onDoubleTapEvent()...。

GestureDetector的使用给一个建议,GestureDetector的所有回调函数,有返回值的。如果你用到了就返回true。因为有些函数你不返回true的话可能后续的事件传递不进来。这里我们可以给大家留一个问题,大家可以自己分下下返回false的情况对应的回调顺序。比如onDown()函数我们返回false,快速点击的时候回调调用的情况。

1.2、GestureDetector源码解析

       GestureDetector源码也不是很复杂,我们做一个非常简单的分析。我们从构造函数开始。

    public GestureDetector(Context context, OnGestureListener listener) {
        this(context, listener, null);
    }
    
    public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
        if (handler != null) {
            mHandler = new GestureHandler(handler);
        } else {
            mHandler = new GestureHandler();
        }
        mListener = listener;
        if (listener instanceof OnDoubleTapListener) {
            setOnDoubleTapListener((OnDoubleTapListener) listener);
        }
        if (listener instanceof OnContextClickListener) {
            setContextClickListener((OnContextClickListener) listener);
        }
        init(context);
    }
    
    private void init(Context context) {
        if (mListener == null) {
            throw new NullPointerException("OnGestureListener must not be null");
        }
        mIsLongpressEnabled = true;

        // Fallback to support pre-donuts releases
        int touchSlop, doubleTapSlop, doubleTapTouchSlop;
        if (context == null) {
            //noinspection deprecation
            touchSlop = ViewConfiguration.getTouchSlop();
            doubleTapTouchSlop = touchSlop; // Hack rather than adding a hiden method for this
            doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
            //noinspection deprecation
            mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
            mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
        } else {
            final ViewConfiguration configuration = ViewConfiguration.get(context);
            touchSlop = configuration.getScaledTouchSlop();
            doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
            doubleTapSlop = configuration.getScaledDoubleTapSlop();
            mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
            mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
        }
        mTouchSlopSquare = touchSlop * touchSlop;
        mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
        mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
    }

       GestureDetector构造函数里面的代码也不复杂,都是在设置一些变量。其中mHandler:用于sendMessage,mListener、mDoubleTapListener、mContextClickListener:三个接口的变量,mIsLongpressEnabled:是否支持长按操作,mMinimumFlingVelocity:fling的最小速度,mMaximumFlingVelocity:fling的最大速度,mTouchSlopSquare:用来判断是否开始scroll,mDoubleTapTouchSlopSquare:判断双击的时候用到,第一个单击的时候产生了MotionEvent.ACTION_MOVE,并且move的距离超过了这个值 就不认为是双击事件,mDoubleTapSlopSquare:判断双击的时候用到,两次单击范围要在这个值之内。否则不算是双击事件。

       分析完GestureDetector的构造函数,接下来我们直接看GestureDetector的onTouchEvent()函数,这个函数我们主要分析:MotionEvent.ACTION_DOWN、MotionEvent.ACTION_MOVE、MotionEvent.ACTION_UP三个事件的处理过程。

    public boolean onTouchEvent(MotionEvent ev) {
        // 这一部分是用于测试的,我们不用管
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 0);
        }

        final int action = ev.getAction();

        // 用来记录滑动速度
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);
        ...
        boolean handled = false;

        switch (action & MotionEvent.ACTION_MASK) {
            ...
            case MotionEvent.ACTION_DOWN:
                if (mDoubleTapListener != null) {
                    boolean hadTapMessage = mHandler.hasMessages(TAP);
                    if (hadTapMessage) mHandler.removeMessages(TAP);
                    if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
                        isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
                        // This is a second tap
                        mIsDoubleTapping = true;
                        // Give a callback with the first tap of the double-tap
                        handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
                        // Give a callback with down event of the double-tap
                        handled |= mDoubleTapListener.onDoubleTapEvent(ev);
                    } else {
                        // This is a first tap
                        mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
                    }
                }

                mDownFocusX = mLastFocusX = focusX;
                mDownFocusY = mLastFocusY = focusY;
                if (mCurrentDownEvent != null) {
                    mCurrentDownEvent.recycle();
                }
                mCurrentDownEvent = MotionEvent.obtain(ev);
                mAlwaysInTapRegion = true;
                mAlwaysInBiggerTapRegion = true;
                mStillDown = true;
                mInLongPress = false;
                mDeferConfirmSingleTap = false;

                // 用于处理长按事件处理 对应onLongPress()函数
                if (mIsLongpressEnabled) {
                    mHandler.removeMessages(LONG_PRESS);
                    mHandler.sendEmptyMessageAtTime(LONG_PRESS,
                                                    mCurrentDownEvent.getDownTime() + LONGPRESS_TIMEOUT);
                }
                // 用于轻触事件处理,对应onShowPress()函数
                mHandler.sendEmptyMessageAtTime(SHOW_PRESS,
                                                mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
                handled |= mListener.onDown(ev);
                break;

            case MotionEvent.ACTION_MOVE:
                if (mInLongPress || mInContextClick) {
                    break;
                }
                final float scrollX = mLastFocusX - focusX;
                final float scrollY = mLastFocusY - focusY;
                if (mIsDoubleTapping) {
                    // Give the move events of the double-tap
                    handled |= mDoubleTapListener.onDoubleTapEvent(ev);
                } else if (mAlwaysInTapRegion) {
                    final int deltaX = (int) (focusX - mDownFocusX);
                    final int deltaY = (int) (focusY - mDownFocusY);
                    int distance = (deltaX * deltaX) + (deltaY * deltaY);
                    int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
                    if (distance > slopSquare) {
                        handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                        mLastFocusX = focusX;
                        mLastFocusY = focusY;
                        mAlwaysInTapRegion = false;
                        mHandler.removeMessages(TAP);
                        mHandler.removeMessages(SHOW_PRESS);
                        mHandler.removeMessages(LONG_PRESS);
                    }
                    int doubleTapSlopSquare = isGeneratedGesture ? 0 : mDoubleTapTouchSlopSquare;
                    if (distance > doubleTapSlopSquare) {
                        mAlwaysInBiggerTapRegion = false;
                    }
                } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
                    handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                    mLastFocusX = focusX;
                    mLastFocusY = focusY;
                }
                break;

            case MotionEvent.ACTION_UP:
                mStillDown = false;
                MotionEvent currentUpEvent = MotionEvent.obtain(ev);
                if (mIsDoubleTapping) {
                    // Finally, give the up event of the double-tap
                    handled |= mDoubleTapListener.onDoubleTapEvent(ev);
                } else if (mInLongPress) {
                    mHandler.removeMessages(TAP);
                    mInLongPress = false;
                } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
                    handled = mListener.onSingleTapUp(ev);
                    if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
                        mDoubleTapListener.onSingleTapConfirmed(ev);
                    }
                } else if (!mIgnoreNextUpEvent) {

                    // A fling must travel the minimum tap distance
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    final int pointerId = ev.getPointerId(0);
                    velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
                    final float velocityY = velocityTracker.getYVelocity(pointerId);
                    final float velocityX = velocityTracker.getXVelocity(pointerId);

                    if ((Math.abs(velocityY) > mMinimumFlingVelocity)
                        || (Math.abs(velocityX) > mMinimumFlingVelocity)){
                        handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
                    }
                }
                if (mPreviousUpEvent != null) {
                    mPreviousUpEvent.recycle();
                }
                // Hold the event we obtained above - listeners may have changed the original.
                mPreviousUpEvent = currentUpEvent;
                if (mVelocityTracker != null) {
                    // This may have been cleared when we called out to the
                    // application above.
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                mIsDoubleTapping = false;
                mDeferConfirmSingleTap = false;
                mIgnoreNextUpEvent = false;
                mHandler.removeMessages(SHOW_PRESS);
                mHandler.removeMessages(LONG_PRESS);
                break;

            case MotionEvent.ACTION_CANCEL:
                cancel();
                break;
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);
        }
        return handled;
    }

MotionEvent.ACTION_DOWN处理部分我们分四个部分来看:

  • 对DoubleTapListener做处理:DoubleTapListener是用于处理双击事件,所以肯定是要有前后两个事件的,我们可以看下大概的逻辑mCurrentDownEvent是前一次事件按下时候的MotionEvent,mPreviousUpEvent是前一次事件抬起是的的MotionEvent。从这段代码我们也能发现onSingleTapConfirmed()函数和onDoubleTap()两个函数是互斥的。其中isConsideredDoubleTap()函数是用于判断是否达到了双击事件的条件。mIsDoubleTapping表示产生了双击事件。
  • 长按事件的处理,mHandler.sendEmptyMessageAtTime(LONG_PRESS,
    mCurrentDownEvent.getDownTime() + LONGPRESS_TIMEOUT); 发送了一个LONG_PRESS类型的延时message。至于长按事件会不会触发,就要看LONG_PRESS对应的message在LONGPRESS_TIMEOUT时间内会不会被remove掉。
  • 轻触事件的处理,mHandler.sendEmptyMessageAtTime(SHOW_PRESS,
    mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);发送了一个SHOW_PRESS类型的延时message。同样轻触事件会不会触发也的看后续SHOW_PRESS对应的message会不会被remove掉。
  • 调用onDown()函数,handled |= mListener.onDown(ev);

       MotionEvent.ACTION_DOWN的时候我们还得关注下返回值,只有返回true才能保证后续事件在进入到onTouchEvent()函数里面来。

MotionEvent.ACTION_MOVE处理部分

       MotionEvent.ACTION_MOVE部分逻辑处理。一开始判断是否产生了长按事件,产生了长按事件直接break掉。接下来关键点在里面的if else。mIsDoubleTapping:表示产生了双击事件,mAlwaysInTapRegion:表示是否进入了滑动状态。从逻辑处理过程可以看到产生了滑动事件就会把TAP,SHOW_PRESS,LONG_PRESS对应的消息都移除掉。

MotionEvent.ACTION_UP处理部分

       MotionEvent.ACTION_UP的逻辑也不难,如果产生了双击事件就回调onDoubleTapEvent()函数,如果还没有进入滑动的状态就回调onSingleTapUp(),然后再看要不要回调onSingleTapConfirmed()函数,这里咱们也能发现产生了双击事件就不会回调onSingleTapConfirmed()函数。最后就是onFling()函数的回调。

二、ScaleGestureDetector

       ScaleGestureDetector是用于处理缩放的工具类,用法与GestureDetector类似,都是通过onTouchEvent()关联相应的MotionEvent事件。

       ScaleGestureDetector类给提供了OnScaleGestureListener接口,来告诉我们缩放的过程中的一些回调。

OnScaleGestureListener回调函数介绍

    public interface OnScaleGestureListener {

        /**
         * 缩放进行中,返回值表示是否下次缩放需要重置,如果返回ture,那么detector就会重置缩放事件,如果返回false,detector会在之前的缩放上继续进行计算
         */
        public boolean onScale(ScaleGestureDetector detector);

        /**
         * 缩放开始,返回值表示是否受理后续的缩放事件
         */
        public boolean onScaleBegin(ScaleGestureDetector detector);

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

ScaleGestureDetector类常用函数介绍,因为在缩放的过程中,要通过ScaleGestureDetector来获取一些缩放信息。

    /**
     * 缩放是否正处在进行中
     */
    public boolean isInProgress();

    /**
     * 返回组成缩放手势(两个手指)中点x的位置
     */
    public float getFocusX();

    /**
     * 返回组成缩放手势(两个手指)中点y的位置
     */
    public float getFocusY();

    /**
     * 组成缩放手势的两个触点的跨度(两个触点间的距离)
     */
    public float getCurrentSpan();

    /**
     * 同上,x的距离
     */
    public float getCurrentSpanX();

    /**
     * 同上,y的距离
     */
    public float getCurrentSpanY();

    /**
     * 组成缩放手势的两个触点的前一次缩放的跨度(两个触点间的距离)
     */
    public float getPreviousSpan();

    /**
     * 同上,x的距离
     */
    public float getPreviousSpanX();

    /**
     * 同上,y的距离
     */
    public float getPreviousSpanY();

    /**
     * 获取本次缩放事件的缩放因子,缩放事件以onScale()返回值为基准,一旦该方法返回true,代表本次事件结束,重新开启下次缩放事件。
     */
    public float getScaleFactor();

    /**
     * 返回上次缩放事件结束时到当前的时间间隔
     */
    public long getTimeDelta();

    /**
     * 获取当前motion事件的时间
     */
    public long getEventTime();

2.1、ScaleGestureDetector使用

       ScaleGestureDetector的使用也是简单的分为三步。

  • 定义ScaleGestureDetector类,
  • 将touch事件交给ScaleGestureDetector(onTouchEvent函数里面调用ScaleGestureDetector的onTouchEvent函数)。
  • 处理OnScaleGestureListener各个回调。

       接下来我们通过重写ImageView,使用ScaleGestureDetector来实现图片的缩放功能。

代码是网上找的

public class ScaleImageView extends AppCompatImageView
    implements ScaleGestureDetector.OnScaleGestureListener, ViewTreeObserver.OnGlobalLayoutListener {

    private static final int MAX_SCALE_TIME = 4;

    private ScaleGestureDetector mScaleGestureDetector;
    // 缩放工具类
    private Matrix               mMatrix;
    private boolean              mFirstLayout;
    private float                mBaseScale;


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

    public ScaleImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ScaleImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScaleGestureDetector = new ScaleGestureDetector(context, this);
        mMatrix = new Matrix();
        setScaleType(ScaleType.MATRIX);
        mFirstLayout = true;
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        //移除OnGlobalLayoutListener
        getViewTreeObserver().removeGlobalOnLayoutListener(this);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mScaleGestureDetector.onTouchEvent(event);
    }

    /**
     * 缩放进行中
     */
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        if (null == getDrawable() || mMatrix == null) {
            return true;
        }
        // 获取缩放因子
        float scaleFactor = detector.getScaleFactor();
        float scale = getScale();
        // 控件图片的缩放范围
        if ((scale < mBaseScale * MAX_SCALE_TIME && scaleFactor > 1.0f) || (scale > mBaseScale && scaleFactor < 1.0f)) {
            if (scale * scaleFactor < mBaseScale) {
                scaleFactor = mBaseScale / scale;
            }
            if (scale * scaleFactor > mBaseScale * MAX_SCALE_TIME) {
                scaleFactor = mBaseScale * MAX_SCALE_TIME / scale;
            }
            // 以屏幕中央位置进行缩放
            mMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
            borderAndCenterCheck();
            setImageMatrix(mMatrix);
        }
        return false;
    }

    /**
     * 缩放开始
     */
    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        return true;
    }

    /**
     * 缩放结束
     */
    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
    }

    @Override
    public void onGlobalLayout() {
        if (mFirstLayout) {
            mFirstLayout = false;
            // 获取控件的宽度和高度
            int viewWidth = getWidth();
            int viewHeight = getHeight();
            // 获取到ImageView对应图片的宽度和高度
            Drawable drawable = getDrawable();
            if (null == drawable) {
                return;
            }
            // 图片固有宽度
            int drawableWidth = drawable.getIntrinsicWidth();
            // 图片固有高度
            int drawableHeight = drawable.getIntrinsicHeight();
            // 接下来对图片做初始的缩放处理,保证图片能看全
            if (drawableWidth >= viewWidth && drawableHeight >= viewHeight) {
                // 图片宽度和高度都大于控件(缩小)
                mBaseScale = Math.min(viewWidth * 1.0f / drawableWidth, viewHeight * 1.0f / drawableHeight);
            } else if (drawableWidth > viewWidth && drawableHeight < viewHeight) {
                // 图片宽度大于控件,高度小于控件(缩小)
                mBaseScale = viewWidth * 1.0f / drawableWidth;
            } else if (drawableWidth < viewWidth && drawableHeight > viewHeight) {
                // 图片宽度小于控件,高度大于控件(缩小)
                mBaseScale = viewHeight * 1.0f / drawableHeight;
            } else {
                // 图片宽度小于控件,高度小于控件(放大)
                mBaseScale = Math.min(viewWidth * 1.0f / drawableWidth, viewHeight * 1.0f / drawableHeight);
            }
            // 将图片移动到手机屏幕的中间位置
            float distanceX = viewWidth / 2 - drawableWidth / 2;
            float distanceY = viewHeight / 2 - drawableHeight / 2;
            mMatrix.postTranslate(distanceX, distanceY);
            mMatrix.postScale(mBaseScale, mBaseScale, viewWidth / 2, viewHeight / 2);
            setImageMatrix(mMatrix);
        }
    }

    private float getScale() {
        float[] values = new float[9];
        mMatrix.getValues(values);
        return values[Matrix.MSCALE_X];
    }

    /**
     * 获得图片放大缩小以后的宽和高
     */
    private RectF getMatrixRectF() {
        RectF rectF = new RectF();
        Drawable drawable = getDrawable();
        if (drawable != null) {
            rectF.set(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
            mMatrix.mapRect(rectF);
        }
        return rectF;
    }

    /**
     * 图片在缩放时进行边界控制
     */
    private void borderAndCenterCheck() {
        RectF rect = getMatrixRectF();
        float deltaX = 0;
        float deltaY = 0;
        int viewWidth = getWidth();
        int viewHeight = getHeight();
        // 缩放时进行边界检测,防止出现白边
        if (rect.width() >= viewWidth) {
            if (rect.left > 0) {
                deltaX = -rect.left;
            }
            if (rect.right < viewWidth) {
                deltaX = viewWidth - rect.right;
            }
        }
        if (rect.height() >= viewHeight) {
            if (rect.top > 0) {
                deltaY = -rect.top;
            }
            if (rect.bottom < viewHeight) {
                deltaY = viewHeight - rect.bottom;
            }
        }
        // 如果宽度或者高度小于控件的宽或者高;则让其居中
        if (rect.width() < viewWidth) {
            deltaX = viewWidth / 2f - rect.right + rect.width() / 2f;

        }
        if (rect.height() < viewHeight) {
            deltaY = viewHeight / 2f - rect.bottom + rect.height() / 2f;
        }
        mMatrix.postTranslate(deltaX, deltaY);
    }
}

2.2、ScaleGestureDetector源码分析

       ScaleGestureDetector的源码比GestureDetector的源码就要稍微复杂点了,因为ScaleGestureDetector的事件涉及到多个手指。

       想要缩放的值,所有的MotionEvent事件都要交给ScaleGestureDetector的onTouchEvent()函数,所以我们就先直接来看下onTouchEvent()函数大概的逻辑。

    public boolean onTouchEvent(MotionEvent event) {
        ...

        // 缩放的手势是需要多个手指来完成的,count 手指的个数
        final int count = event.getPointerCount();
        ...
        // streamComplete表示当前事件留是否完成
        final boolean streamComplete = action == MotionEvent.ACTION_UP ||
                                       action == MotionEvent.ACTION_CANCEL || anchoredScaleCancelled;

        if (action == MotionEvent.ACTION_DOWN || streamComplete) {
            // mInProgress表示是否进行缩放,这里是停掉上一次的缩放调用onScaleEnd()
            if (mInProgress) {
                mListener.onScaleEnd(this);
                mInProgress = false;
                mInitialSpan = 0;
                mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
            } else if (inAnchoredScaleMode() && streamComplete) {
                mInProgress = false;
                mInitialSpan = 0;
                mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
            }

            if (streamComplete) {
                return true;
            }
        }
        ...
        if (inAnchoredScaleMode()) {
            ...
        } else {
            // 所有手指的距离相加
            for (int i = 0; i < count; i++) {
                if (skipIndex == i) continue;
                sumX += event.getX(i);
                sumY += event.getY(i);
            }
            // 所有手指的中心点
            focusX = sumX / div;
            focusY = sumY / div;
        }

        // Determine average deviation from focal point
        float devSumX = 0, devSumY = 0;
        for (int i = 0; i < count; i++) {
            if (skipIndex == i) continue;

            // 所有手指相对于中心点(所有手指的中心点)的距离之和
            devSumX += Math.abs(event.getX(i) - focusX);
            devSumY += Math.abs(event.getY(i) - focusY);
        }
        // 所有手指相对于中心点的平均值
        final float devX = devSumX / div;
        final float devY = devSumY / div;

        // *2 相当于是两个手指之间的距离跨度
        final float spanX = devX * 2;
        final float spanY = devY * 2;
        final float span;
        if (inAnchoredScaleMode()) {
            span = spanY;
        } else {
            span = (float) Math.hypot(spanX, spanY);
        }

        ...
        final int minSpan = inAnchoredScaleMode() ? mSpanSlop : mMinSpan;
        // 回调onScaleBegin(),返回值表示是否开始缩放
        if (!mInProgress && span >=  minSpan &&
            (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
            mPrevSpanX = mCurrSpanX = spanX;
            mPrevSpanY = mCurrSpanY = spanY;
            mPrevSpan = mCurrSpan = span;
            mPrevTime = mCurrTime;
            mInProgress = mListener.onScaleBegin(this);
        }

        // 回调onScale(),如果onScale()返回true,则重新保存mPrevSpanX,mPrevSpanY,mPrevSpan,mPrevTime
        if (action == MotionEvent.ACTION_MOVE) {
            mCurrSpanX = spanX;
            mCurrSpanY = spanY;
            mCurrSpan = span;

            boolean updatePrev = true;

            if (mInProgress) {
                updatePrev = mListener.onScale(this);
            }

            if (updatePrev) {
                mPrevSpanX = mCurrSpanX;
                mPrevSpanY = mCurrSpanY;
                mPrevSpan = mCurrSpan;
                mPrevTime = mCurrTime;
            }
        }

        return true;
    }

我onTouchEvent()里面的一些关键的地方,直接注释在代码里面了。

       onTouchEvent()函数里面,我们要注意onScale()的返回值为true的时候mPrevSpanX,mPrevSpanY,mPrevSpan,mPrevTime这些才会改变。

       我再看下缩放过程中的缩放因子是怎么计算到的。getScaleFactor()函数。

    public float getScaleFactor() {
        ...
        return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
    }

       简单吧,用当前两个手指之间的跨度除以上一次记录的两个手指之间的跨度。同时我们也注意到上面讲到的onTouchEvent()函数里面onScale()返回true的时候mPrevSpan才会重新赋值。什么意思,比如我们两个手指放在屏幕上,手指慢慢的拉开。假设回调过程中我们onScale()函数每次返回的是true,每次onScale()之后getScaleFactor()会重新去计算缩放因子,但是如果onScale()函数返回的是false,getScaleFactor()的返回值是一直增大的。


       本文中对应的实例下载地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容