轉載請注明本文出自maplejaw的博客(http://blog.csdn.net/maplejaw_)
開源庫地址:https://github.com/chrisbanes/PhotoView
PhotoView是1個用來幫助開發者輕松實現ImageView縮放的庫。開發者可以輕易控制對圖片的縮放旋等等操作。
PhotoView的使用極為簡單,而且提供了兩種方案??墒褂闷胀ǖ腎mageView,也能夠使用該庫中提供的ImageView(PhotoView)。
-
使用PhotoView
只需以下援用該庫中的ImageView,無需關心其它實現細節,你的ImageView即可具有縮放效果。
"@+id/iv_photo" android:layout_width="fill_parent" android:layout_height="fill_parent" />
-
針對普通ImageView
有的時候,可能由于1些歷史緣由,使得你不能不用原來的ImageView。榮幸的是該庫也提供了1種解決方案。只需用PhotoViewAttacher包裝便可。
PhotoViewAttacher mAttacher=new PhotoViewAttacher(mImageView); mAttacher.update(); mAttacher.cleanup();
PhotoView真的很奇異,接下來我們去源碼里1探究竟吧。順便多說1句,圖片的縮放大量應用到了Matrix相干知識,不了解的務必要先查閱相干資料哦。強烈推薦Android Matrix 這篇文章,固然也能夠看我的這篇Android Matrix矩陣詳解。
源碼解讀
這次源碼解讀我們從使用普通ImageView入手,普通的ImageView如果想縮放,必須依賴于PhotoViewAttacher,而PhotoViewAttacher又實現了IPhotoView接口。IPhotoView主要定義了1些經常使用的操作和默許值,由于方法實在太多了,就不逐一羅列了,直接上圖。
IPhotoView定義的所有抽象方法以下。
IPhotoView的部份源碼以下。
public interface IPhotoView { float DEFAULT_MAX_SCALE = 3.0f; float DEFAULT_MID_SCALE = 1.75f; float DEFAULT_MIN_SCALE = 1.0f; int DEFAULT_ZOOM_DURATION = 200; boolean canZoom(); RectF getDisplayRect(); boolean setDisplayMatrix(Matrix finalMatrix); Matrix getDisplayMatrix();
介紹完IPhotoView接口后,現在改來看看PhotoViewAttacher了,PhotoViewAttacher的屬性也比較多,以下:
private Interpolator mInterpolator = new AccelerateDecelerateInterpolator(); int ZOOM_DURATION = DEFAULT_ZOOM_DURATION; static final int EDGE_NONE = -1; static final int EDGE_LEFT = 0; static final int EDGE_RIGHT = 1; static final int EDGE_BOTH = 2; static int SINGLE_TOUCH = 1; private float mMinScale = DEFAULT_MIN_SCALE; private float mMidScale = DEFAULT_MID_SCALE; private float mMaxScale = DEFAULT_MAX_SCALE; private boolean mAllowParentInterceptOnEdge = true; private boolean mBlockParentIntercept = false; private WeakReferencemImageView; private GestureDetector mGestureDetector; private uk.co.senab.photoview.gestures.GestureDetector mScaleDragDetector; private final Matrix mBaseMatrix = new Matrix(); private final Matrix mDrawMatrix = new Matrix(); private final Matrix mSuppMatrix = new Matrix(); private final RectF mDisplayRect = new RectF(); private final float[] mMatrixValues = new float[9]; private OnMatrixChangedListener mMatrixChangeListener; private OnPhotoTapListener mPhotoTapListener; private OnViewTapListener mViewTapListener; private OnLongClickListener mLongClickListener; private OnScaleChangeListener mScaleChangeListener; private OnSingleFlingListener mSingleFlingListener; private int mIvTop, mIvRight, mIvBottom, mIvLeft; private FlingRunnable mCurrentFlingRunnable; private int mScrollEdge = EDGE_BOTH; private float mBaseRotation; private boolean mZoomEnabled; private ScaleType mScaleType = ScaleType.FIT_CENTER;
另外PhotoViewAttacher中還定義了以下幾個接口。
public interface OnMatrixChangedListener { /**
* 當用來顯示Drawable的Matrix改變時回調
* @param rect - 顯示Drawable的新邊界
*/ void onMatrixChanged(RectF rect);
} public interface OnScaleChangeListener { /**
* 當ImageView改變縮放時回調
*
* @param scaleFactor 小于1表示縮小,大于1表示放大
* @param focusX 縮放焦點X
* @param focusY 縮放焦點Y
*/ void onScaleChange(float scaleFactor, float focusX, float focusY);
} public interface OnPhotoTapListener { /**
*
*當用戶敲擊在照片上時回調,如果在空白區域不會回調
* @param view - ImageView
* @param x -用戶敲擊的位置(在圖片中從左往右的位置)占圖片寬度的百分比
* @param y -用戶敲擊的位置(在圖片中從上往下的位置)占圖片高度的百分比
*/ void onPhotoTap(View view, float x, float y); /**
* 在圖片外部的空白區域敲擊回調
* */ void onOutsidePhotoTap();
} public interface OnViewTapListener { /**
* 只要用戶敲擊ImageView就會回調,不論是不是在圖片上。
* @param view - View the user tapped.
* @param x -敲擊View的x坐標
* @param y -敲擊View的y坐標
*/ void onViewTap(View view, float x, float y);
} public interface OnSingleFlingListener { /**
* 用戶使用單指在ImageView上快速滑動時回調,不論是不是在圖片上。
* @param e1 - 第1次觸摸事件
* @param e2 - 第2次觸摸事件
* @param velocityX - 水平滑過的速度.
* @param velocityY - 豎直滑過的素組.
*/ boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}
在看完PhotoViewAttacher的1些屬性和接口外,現在就來看PhotoViewAttacher的構造方法。即new PhotoViewAttacher(mImageView)這1句。
public PhotoViewAttacher(ImageView imageView) { this(imageView, true);
} public PhotoViewAttacher(ImageView imageView, boolean zoomable) {
mImageView = new WeakReference<>(imageView); imageView.setDrawingCacheEnabled(true); imageView.setOnTouchListener(this); ViewTreeObserver observer = imageView.getViewTreeObserver(); if (null != observer)
observer.addOnGlobalLayoutListener(this); setImageViewScaleTypeMatrix(imageView); if (imageView.isInEditMode()) { return;
} mScaleDragDetector = VersionedGestureDetector.newInstance(
imageView.getContext(), this); mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() { @Override public void onLongPress(MotionEvent e) { if (null != mLongClickListener) {
mLongClickListener.onLongClick(getImageView());
}
} @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (mSingleFlingListener != null) { if (getScale() > DEFAULT_MIN_SCALE) { return false;
} if (MotionEventCompat.getPointerCount(e1) > SINGLE_TOUCH
|| MotionEventCompat.getPointerCount(e2) > SINGLE_TOUCH) { return false;
} return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY);
} return false;
}
}); mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this)); mBaseRotation = 0.0f; setZoomable(zoomable);
}
構造方法主要做了1些初始化工作,比如添加了手勢監聽(雙指縮放,拖拽,雙擊,長按)等等。而且,如果希望圖片具有縮放功能,還得設置ImageView的scaleType為matrix,下面我們就1步步剖析。
默許設置
為了理解起來更聯貫1點,我們先看setZoomable中的源碼。
@Override public void setZoomable(boolean zoomable) {
mZoomEnabled = zoomable;
update();
} public void update() {
ImageView imageView = getImageView(); if (null != imageView) { if (mZoomEnabled) { setImageViewScaleTypeMatrix(imageView); updateBaseMatrix(imageView.getDrawable());
} else { resetMatrix();
}
}
}
可以看出,除賦值mZoomEnabled外,還調用了update()方法,前面我們說了,每次更換圖片時需調用update()刷新。在update()中,如果是可縮放的,就更新mBaseMatrix,否則重置矩陣。
updateBaseMatrix的源碼以下:
private void updateBaseMatrix(Drawable d) {
ImageView imageView = getImageView(); if (null == imageView || null == d) { return;
} final float viewWidth = getImageViewWidth(imageView); final float viewHeight = getImageViewHeight(imageView); final int drawableWidth = d.getIntrinsicWidth(); final int drawableHeight = d.getIntrinsicHeight();
mBaseMatrix.reset(); final float widthScale = viewWidth / drawableWidth; final float heightScale = viewHeight / drawableHeight; if (mScaleType == ScaleType.CENTER) { mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F,
(viewHeight - drawableHeight) / 2F);
} else if (mScaleType == ScaleType.CENTER_CROP) { float scale = Math.max(widthScale, heightScale); mBaseMatrix.postScale(scale, scale); mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
(viewHeight - drawableHeight * scale) / 2F);
} else if (mScaleType == ScaleType.CENTER_INSIDE) { float scale = Math.min(1.0f, Math.min(widthScale, heightScale)); mBaseMatrix.postScale(scale, scale); mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
(viewHeight - drawableHeight * scale) / 2F); } else { RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight);
RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight); if ((int) mBaseRotation % 180 != 0) {
mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth);
} switch (mScaleType) { case FIT_CENTER:
mBaseMatrix
.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER); break; case FIT_START:
mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START); break; case FIT_END:
mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END); break; case FIT_XY:
mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL); break; default: break;
}
} resetMatrix();
}
可以看出updateBaseMatrix,主要是在根據ScaleType來調劑顯示位置和縮放級別,使其到達ImageView的ScaleType效果。為何需要這個功能?由于ImageView已被強迫設置ScaleType為Matrix,但是如果我們依然需要ScaleType的顯示效果怎樣辦?因而PhotoViewAttacher提供了setScaleType來摹擬相干效果。從上面的源碼應當不難看出,mBaseMatrix用來保存根據ScaleType調劑過的的原始矩陣。默許的ScaleType為ScaleType.FIT_CENTER。
接下來,我們來看resetMatrix()。
private void resetMatrix() {
mSuppMatrix.reset(); setRotationBy(mBaseRotation); setImageViewMatrix(getDrawMatrix()); checkMatrixBounds(); }
設置旋轉角度的源碼以下,mSuppMatrix后乘了旋轉角度。然落后行檢查邊界,最落后行顯示。
public void setRotationBy(float degrees) {
mSuppMatrix.postRotate(degrees % 360); checkAndDisplayMatrix(); } private void checkAndDisplayMatrix() { if (checkMatrixBounds()) { setImageViewMatrix(getDrawMatrix());
}
}
checkMatrixBounds()用來檢查Matrix邊界。相干源碼以下。
private boolean checkMatrixBounds() { final ImageView imageView = getImageView(); if (null == imageView) { return false;
} final RectF rect = getDisplayRect(getDrawMatrix()); if (null == rect) { return false;
} final float height = rect.height(), width = rect.width(); float deltaX = 0, deltaY = 0; final int viewHeight = getImageViewHeight(imageView); if (height <= viewHeight) { switch (mScaleType) { case FIT_START:
deltaY = -rect.top; break; case FIT_END:
deltaY = viewHeight - height - rect.top; break; default:
deltaY = (viewHeight - height) / 2 - rect.top; break;
}
} else if (rect.top > 0) { deltaY = -rect.top; } else if (rect.bottom < viewHeight) { deltaY = viewHeight - rect.bottom;
} final int viewWidth = getImageViewWidth(imageView); if (width <= viewWidth) { switch (mScaleType) { case FIT_START:
deltaX = -rect.left; break; case FIT_END:
deltaX = viewWidth - width - rect.left; break; default:
deltaX = (viewWidth - width) / 2 - rect.left; break;
}
mScrollEdge = EDGE_BOTH; } else if (rect.left > 0) {
mScrollEdge = EDGE_LEFT; deltaX = -rect.left;
} else if (rect.right < viewWidth) { deltaX = viewWidth - rect.right; mScrollEdge = EDGE_RIGHT; } else {
mScrollEdge = EDGE_NONE; } mSuppMatrix.postTranslate(deltaX, deltaY); return true;
}
為何要檢查邊界呢?那是由于當你進行旋轉或縮放變換后,由于縮放的錨點是以手指為中心的,有時候會發現顯示的區域不對,比如說,當圖片大于View的寬高時,但是矩陣的邊界與View之間竟然還有空白區,明顯不太公道。這時候需要進行平移對齊View的寬高。
在檢查顯示邊界時,我們需要獲得圖片的顯示矩形,那末怎樣獲得Drawable的終究顯示矩形呢?
getDrawMatrix()用來獲得mDrawMatrix終究矩陣,mDrawMatrix實際上是在mBaseMatrix基礎矩陣上后乘mSuppMatrix供應矩陣產生的。
public Matrix getDrawMatrix() {
mDrawMatrix.set(mBaseMatrix);
mDrawMatrix.postConcat(mSuppMatrix); return mDrawMatrix;
}
通過setImageViewMatrix將終究的矩陣利用到ImageView中,這時候我們就可以看到顯示效果了。
private void setImageViewMatrix(Matrix matrix) {
ImageView imageView = getImageView(); if (null != imageView) {
checkImageViewScaleType(); imageView.setImageMatrix(matrix); if (null != mMatrixChangeListener) {
RectF displayRect = getDisplayRect(matrix); if (null != displayRect) {
mMatrixChangeListener.onMatrixChanged(displayRect);
}
}
}
}
另外,通過以下的源碼可以獲得顯示矩形,matrix.mapRect用來映照最新的變換到原始的矩形。
private RectF getDisplayRect(Matrix matrix) {
ImageView imageView = getImageView(); if (null != imageView) {
Drawable d = imageView.getDrawable(); if (null != d) {
mDisplayRect.set(0, 0, d.getIntrinsicWidth(),
d.getIntrinsicHeight()); matrix.mapRect(mDisplayRect); return mDisplayRect;
}
} return null;
}
看完以上的源碼,相信流程已非常清楚了,當設置圖片時,通過update()我們可以初始化1個mBaseMatrix,然后如果想縮放、旋轉等,進行設置利用到mSuppMatrix,終究通過對mBaseMatrix和mSuppMatrix計算得到mDrawMatrix,然后利用到ImageView中,便完成了我們的使命了。
既然1切的變換都會利用到mSuppMatrix中。那末接下來我們回到PhotoViewAttacher的構造方法中繼續瀏覽其他源碼,以了解這個進程究竟是怎樣實現的。
Touch事件監聽
Touch事件中,主要讓手勢探測器進行處理事件。核心源碼以下。
public boolean onTouch(View v, MotionEvent ev) { boolean handled = false; if (mZoomEnabled && hasDrawable((ImageView) v)) {
ViewParent parent = v.getParent(); switch (ev.getAction()) { case ACTION_DOWN: if (null != parent) { parent.requestDisallowInterceptTouchEvent(true);
} else {
LogManager.getLogger().i(LOG_TAG, "onTouch getParent() returned null");
}
cancelFling(); break; case ACTION_CANCEL: case ACTION_UP: if (getScale() < mMinScale) { RectF rect = getDisplayRect(); if (null != rect) { v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
rect.centerX(), rect.centerY()));
handled = true;
}
} break;
} if (null != mScaleDragDetector) { boolean wasScaling = mScaleDragDetector.isScaling(); boolean wasDragging = mScaleDragDetector.isDragging();
handled = mScaleDragDetector.onTouchEvent(ev); boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling(); boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging();
mBlockParentIntercept = didntScale && didntDrag; } if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) {
handled = true;
}
} return handled;
}
雙擊縮放
我們來看1下雙擊縮放mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this));這類實現方案。DefaultOnDoubleTapListener實現了GestureDetector.OnDoubleTapListener接口。
public interface OnDoubleTapListener { /**
* 當單擊時回調,不同于OnGestureListener.onSingleTapUp(MotionEvent),這個回調方法只在確信譽戶不會產生第2次敲擊時調用
* @param e MotionEvent.ACTION_DOWN
* @return true if the event is consumed, else false
*/ boolean onSingleTapConfirmed(MotionEvent e); /**
* 當雙擊時調用.
* @param e MotionEvent.ACTION_DOWN
* @return true if the event is consumed, else false
*/ boolean onDoubleTap(MotionEvent e); /**
*當兩次敲擊間回調,回調 MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP事件
* @param e The motion event that occurred during the double-tap gesture.
* @return true if the event is consumed, else false
*/ boolean onDoubleTapEvent(MotionEvent e);
}
既然知道DefaultOnDoubleTapListener實現了GestureDetector.OnDoubleTapListener接口,那末直接去看DefaultOnDoubleTapListener中是怎樣實現的。
@Override public boolean onSingleTapConfirmed(MotionEvent e) { if (this.photoViewAttacher == null) return false;
ImageView imageView = photoViewAttacher.getImageView(); if (null != photoViewAttacher.getOnPhotoTapListener()) { final RectF displayRect = photoViewAttacher.getDisplayRect(); if (null != displayRect) { final float x = e.getX(), y = e.getY(); 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;
}else{ photoViewAttacher.getOnPhotoTapListener().onOutsidePhotoTap();
}
}
} if (null != photoViewAttacher.getOnViewTapListener()) {
photoViewAttacher.getOnViewTapListener().onViewTap(imageView, e.getX(), e.getY());
} return false;
} @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) { } return true;
} @Override public boolean onDoubleTapEvent(MotionEvent e) { return false;
}
從這里可以看出,在單擊時,會回調OnPhotoTapListener和OnViewTapListener,然后將坐標回調出去,如果是雙擊,則根據當前縮放比來判定現在的縮放比然后通過setScale設置縮放比和敲擊的坐標。單擊操作我們其實不怎樣關心,我們更關心雙擊的縮放操作,因而,查看setScale源碼。
@Override public void setScale(float scale, float focalX, float focalY, boolean animate) {
ImageView imageView = getImageView(); if (animate) {
imageView.post(new AnimatedZoomRunnable(getScale(), scale,
focalX, focalY));
} else { mSuppMatrix.setScale(scale, scale, focalX, focalY);
checkAndDisplayMatrix();
}
}
}
setScale的源碼還是比較簡單的,如果不需要動畫,直接設置給mSuppMatrix,然落后行檢查顯示。如果需要動畫的話,就履行AnimatedZoomRunnable。AnimatedZoomRunnable實現了Runnable接口,主要實現代碼以下。
private class AnimatedZoomRunnable implements Runnable { private final float mFocalX, mFocalY; private final long mStartTime; private final float mZoomStart, mZoomEnd; public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, final float focalX, final float focalY) {
mFocalX = focalX;
mFocalY = focalY;
mStartTime = System.currentTimeMillis();
mZoomStart = currentZoom;
mZoomEnd = targetZoom;
} @Override public void run() {
ImageView imageView = getImageView(); if (imageView == null) { return;
} float t = interpolate(); float scale = mZoomStart + t * (mZoomEnd - mZoomStart); float deltaScale = scale / getScale(); onScale(deltaScale, mFocalX, mFocalY); if (t < 1f) { Compat.postOnAnimation(imageView, this); }
}
} private float interpolate() { float t = 1.0F * (float)(System.currentTimeMillis() - this.mStartTime) / (float)PhotoViewAttacher.this.ZOOM_DURATION;
t = Math.min(1.0F, t);
t = PhotoViewAttacher.sInterpolator.getInterpolation(t); return t;
}
}
onScale的相干源碼以下,可以看出,調用了mSuppMatrix.postScale和checkAndDisplayMatrix()來進行顯示縮放。
@Override public void onScale(float scaleFactor, float focusX, float focusY) { if ((getScale() < mMaxScale || scaleFactor < 1f) && (getScale() > mMinScale || scaleFactor > 1f)) { if (null != mScaleChangeListener) { mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY);
} mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
checkAndDisplayMatrix();
}
}
雙擊縮放中的動畫縮放的流程是這樣的,首先會記錄1個開始時間mStartTime,然后根據當前時間來獲得插值interpolate()以便了解當前應當處于的進度,根據插值求出當前的縮放值scale,然后與上次相比求出縮放比差值deltaScale,然后通過onScale回調出去,終究通過Compat.postOnAnimation來履行這個Runable,如此反復直到插值為1,縮放到目標值為止。
雙指縮放及拖拽
雙擊縮放的相干源碼到此為止,接下來看看通過雙指縮放與拖拽的實現源碼。即VersionedGestureDetector.newInstance(imageView.getContext(), this);這句。
VersionedGestureDetector看名字便知道又做了版本兼容處理。里面只有1個靜態方法newInstance,源碼以下。
public final class VersionedGestureDetector { public static GestureDetector newInstance(Context context,
OnGestureListener listener) { final int sdkVersion = Build.VERSION.SDK_INT;
GestureDetector detector; if (sdkVersion < Build.VERSION_CODES.ECLAIR) { detector = new CupcakeGestureDetector(context);
} else if (sdkVersion < Build.VERSION_CODES.FROYO) { detector = new EclairGestureDetector(context);
} else {
detector = new FroyoGestureDetector(context);
}
detector.setOnGestureListener(listener); return detector;
}
}
newInstance中傳入了OnGestureListener,這個OnGestureListener是自定義的接口,源碼以下。
public interface OnGestureListener { void onDrag(float dx, float dy); void onFling(float startX, float startY, float velocityX, float velocityY); void onScale(float scaleFactor, float focusX, float focusY);
}
可以看出,回調了縮放、Fling和拖拽3種情況。現在我們回到newInstance相干源碼,可以看出有3種探測器CupcakeGestureDetector、EclairGestureDetector和FroyoGestureDetector。且3者是相互繼承的關系,FroyoGestureDetector繼承于EclairGestureDetector,EclairGestureDetector繼承于CupcakeGestureDetector。
其中CupcakeGestureDetector和EclairGestureDetector不支持雙指縮放。由于Android2.0以下不支持多點觸控,因而CupcakeGestureDetector核心源碼以下:
float getActiveX(MotionEvent ev) { return ev.getX();
} float getActiveY(MotionEvent ev) { return ev.getY();
} @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) { 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;