Android 手把手進階自定義View(十二)- 縮放手勢檢測 ScaleGestureDetector

一、前言


Android 縮放手勢檢測 ScaleGestureDetector,在大多數的情況下縮放手勢都不是單獨存在的,需要配合其它的手勢來使用,如果是用在自定義的控件上,則需要配合 Matrix 相關內容使用起來可能會更加方便。縮放手勢最常見於以下的一些應用場景中,例如:圖片瀏覽,網頁縮放、地圖等。

 

二、縮放手勢檢測


縮放手勢檢測同樣是官方提供的手勢檢測工具,它的使用方式的 GentureDetector 類似,也是通過 Listener 進行監聽用戶的操作手勢,它是對縮放手勢進行了一次封裝, 可以方便用戶快速的完成縮放相關功能的開發。縮放手勢相對比較簡單,網絡上也能查到不少非官方實現的縮放手勢計算方案,但部分非官方的方案確實有所侷限,例如只支持兩個手指的計算,在出現超過兩個手指時,只計算了前兩個手指的移動,這樣顯然是不合理的。而官方的這種實現方案輕鬆的應對了多個手指的情況,下面我們就來看看它是如何實現的吧。

2.1 構造方法

它有兩個構造方法,和 GestureDetector 類似,如下所示:

ScaleGestureDetector(Context context, ScaleGestureDetector.OnScaleGestureListener listener)

ScaleGestureDetector(Context context, ScaleGestureDetector.OnScaleGestureListener listener, Handler handler)

2.2 手勢監聽器

它只有兩個監聽器,但嚴格來說,這兩個監聽器是同一個,只不過一個是接口,另一個是空實現而已。

監聽器 簡介
OnScaleGestureListener 縮放手勢檢測器
SimpleOnScaleGestureListener 縮放手勢檢測器的空實現

OnScaleGestureListener 縮放手勢監聽器有 3 個方法:

//縮放手勢開始,當兩個手指放在屏幕上的時候會調用該方法(只調用一次)。如果返回 false 則表示不使用當前這次縮放手勢。
boolean onScaleBegin(ScaleGestureDetector detector)	

//縮放被觸發(會調用0次或者多次),如果返回 true 則表示當前縮放事件已經被處理,檢測器會重新積累縮放因子,
//返回 false 則會繼續積累縮放因子。
boolean onScale(ScaleGestureDetector detector)	

//縮放手勢結束。
void onScaleEnd(ScaleGestureDetector detector)	

2.3 簡單示例

這是使用 ScaleGestureDetector 的一個極簡用例,當然,它沒有實現任何功能,只是用日誌的方式輸出了幾個我們比較關心的參數而已。

public class ScaleGestureDemoView extends View {
    private static final String TAG = "ScaleGestureDemoView";

    private ScaleGestureDetector mScaleGestureDetector;

    public ScaleGestureDemoView(Context context) {
        super(context);
    }

    public ScaleGestureDemoView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initScaleGestureDetector();
    }

    private void initScaleGestureDetector() {
        mScaleGestureDetector = new ScaleGestureDetector(getContext(), 
                new ScaleGestureDetector.SimpleOnScaleGestureListener() {
            @Override
            public boolean onScaleBegin(ScaleGestureDetector detector) {
                return true;
            }

            @Override
            public boolean onScale(ScaleGestureDetector detector) {
                Log.i(TAG, "focusX = " + detector.getFocusX());       // 縮放中心,x座標
                Log.i(TAG, "focusY = " + detector.getFocusY());       // 縮放中心y座標
                Log.i(TAG, "scale = " + detector.getScaleFactor());   // 縮放因子
                return true;
            }

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

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

 

三、基本原理

由於縮放手勢檢測使用起來非常簡單,沒有什麼複雜的內容,不僅如此,它的實現也非常簡單,下面簡單分析一下它的基本原理。在縮放手勢中我們其實主要關心的只有兩個參數而已,一個是縮放的中心點,另一個就是縮放比例了。下面我們就看看這兩個參數是如何計算出來的。

3.1 計算縮放的中心點(焦點)

如果只有兩個手指的話,縮放的中心點自然是非常容易計算的,那就是兩個手指座標的中點,但是如果有多個手指該如何計算縮放的中心點呢?計算中心點的原理其實也非常簡單,那就是將所有的座標都加起來,然後除以數量即可。

這是一個簡單的數學原理,並不複雜,如果有不理解的,自己嘗試計算一下也就能明白了。不過在實際運用中還是需要注意一下的, 用戶的手指數量可能並不是固定的,用戶可能隨時擡起來或者按下手指,ScaleGestureDetector 中是這樣實現的:

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) {
		//重置偵聽器正在進行的任何縮放。
        //如果是ACTION_DOWN,我們正在開始一個新的事件流。
        //這意味着應用程序可能沒有給我們所有的事件(事件被上層直接攔截了)。
        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;
        }
    }

可以看到,當觸發 down 或者觸發 up、cancel 時,如果之前處於縮放計算的狀態,會將其狀態重置, 並調用 onScaleEnd 方法。當然, 你可能注意到了 mAnchoredScaleMode 等內容,這些是對觸控筆等外設的支持,對於大部分工程師來說,用到的機會比較少, 不管即可。

計算中心點:

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;

// 確定焦點
float sumX = 0, sumY = 0;
final int div = pointerUp ? count - 1 : count;
final float focusX;
final float focusY;
if (inAnchoredScaleMode()) {
    // 在錨定比例模式下,焦點始終是雙擊或按鈕按下時手勢開始的位置
    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;
}

3.2 計算縮放比例

計算縮放比例也很簡單,就是計算各個手指到焦點的平均距離,在用戶手指移動後用新的平均距離除以舊的平均距離,並以此計算得出縮放比例。

// 計算到焦點的平均距離
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;

// 注意這裏
final float spanX = devX * 2;
final float spanY = devY * 2;
final float span;
if (inAnchoredScaleMode()) {
    span = spanY;
} else {
    // 相當於 sqrt(x*x + y*y)
    span = (float) Math.hypot(spanX, spanY);
}

當用戶移動的距離超過一定數值(數值大小由系統定義)後,會觸發 onScaleBegin 方法,如果用戶在 onScaleBegin 方法裏面返回了 true,表示接受事件後,就會重置縮放相關數值,並且開始積累縮放因子。

//mSpanSlop 和 mMinSpan 都是從系統裏面取得的預定義數值,該數值實際上影響的是縮放的靈敏度。
//不過該參數並沒有提供設置的方法,如果對靈敏度不滿意的話,和通過直接之際複製一個ScaleGestureDetector到項目中,並且修改其中的數值。
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);
}

通知用戶縮放:

if (action == MotionEvent.ACTION_MOVE) {
    mCurrSpanX = spanX;
    mCurrSpanY = spanY;
    mCurrSpan = span;

    boolean updatePrev = true;

    if (mInProgress) {
        // 注意這裏,用戶的返回值決定了是否重新計算縮放因子
        updatePrev = mListener.onScale(this);
    }

    // 如果用戶返回了 true ,就會重新計算縮放因子
    if (updatePrev) {
        mPrevSpanX = mCurrSpanX;
        mPrevSpanY = mCurrSpanY;
        mPrevSpan = mCurrSpan;
        mPrevTime = mCurrTime;
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章