縮放手勢對于大部分 Android 工程師來說,需要用到的機會比較少,它最常見于以下的一些應用場景中,例如:圖片瀏覽,圖片編輯(貼圖效果)、網頁縮放、地圖、文本閱讀(通過縮放手勢調整文字大小)等。應用場景相對比較狹窄,不過肯定也會有一些用武之地,它可以實現如下的效果:
2.縮放手勢檢測(ScaleGestureDetector)
縮放手勢檢測同樣是官方提供的手勢檢測工具,它的使用方式的 GentureDetector 類似,也是通過 Listener 進行監聽用戶的操作手勢,它是對縮放手勢進行了一次封裝, 可以方便用戶快速的完成縮放相關功能的開發。縮放手勢相對比較簡單,網絡上也能查到不少非官方實現的縮放手勢計算方案,但部分非官方的方案確實有所局限,例如只支持兩個手指的計算,在出現超過兩個手指時,只計算了前兩個手指的移動,這樣顯然是不合理的。而官方的這種實現方案輕松的應對了多個手指的情況,下面我們就來看看它是如何實現的吧。
2.1 構造方法
它有兩個構造方法,和 GestureDetector 類似,如下所示:
ScaleGestureDetector(Contextcontext,ScaleGestureDetector.OnScaleGestureListenerlistener) ScaleGestureDetector(Contextcontext,ScaleGestureDetector.OnScaleGestureListenerlistener,Handlerhandler)
2.2 手勢監聽器
它只有兩個監聽器,但嚴格來說,這兩個監聽器是同一個,只不過一個是接口,另一個是空實現而已。
監聽器 簡介
OnScaleGestureListener 縮放手勢檢測器
SimpleOnScaleGestureListene r縮放手勢檢測器的空實現。
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. 基本原理
由于縮放手勢檢測使用起來非常簡單,沒有什么復雜的內容,不僅如此,它的實現也非常簡單,下面我就帶大家簡單分析一下它的基本原理。在縮放手勢中我們其實主要關心的只有兩個參數而已,一個是縮放的中心點,另一個就是縮放比例了。 下面我們就看看這兩個參數是如何計算出來的.
3.1 計算縮放的中心點(焦點)
如果只有兩個手指的話,縮放的中心點自然是非常容易計算的,那就是兩個手指坐標的中點,但是如果有多個手指該如何計算縮放的中心點呢?
計算中心點的原理其實也非常簡單,那就是將所有的坐標都加起來,然后除以數量即可。
這是一個簡單的數學原理,并不復雜,如果有不理解的,自己嘗試計算一下也就能明白了。不過在實際運用中還是需要注意一下的, 用戶的手指數量可能并不是固定的,用戶可能隨時抬起來或者按下手指,ScaleGestureDetector 中是這樣實現的:
finalbooleananchoredScaleCancelled=
mAnchoredScaleMode == ANCHORED_SCALE_MODE_STYLUS && !isStylusButtonDown;
final boolean streamComplete = action == MotionEvent.ACTION_UP ||
finalbooleanstreamComplete=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;
}elseif(inAnchoredScaleMode()&&streamComplete){
mInProgress=false;
mInitialSpan=0;
mAnchoredScaleMode=ANCHORED_SCALE_MODE_NONE;
}
if(streamComplete){
returntrue;
}
}
可以看到,當觸發 down 或者觸發 up,cancel 時,如果之前處于縮放計算的狀態,會將其狀態重置, 并調用 onScaleEnd 方法。
計算中心點:
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 計算縮放比例
計算縮放比例也很簡單,就是計算各個手指到焦點的平均距離,在用戶手指移動后用新的平均距離除以舊的平均距離,并以此計算得出縮放比例。
// 計算到焦點的平均距離
floatdevSumX=0,devSumY=0;
for(inti=0;i<count;i++){
if (skipIndex == i) continue;
devSumX+=Math.abs(event.getX(i)-focusX);
devSumY+=Math.abs(event.getY(i)-focusY);
}
finalfloatdevX=devSumX/div;
finalfloatdevY=devSumY/div;
// 注意這里
finalfloatspanX=devX*2;
finalfloatspanY=devY*2;
finalfloatspan;
if(inAnchoredScaleMode()){
span=spanY;
}else{
// 相當于 sqrt(x*x + y*y)
span=(float)Math.hypot(spanX,spanY);
}
當用戶移動的距離超過一定數值(數值大小由系統定義)后,會觸發 onScaleBegin 方法,如果用戶在 onScaleBegin 方法里面返回了 true,表示接受事件后,就會重置縮放相關數值,并且開始積累縮放因子。
// mSpanSlop 和 mMinSpan 都是從系統里面取得的預定義數值,該數值實際上影響的是縮放的靈敏度。
// 不過該參數并沒有提供設置的方法,如果對靈敏度不滿意的話,和通過直接之際復制一個 ScaleGestureDetector 到項目中, 并且修改其中的數值。
finalintminSpan=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;
booleanupdatePrev=true;
if(mInProgress){
// 注意這里,用戶的返回值決定了是否重新計算縮放因子
updatePrev=mListener.onScale(this);
}
// 如果用戶返回了 true ,就會重新計算縮放因子
if(updatePrev){
mPrevSpanX=mCurrSpanX;
mPrevSpanY=mCurrSpanY;
mPrevSpan=mCurrSpan;
mPrevTime=mCurrTime;
}
}
由于縮放手勢檢測確實比較簡單,也大概就這么多了,感興趣的話,可以私信我






