PhotoView 在做圖片縮放的組件或者有類似的需求功能時提供了極大的便利,自身功能也是十分強大。比如
- 支持手勢雙擊 多指觸摸 輕擊
- 完美解決了和一些scroll控件的沖突 比如ViewPager
- 有drag fling scale 這些操作的回調
- 兼容性十分好 基本覆蓋了全版本 (低版本部分功能不支持)
PhotoView這個library中最重要核心的部分就是手勢的操作,所以這篇文章主要分析整個PhotoView的手勢設計思想,學習其中的實現(xiàn)方式。相信看完后對于基本的手指縮放,雙擊,以及多指操作和事件處理有一個更好的理解。
手勢版本兼容和監(jiān)聽
先放一張整個library的結構圖
看上面的圖中能看到有一個類叫做 VersionedGestureDetector
,這就是整個手勢的入口,它實際上是一個代理類,里面就一個靜態(tài)方法 newInstance
,通過不同的版本拿到對應的GestureDetector,不過這個并不是系統(tǒng)內(nèi)部的手勢,這個是一個自己創(chuàng)建的抽象接口
public interface GestureDetector {
public boolean onTouchEvent(MotionEvent ev);
public boolean isScaling();
public boolean isDragging();
public void setOnGestureListener(OnGestureListener listener);
}
整個手勢監(jiān)聽的結構是一種高版本繼承低版本,必要時進行重寫的思想,類之間的繼承圖是這樣的
整個GestureDetector提供了
OnGestureListener
監(jiān)聽的注冊,這個監(jiān)聽從PhotoViewAttacher傳遞到VersionedGestureDetector,然后到上面繼承實現(xiàn)的每個類中。那么還有一個問題就是在什么地方把onTouchEvent這個方法從實現(xiàn)類中注入到PhotoViewAttacher中,我們直接搜一下這個touchEvent在哪調用的
public boolean onTouch(View v, MotionEvent ev) {
boolean handled = false;
if (mZoomEnabled && hasDrawable((ImageView) v)) {
...
// Try the Scale/Drag detector
if (null != mScaleDragDetector) {
boolean wasScaling = mScaleDragDetector.isScaling();
boolean wasDragging = mScaleDragDetector.isDragging();
//注入onTouchEvent
handled = mScaleDragDetector.onTouchEvent(ev);
boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling();
boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging();
mBlockParentIntercept = didntScale && didntDrag;
}
// Check to see if the user double tapped
if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) {
handled = true;
}
}
return handled;
}
從這段代碼可以看出在 onTouch
中如果兩個變量滿足,就會調用 onTouchEvent
把事件傳遞進去,而onTouch實際上是ImageView設置的setOnTouchListener的回調實現(xiàn)。
imageView.setOnTouchListener(this);
到這里,其實就很清晰了,imageView觸發(fā)了Touch事件并且將這個event傳遞給抽象的自定義GestureDetector處理。在這里,事件的處理會調用設置進來的OnGestureListener的對應方法,這也是PhotoView這個庫如何實現(xiàn)手勢拖動,滑動,縮放的重點。
public interface OnGestureListener {
public void onDrag(float dx, float dy);
public void onFling(float startX, float startY, float velocityX,
float velocityY);
public void onScale(float scaleFactor, float focusX, float focusY);
}
然后繼續(xù)往下看event是怎么處理的。先從最低版本的實現(xiàn)開始看,也就是CupcakeGestureDetector,主要就是看onTouchEvent的實現(xiàn)。
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
mVelocityTracker = VelocityTracker.obtain();
if (null != mVelocityTracker) {
mVelocityTracker.addMovement(ev);
} else {
LogManager.getLogger().i(LOG_TAG, "Velocity tracker is null");
}
mLastTouchX = getActiveX(ev);
mLastTouchY = getActiveY(ev);
mIsDragging = false;
break;
}
case MotionEvent.ACTION_MOVE: {
final float x = getActiveX(ev);
final float y = getActiveY(ev);
final float dx = x - mLastTouchX, dy = y - mLastTouchY;
if (!mIsDragging) {
// Use Pythagoras to see if drag length is larger than
// touch slop
mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;
}
if (mIsDragging) {
mListener.onDrag(dx, dy);
mLastTouchX = x;
mLastTouchY = y;
if (null != mVelocityTracker) {
mVelocityTracker.addMovement(ev);
}
}
break;
}
case MotionEvent.ACTION_CANCEL: {
// Recycle Velocity Tracker
if (null != mVelocityTracker) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
case MotionEvent.ACTION_UP: {
if (mIsDragging) {
if (null != mVelocityTracker) {
mLastTouchX = getActiveX(ev);
mLastTouchY = getActiveY(ev);
// Compute velocity within the last 1000ms
mVelocityTracker.addMovement(ev);
mVelocityTracker.computeCurrentVelocity(1000);
final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker
.getYVelocity();
// If the velocity is greater than minVelocity, call
// listener
if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) {
mListener.onFling(mLastTouchX, mLastTouchY, -vX,
-vY);
}
}
}
// Recycle Velocity Tracker
if (null != mVelocityTracker) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
}
return true;
}
代碼比較長,不過還是比較清晰簡單的,在ACTION_DOWN的事件中初始化了一個VelocityTracker,這個是系統(tǒng)用于監(jiān)聽速度的一個類,獲取對象的方法為obtain,看到這種形式就應該想到是設計模式中的享元模式,Message其實也是一樣,在DOWN事件同時初始化了一個mIsDragging的flag。
然后就是MOVE事件,如果mIsDragging為false,也就是當前沒有處于Drag狀態(tài),就判斷滑動的相對位移是否大于系統(tǒng)認定的一個滑動大小。如果是的話就回調設置進來的 onDrag(dx, dy)
方法。
最后就是UP事件,如果當前已經(jīng)處于Drag狀態(tài),在手指釋放的瞬間,通過前面所說的VelocityTracker的一個方法 computeCurrentVelocity
來計算速度,這里傳進去1000,也就是計算前面1000ms的平均速度,如果大于一個可以判定為Fling狀態(tài)的最小速度,那么就直接回調onFling(mLastTouchX, mLastTouchY, -vX,-vY)
,最后再將VelocityTracker回收掉。
所以這個類是一個基礎的手勢處理,主要是回調drag,fling兩個方法,而真正的scale方法并不是在這個類實現(xiàn)了,也說明了scale是存在版本限制的。
然后我們繼續(xù)看EclairGestureDetector,它繼承了上面的類,也就是說擁有了父類這些方法,而且看到這個類的API限制為5。同樣的我們直接看onTouchEvent方法。
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = ev.getPointerId(0);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mActivePointerId = INVALID_POINTER_ID;
break;
case MotionEvent.ACTION_POINTER_UP:
final int pointerIndex = Compat.getPointerIndex(ev.getAction());
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = ev.getPointerId(newPointerIndex);
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
}
break;
}
mActivePointerIndex = ev
.findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId
: 0);
return super.onTouchEvent(ev);
}
這個API為5的限制主要是因為加入了多指監(jiān)聽,如果想要詳細的了解多指監(jiān)聽,可以看官方文檔,這里簡短的描述一下,如果想要監(jiān)聽多指,首先在獲取action時,需要使用 action & MotionEvent.ACTION_MASK
,普通的action是拿不到ACTION_POINTER_UP的事件的,這個事件只有在手指UP并且屏幕上依然還有手指時才會回調,這里所做的工作就是將x,y的坐標切換到新的手指上,修正坐標計算的偏差,在多指操作上這個步驟十分重要。
最后看FroyoGestureDetector這個類,這個類的api限制是8,因為系統(tǒng)在8之后才加入了ScaleGestureDetector這個類。
@Override
public boolean onTouchEvent(MotionEvent ev) {
mDetector.onTouchEvent(ev);
return super.onTouchEvent(ev);
}
在這個onTouchEvent中只是加入了一個ScaleGestureDetector的對象來進行監(jiān)聽縮放。下面就會分析這個類是做什么的。
手勢縮放監(jiān)聽ScaleGestureDetector
先看一下這個類的系統(tǒng)注釋
/**
* Detects scaling transformation gestures using the supplied {@link MotionEvent}s.
* The {@link OnScaleGestureListener} callback will notify users when a particular
* gesture event has occurred.
*
* This class should only be used with {@link MotionEvent}s reported via touch.
*/
從注釋可以看出這個類主要就是用來檢測手勢變換,并且有一個callback來通知用戶scale發(fā)生。
ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor))
return false;
mListener.onScale(scaleFactor,
detector.getFocusX(), detector.getFocusY());
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
// NO-OP
}
};
mDetector = new ScaleGestureDetector(context, mScaleListener);
在前面所說的FroyoGestureDetector中,就是創(chuàng)建了這樣一個對象,并且實現(xiàn)了一個OnScaleGestureListener 的接口,用法也是十分的簡單,onScale
這個方法中可以獲取縮放倍數(shù),以及控制點,所以在這里將結果通過 mListener.onScale(scaleFactor,detector.getFocusX(), detector.getFocusY())
回調出去,這些參數(shù)用于后面通過Matrix來縮放ImageView。
我們繼續(xù)跟到系統(tǒng)ScaleGestureDetector里面看看是怎么判斷縮放的。
首先第一步就是確定縮放的焦點,簡單的雙擊和單擊焦點就不說了,主要是多指焦點的判斷。
在多指情況下分為幾種事件,其中POINT_UP的計算和非POINT_UP的事件焦點計算是不一樣的,來一段簡短的代碼
final int count = event.getPointerCount();
final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
final int skipIndex = pointerUp ? event.getActionIndex() : -1;
final int div = pointerUp ? count - 1 : count;
...
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;
可以看到是將所有手指的坐標加起來除上手指的個數(shù),并且其中考慮了POINT_UP,并且將抬起的手指坐標移出計算的范圍。
第二步就是通過焦點坐標和每個手指的坐標,計算一個偏差,源碼里稱之為span。
計算的代碼比較長,這里就不貼了,先分別計算每個點距離焦點的X軸距離和Y軸距離,求得一個平均數(shù),然后一個 Math.hypot
求得最終的span。這個span主要是后面用來計算縮放比例的一個值。
第三步就是根據(jù)前面的span來判斷是否滿足Scale的標準,如果滿足就先觸發(fā) 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);
}
這里有一個mInProgress 的返回值,是用來后面的onScale判定的,如果覆寫成false,那么后面的一系列回調都不會發(fā)生。
第四步就是在ACTION_MOVE的事件中產(chǎn)生縮放
if (action == MotionEvent.ACTION_MOVE) {
mCurrSpanX = spanX;
mCurrSpanY = spanY;
mCurrSpan = span;
boolean updatePrev = true;
if (mInProgress) {
updatePrev = mListener.onScale(this);
}
第五步也就是結束整個Scale,
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);
可以看到結束的場景很多,ACTION_DOWN ,ACTION_UP ,ACTION_CANCEL以及取消縮放設置都會導致 onScaleEnd
的回調。
到這里就分析完了整個ScaleGestureDetector是如何產(chǎn)生以及縮放比例的設置,以及焦點和結束所有的操作。
雙擊放大縮小以及單擊監(jiān)聽
相比于前面的內(nèi)容,這里就更簡單的, PhotoViewAttacher在構造函數(shù)中給了一個默認的單擊和雙擊的手勢實現(xiàn)
mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this));
可以看到所有的實現(xiàn)其實是在DefaultOnDoubleTapListener這個類中。
當然為了擴展性,PhotoView也同樣提供了一個方法,可以讓我們在外部設置這個實現(xiàn)。
@Override
public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) {
if (newOnDoubleTapListener != null) {
this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener);
} else {
this.mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this));
}
}
這里就直接看DefaultOnDoubleTapListener的實現(xiàn)了
這里比較難理解的一塊就是圖中的 onSingleTapConfirmed
方法,這個方法主要是判斷當前的點擊是否在ImageView上面,實際使用的話會發(fā)現(xiàn)如果點擊在ImageView的縮放之外是無法觸發(fā)單擊的那個事件的
final RectF displayRect = photoViewAttacher.getDisplayRect();
if (null != displayRect) {
final float x = e.getX(), y = e.getY();
// Check to see if the user tapped on the photo
if (displayRect.contains(x, y)) {
float xResult = (x - displayRect.left)
/ displayRect.width();
float yResult = (y - displayRect.top)
/ displayRect.height();
photoViewAttacher.getOnPhotoTapListener().onPhotoTap(imageView, xResult, yResult);
return true;
}
}
這里有一個方法getDisplayRect,主要是根據(jù)我們設置的ScaleType轉換后的matrix的坐標進行一個變換得到一個新的RectF ,這個就是Imageview實際縮放后的邊界,再與Event進行比較看是否單擊發(fā)生在邊界之內(nèi),如果在里面,就會觸發(fā) onPhotoTap
這個回調。
這個類另一個就是雙擊的實現(xiàn)
@Override
public boolean onDoubleTap(MotionEvent ev) {
if (photoViewAttacher == null)
return false;
try {
float scale = photoViewAttacher.getScale();
float x = ev.getX();
float y = ev.getY();
if (scale < photoViewAttacher.getMediumScale()) {
photoViewAttacher.setScale(photoViewAttacher.getMediumScale(), x, y, true);
} else if (scale >= photoViewAttacher.getMediumScale() && scale < photoViewAttacher.getMaximumScale()) {
photoViewAttacher.setScale(photoViewAttacher.getMaximumScale(), x, y, true);
} else {
photoViewAttacher.setScale(photoViewAttacher.getMinimumScale(), x, y, true);
}
} catch (ArrayIndexOutOfBoundsException e) {
// Can sometimes happen when getX() and getY() is called
}
return true;
}
可以看到雙擊放大和縮小都是在這里產(chǎn)生的,根據(jù)當前的縮放比來決定下一次縮放到哪一個等級。
后記
經(jīng)過以上分析就能了解整個PhotoView關于手勢的一切實現(xiàn),如何產(chǎn)生拖拽,如何在手指抬起后依然滑動,如何實現(xiàn)多指的縮放,以及單擊和雙擊的事件響應。當然這篇文章只是講解手勢部分。
至于產(chǎn)生事件后,PhotoView是如何通過Matrix來進行變換在這里并沒有講解,因為Matrix這個類能使用的場景遠比這個需求復雜,不僅是平移和縮放,更能做到錯切以及3維變換,這個部分將會在以后的文章中詳細講解。