转载请注明本文出自maplejaw的博客(http://blog.csdn.net/maplejaw_)
开源库地址:https://github.com/chrisbanes/PhotoView
PhotoView是1个用来帮助开发者轻松实现ImageView缩放的库。开发者可以轻易控制对图片的缩放旋等等操作。
PhotoView的使用极为简单,而且提供了两种方案。可使用普通的ImageView,也能够使用该库中提供的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;