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閱讀 230,431評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380

推薦閱讀更多精彩內容