縮放手勢 ScaleGestureDetector 源碼解析,這一篇就夠了

其實在咱們平常的編程中,對於縮放手勢的使用並非很常常,這一手勢主要是用在圖片瀏覽方面,好比下方例子。可是(敲重點),做爲 Android 入門的基礎來講,學習 ScaleGestureDetector 的使用,算是不得不過的一道坎,好在 ScaleGestureDetector 使用起來很是簡單,就是源碼分析上得花些功夫。java

本文首先將簡單的介紹下 ScaleGestureDetector 的使用,在重點給你們分析下源碼(因爲源碼方面是我本身的理解,可能有誤差,但願各位大佬能在評論區指出,萬分感謝~)編程


ScaleGestureDetector 使用

ScaleGestureDetector 包括一個監聽器,以及它全部方法的空實現:app

名稱 用途
ScaleGestureDetector 縮放手勢的監聽器
SimpleOnScaleGestureListener 該監聽器的空實現,在其中重寫方法

ScaleGestureDetector 方法

名稱 用途
onScaleBegin 當 >= 2 個手指碰觸屏幕時調用,若返回 false 則忽略改事件調用
onScale 滑動(縮放)過程當中調用,若成功處理,則用戶返回 true,監聽器繼續記錄下一個縮放等動做,若爲 false 代表數據未處理,則監聽器繼續積累
onScaleEnd 所有手指離開屏幕,結束監聽

一般狀況下,手勢監聽會結合自定義 View 來說,這裏我給出一個最簡單的使用,具體的使用實例,之後再結合自定義 View 講講。ide

private void iniScaleGestureListener(){
        mListener = new ScaleGestureDetector.SimpleOnScaleGestureListener(){
            @Override
            public boolean onScaleBegin(ScaleGestureDetector detector) {
                return super.onScaleBegin(detector);
            }

            @Override
            public boolean onScale(ScaleGestureDetector detector) {
                MyLog.d("X:" + detector.getFocusX());
                MyLog.d("Y:" + detector.getFocusY());
                MyLog.d("scale:" + detector.getScaleFactor());
                return super.onScale(detector);
            }

            @Override
            public void onScaleEnd(ScaleGestureDetector detector) {
                super.onScaleEnd(detector);
            }
        };

        detector = new ScaleGestureDetector(getContext(), mListener);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        detector.onTouchEvent(event);
        return true;
    }
複製代碼

ScaleGestureDetector 的使用

ScaleGestureDetector 在具體項目的使用有點複雜,我打算過段時間結合自定義 View 寫一篇用來總結,因此這篇咱們就先了解下 ScaleGestureDetector 的基本使用。源碼分析


ScaleGestureDetector 源碼分析

好了,如今咱們進入本章重點,ScaleGestureDetector 源碼分析,敲黑板敲黑板。首先,咱們打開 ScaleGestureDetector 的源碼能夠看到,幾乎全部的代碼都集中在了 onTouchEvent 這個方法上,因此在這裏,我就主要給你們介紹這個方法的實現。學習

第一部分:前期準備

if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        mCurrTime = event.getEventTime();

        final int action = event.getActionMasked();

        // Forward the event to check for double tap gesture
        if (mQuickScaleEnabled) {
            mGestureDetector.onTouchEvent(event);
        }

        final int count = event.getPointerCount();
        final boolean isStylusButtonDown =
                (event.getButtonState() & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0;
複製代碼

mInputEventConsistencyVerifier

  • 輸入事件一致性驗證器 @有道
  • 根據名字以及前面的定義
  • 咱們能夠猜想這個對象應該是手勢監聽 Event 是否註冊(鏈接到硬件)
  • 因此,若是他爲空,那麼咱們在這裏調用 onTouchEvent 進行註冊
if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }
複製代碼

mCurrTime

  • 得到事件發生時的時間
mCurrTime = event.getEventTime();
複製代碼

action

  • 得到事件類型
final int action = event.getActionMasked();
複製代碼

mQuickScaleEnabled

  • Forward the event to check for double tap gesture
  • @有道 轉發事件以檢查雙擊手勢
  • 首先是 mQuickScaleEnabled 這個對象
  • 翻譯過來是: @有道 啓用快速擴展
  • 做用大概就是調用雙擊監聽事件,好比雙擊最大化
if (mQuickScaleEnabled) {
            mGestureDetector.onTouchEvent(event);
        }
複製代碼

count

  • 得到屏幕上手指的數目
final int count = event.getPointerCount();
複製代碼

isStylusButtonDown

這個主要是因爲判斷手寫筆是否按下 因爲咱們不多處理手寫筆,因此這裏不作過多說明ui

final boolean isStylusButtonDown =
               (event.getButtonState() & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0;
複製代碼

第二部分:處理與手勢變化

用戶的縮放手勢不老是必定的,就是說對於用戶而言,隨時可能有手指碰觸或離開屏幕,這就使得縮放中心的(焦點)隨時可能發生變化,這部分主要是用來處理這一變化,並作出響應。this

final boolean anchoredScaleCancelled =
               mAnchoredScaleMode == ANCHORED_SCALE_MODE_STYLUS && !isStylusButtonDown;
       
       final boolean streamComplete = action == MotionEvent.ACTION_UP ||
               action == MotionEvent.ACTION_CANCEL || anchoredScaleCancelled;

       // 若是發生了上面這種小動做,或者說有一手指離開了屏幕,進行調用
       if (action == MotionEvent.ACTION_DOWN || streamComplete) {
           // Reset any scale in progress with the listener.
           // If it's an ACTION_DOWN we're beginning a new event stream.
           // This means the app probably didn't give us all the events. Shame on it.

           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;
           }
       }
複製代碼

anchoredScaleCancelled

  • @Google 錨定規模取消
  • 個人理解是:用於判斷滑動事件是否被取消
final boolean anchoredScaleCancelled =
                mAnchoredScaleMode == ANCHORED_SCALE_MODE_STYLUS && !isStylusButtonDown;
複製代碼

streamComplete

  • @Google Translate: 流完成
  • 個人理解是,這個布爾變量用於標記
  • 當前動做是否完成
  • 我這裏說的動做有兩種
  • 這裏指的是:在大動做如三指觸屏放大過程當中,又一個手指離開了屏幕這種
  • 在大動做三指觸屏中發生的一個小動做,離開一指
final boolean streamComplete = action == MotionEvent.ACTION_UP ||
                action == MotionEvent.ACTION_CANCEL || anchoredScaleCancelled;
複製代碼

action == MotionEvent.ACTION_DOWN || streamComplete

  • 若是發生了上面這種小動做,或者說有一手指離開了屏幕,就進行調用
if (action == MotionEvent.ACTION_DOWN || streamComplete) {...}
複製代碼

if (mInProgress)

  • @google Translate:重置偵聽器正在進行的任何縮放。
  • 若是是ACTION_DOWN,咱們開始一個新的事件流。
  • 這意味着應用程序可能沒有給咱們全部的事件。很遺憾。
  • 首先判斷該進程(從第一個手指碰上屏幕,到最後一個手指離開屏幕爲止)是否結束
  • 若是仍在運行中,這調用回調方法:onScaleEnd 使其結束
if (mInProgress) {
                mListener.onScaleEnd(this);
                mInProgress = false;
                mInitialSpan = 0;
                mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
            }
複製代碼

else if (inAnchoredScaleMode() && streamComplete)

  • 若是當前進程已經結束
  • 判斷 mAnchoredScaleMode 是否爲 ANCHORED_SCALE_MODE_STYLUS 狀態
  • 同時判斷操做流 streamComplete 是否完成
  • 都符合的狀況下結束這一手勢變化
else if (inAnchoredScaleMode() && streamComplete) {
                mInProgress = false;
                mInitialSpan = 0;
                mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
            }
複製代碼

if (streamComplete)

  • 結束本次 onTouchEvent 方法的調用,等待下一次調用發生
if (streamComplete) {
                return true;
            }
複製代碼

總結: 能夠看到,當觸發 down 或者觸發 up,cancel 時,若是以前處於縮放計算的狀態,會將其狀態重置, 並調用 onScaleEnd 方法。google


進入錨定比例模式

  • 當判斷用戶動做,若是爲雙擊這類點擊事件,進入該模式
  • 與正常縮放區分。這個模式功能通常是:雙擊最大化和最小化
if (!mInProgress && mStylusScaleEnabled && !inAnchoredScaleMode()
                && !streamComplete && isStylusButtonDown) {
            // Start of a button scale gesture
            mAnchoredScaleStartX = event.getX();
            mAnchoredScaleStartY = event.getY();
            mAnchoredScaleMode = ANCHORED_SCALE_MODE_STYLUS;
            mInitialSpan = 0;
        }
複製代碼

mAnchoredScaleStartX & mAnchoredScaleStartY

  • 後文中將用於從新計算焦點
mAnchoredScaleStartX = event.getX();
            mAnchoredScaleStartY = event.getY();
複製代碼

mAnchoredScaleMode

  • 賦值以後,再次調用 inAnchoredScaleMode() 方法,返回值變爲 true
mAnchoredScaleMode = ANCHORED_SCALE_MODE_STYLUS;
複製代碼

計算縮放中心

final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
                action == MotionEvent.ACTION_POINTER_UP ||
                action == MotionEvent.ACTION_POINTER_DOWN || anchoredScaleCancelled;

        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
        final int skipIndex = pointerUp ? event.getActionIndex() : -1;

        // Determine focal point
        float sumX = 0, sumY = 0;
        final int div = pointerUp ? count - 1 : count;
        final float focusX;
        final float focusY;
        if (inAnchoredScaleMode()) {
            // In anchored scale mode, the focal pt is always where the double tap
            // or button down gesture started
            focusX = mAnchoredScaleStartX;
            focusY = mAnchoredScaleStartY;
            if (event.getY() < focusY) {
                mEventBeforeOrAboveStartingGestureEvent = true;
            } else {
                mEventBeforeOrAboveStartingGestureEvent = false;
            }
        } 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;
        }
複製代碼

configChanged

  • 布爾類型量,標誌着一個操做的完成或者結束(手指離開,手指按下)
final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
                action == MotionEvent.ACTION_POINTER_UP ||
                action == MotionEvent.ACTION_POINTER_DOWN || anchoredScaleCancelled;
複製代碼

pointerUp

  • 布爾類型量,用於判斷當前動做,是否爲手指離開(擡起動做)
final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
複製代碼

skipIndex

  • 標記量,在是手指離開的狀況下,標記離開手指
  • 在後面計算新的焦點代碼中,跳過該手指的標記點座標,進行計算
final int skipIndex = pointerUp ? event.getActionIndex() : -1;
複製代碼

初始化計算所需臨時變量

// Determine focal point
        float sumX = 0, sumY = 0;
        // 若是是擡起手指,則當前手指數減1,不然不變
        final int div = pointerUp ? count - 1 : count;
        final float focusX;
        final float focusY;
複製代碼

判斷是否爲錨定比例模式

  • 是的話直接將點擊時記下的點,做爲焦點
  • 不是的話,把全部點累加求和,除以總個數,計算平均值
if (inAnchoredScaleMode()) {
            // In anchored scale mode, the focal pt is always where the double tap
            // or button down gesture started
            // 在錨定比例模式中,焦點pt始終是雙擊的位置,或按下手勢開始
            focusX = mAnchoredScaleStartX;
            focusY = mAnchoredScaleStartY;
            if (event.getY() < focusY) {
                mEventBeforeOrAboveStartingGestureEvent = true;
            } else {
                mEventBeforeOrAboveStartingGestureEvent = false;
            }
        } 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 @Google translate 
        float devSumX = 0, devSumY = 0;
        for (int i = 0; i < count; i++) {
            if (skipIndex == i) continue;

            // Convert the resulting diameter into a radius.
            devSumX += Math.abs(event.getX(i) - focusX);
            devSumY += Math.abs(event.getY(i) - focusY);
        }
        final float devX = devSumX / div;
        final float devY = devSumY / div;

        // Span is the average distance between touch points through the focal point;
        // i.e. the diameter of the circle with a radius of the average deviation from
        // the focal point.
        final float spanX = devX * 2;
        final float spanY = devY * 2;
        final float span;
        if (inAnchoredScaleMode()) {
            span = spanY;
        } else {
            span = (float) Math.hypot(spanX, spanY);
        }
複製代碼

計算平均誤差

  • 肯定焦點的平均誤差
float devSumX = 0, devSumY = 0;
        for (int i = 0; i < count; i++) {
            if (skipIndex == i) continue;

            // Convert the resulting diameter into a radius.
            devSumX += Math.abs(event.getX(i) - focusX);
            devSumY += Math.abs(event.getY(i) - focusY);
        }
        final float devX = devSumX / div;
        final float devY = devSumY / div;
複製代碼

計算縮放比例

  • 跨度是經過焦點的觸摸點之間的平均距離;
  • 即圓的直徑,其半徑爲平均誤差
  • 這裏的 Math.hypot(spanX, spanY) 方法,至關於 sqrt(xx + yy)
final float spanX = devX * 2;
        final float spanY = devY * 2;
        final float span;
        if (inAnchoredScaleMode()) {
            span = spanY;
        } else {
            span = (float) Math.hypot(spanX, spanY);
        }
複製代碼

結束縮放事件

  • @Google Translate:根據須要調度開始/結束事件。
  • 若是配置發生更改,請經過開始通知應用重置其當前狀態
  • 一個新的比例事件流。
  • 這裏就不作太多描述,主要就是:
  • 判斷是否是全部手指都離開了屏幕
  • 若是是,那麼索命這個縮放進程結束了
  • 則保存當前縮放的數據
  • 調用 onScaleEnd 方法,結束當前操做
final boolean wasInProgress = mInProgress;
        mFocusX = focusX;
        mFocusY = focusY;
        if (!inAnchoredScaleMode() && mInProgress && (span < mMinSpan || configChanged)) {
            mListener.onScaleEnd(this);
            mInProgress = false;
            mInitialSpan = span;
        }
        if (configChanged) {
            mPrevSpanX = mCurrSpanX = spanX;
            mPrevSpanY = mCurrSpanY = spanY;
            mInitialSpan = mPrevSpan = mCurrSpan = span;
        }
複製代碼

觸發 onScaleBegin 開始縮放

  • 當手指移動的距離超過必定數值(數值大小由系統定義)後,會觸發 onScaleBegin 方法
  • 若是用戶在 onScaleBegin 方法裏面返回了 true,則接受事件後,就會重置縮放相關數值,而且開始積累縮放因子。
final int minSpan = inAnchoredScaleMode() ? mSpanSlop : mMinSpan;
        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);
        }
複製代碼

通知用戶進行縮放處理

  • @ Google Translate: 處理動做;焦點和跨度/比例因子正在發生變化。
  • 這塊代碼的功能主要就是通知用戶(編程者)
  • 根據這些數據進行縮放
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;
            }
        }
複製代碼

updatePrev

  • 這個用於接收用戶的返回值
  • 只要咱們放回 true ,系統就會保存當前數據
  • 從新獲取並計算新的數據和比例
  • 系統默認返回 false 而後進行下一次事件的計算
if (mInProgress) {
                updatePrev = mListener.onScale(this);
            }

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

結語

我要講的全部內容,到這裏就徹底結束了spa

因爲源碼是按照我本身的理解來說的,因此不免會有一些出入

但願你們能在評論區中幫我指出,謝謝~ 🙏

相關文章
相關標籤/搜索