程序员人生 网站导航

PhotoView 源码解读

栏目:综合技术时间:2016-08-01 17:24:36

转载请注明本文出自maplejaw的博客(http://blog.csdn.net/maplejaw_)

开源库地址:https://github.com/chrisbanes/PhotoView
PhotoView是1个用来帮助开发者轻松实现ImageView缩放的库。开发者可以轻易控制对图片的缩放旋等等操作。
image_1akkgdkg01saq1l01jdvaaqeeh9.png-11.6kB
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);//用PhotoViewAttacher包装 mAttacher.update();//当图片改变时需调用update(); mAttacher.cleanup();//当ImageView不再使用时回收资源(可在onDestory中 调用)。PhotoView已实现了这个功能不需要自己管理。

PhotoView真的很奇异,接下来我们去源码里1探究竟吧。顺便多说1句,图片的缩放大量应用到了Matrix相干知识,不了解的务必要先查阅相干资料哦。强烈推荐Android Matrix 这篇文章,固然也能够看我的这篇Android Matrix矩阵详解。

源码解读

这次源码解读我们从使用普通ImageView入手,普通的ImageView如果想缩放,必须依赖于PhotoViewAttacher,而PhotoViewAttacher又实现了IPhotoView接口。IPhotoView主要定义了1些经常使用的操作和默许值,由于方法实在太多了,就不逐一罗列了,直接上图。
IPhotoView定义的所有抽象方法以下。
image_1am7q83srm9n1nb5odsarv17sm9.png-98.5kB
IPhotoView的部份源码以下。

public interface IPhotoView { float DEFAULT_MAX_SCALE = 3.0f;//默许最大缩放倍数为3倍 float DEFAULT_MID_SCALE = 1.75f;//默许中间缩放倍数为1.75倍 float DEFAULT_MIN_SCALE = 1.0f;//默许最小缩放倍数为1倍 int DEFAULT_ZOOM_DURATION = 200;//默许的缩放间隔为200ms 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;//图片左侧显示在View的左侧缘内 static final int EDGE_RIGHT = 1;//图片右侧显示在View的右侧缘内 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;//单击,长按,Fling private uk.co.senab.photoview.gestures.GestureDetector mScaleDragDetector;//缩放和拖拽 private final Matrix mBaseMatrix = new Matrix();//基础矩阵,用来保存初始的显示矩阵 private final Matrix mDrawMatrix = new Matrix();//绘画矩阵,用来计算最后显示区域的矩阵,是在mBaseMatrix和mSuppMatrix的基础上计算出来的。 private final Matrix mSuppMatrix = new Matrix();//这个矩阵我也不知道怎样称呼,也不知道是否是Supply的意思,暂且叫作供应矩阵吧,用来保存旋转平移和缩放的矩阵。 private final RectF mDisplayRect = new RectF();//显示矩形 private final float[] mMatrixValues = new float[9];//用来保存矩阵的值。3*3 // 各类监听 private OnMatrixChangedListener mMatrixChangeListener; private OnPhotoTapListener mPhotoTapListener; private OnViewTapListener mViewTapListener; private OnLongClickListener mLongClickListener; private OnScaleChangeListener mScaleChangeListener; private OnSingleFlingListener mSingleFlingListener; //保存ImageView的top,right,bottom,left private int mIvTop, mIvRight, mIvBottom, mIvLeft; //Fling时的Runable 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);//开启绘制缓存区,用于获得可见区的bitmap imageView.setOnTouchListener(this);//设置Touch监听,用于添加手势监听 ViewTreeObserver observer = imageView.getViewTreeObserver(); if (null != observer) observer.addOnGlobalLayoutListener(this);//用于监听ImageView的大小 // 确保ImageView的ScaleType为Matrix setImageViewScaleTypeMatrix(imageView); if (imageView.isInEditMode()) { return; } //初始化多指缩放/拖拽手势探测器 mScaleDragDetector = VersionedGestureDetector.newInstance( imageView.getContext(), this); //初始化其它手势监听(长按,Fling) mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() { //长按 @Override public void onLongPress(MotionEvent e) { if (null != mLongClickListener) { mLongClickListener.onLongClick(getImageView()); } } //Fling @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();//获得ImageView if (null != imageView) { if (mZoomEnabled) { //再次确保ImageView的ScaleType为MATRIX setImageViewScaleTypeMatrix(imageView); //更新基础矩阵mBaseMatrix updateBaseMatrix(imageView.getDrawable()); } else { //重置矩阵 resetMatrix(); } } }

可以看出,除赋值mZoomEnabled外,还调用了update()方法,前面我们说了,每次更换图片时需调用update()刷新。在update()中,如果是可缩放的,就更新mBaseMatrix,否则重置矩阵。
updateBaseMatrix的源码以下:

private void updateBaseMatrix(Drawable d) { ImageView imageView = getImageView();//获得ImageView if (null == imageView || null == d) { return; } //获得ImageView的宽高 final float viewWidth = getImageViewWidth(imageView); final float viewHeight = getImageViewHeight(imageView); //获得Drawable的固有的宽高 final int drawableWidth = d.getIntrinsicWidth(); final int drawableHeight = d.getIntrinsicHeight(); mBaseMatrix.reset();//重置mBaseMatrix矩阵 //获得宽的缩放比,drawableWidth * widthScale = viewWidth final float widthScale = viewWidth / drawableWidth; //获得高的缩放比,drawableHeight * heightScale = viewHeight final float heightScale = viewHeight / drawableHeight; //注意,这里的ScaleType不是ImageView的ScaleType,由于ImageView的ScaleType已被强迫设为Matrix。这里的ScaleType是PhotoViewAttacher的ScaleType,因此可以通过设置PhotoViewAttacher的setScaleType来摹拟原ImageView的效果,以满足实际需求。 if (mScaleType == ScaleType.CENTER) {//如果缩放类型为ScaleType.CENTER //基础矩阵就平移二者的宽度差1半,以保持居中 mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F, (viewHeight - drawableHeight) / 2F); } else if (mScaleType == ScaleType.CENTER_CROP) {//如果缩放类型为ScaleType.CENTER_CROP float scale = Math.max(widthScale, heightScale);//取最大值 mBaseMatrix.postScale(scale, scale);//使最小的那1边也缩放到View的尺寸 //平移到中间 mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, (viewHeight - drawableHeight * scale) / 2F); } else if (mScaleType == ScaleType.CENTER_INSIDE) { //如果缩放类型为ScaleType.CENTER_INSIDE //计算缩放值 float scale = Math.min(1.0f, Math.min(widthScale, heightScale)); //当图片宽高超越View宽高时调用,否则缩放还是1 mBaseMatrix.postScale(scale, scale); //平移到中间 mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, (viewHeight - drawableHeight * scale) / 2F);//平移 } else { //如果是FIT_XX相干的缩放类型 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); } //直接根据Matrix提供的setRectToRect来设置 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());//把最mDrawMatrix设置给ImageView,以对图片进行变化。 checkMatrixBounds();//检查Matrix边界 }

设置旋转角度的源码以下,mSuppMatrix后乘了旋转角度。然落后行检查边界,最落后行显示。

public void setRotationBy(float degrees) { mSuppMatrix.postRotate(degrees % 360);//后乘旋转角度 checkAndDisplayMatrix();//检查Matrix边界,然后显示 } //检查Matrix边界和显示 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);//获得View的高 if (height <= viewHeight) {//如果图片的高小于等于View,说明图片的垂直方向可以完全显示在View里面 //因而根据缩放类型进行边界调剂 switch (mScaleType) { case FIT_START: deltaY = -rect.top;//向上移动到View的顶部 break; case FIT_END: deltaY = viewHeight - height - rect.top;//向下移动到View的底部 break; default: deltaY = (viewHeight - height) / 2 - rect.top;//否则就居中显示 break; } } else if (rect.top > 0) { //如果图片高度超越来View的高,但是rect.top > 0说明ImageView上边还有空余的区域。 deltaY = -rect.top;//因而计算偏移距离 } else if (rect.bottom < viewHeight) { //同理。底部也有空余 deltaY = viewHeight - rect.bottom; } //获得ImageView的宽,同理进行边界调剂。 final int viewWidth = getImageViewWidth(imageView); if (width <= viewWidth) {//如果宽度小于View的宽,进行相应调剂 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;//图片宽度小于View的宽度,说明两边显示在边沿内 } else if (rect.left > 0) { mScrollEdge = EDGE_LEFT;//rect.left > 0表示显示在左侧边沿内 deltaX = -rect.left; } else if (rect.right < viewWidth) { deltaX = viewWidth - rect.right; mScrollEdge = EDGE_RIGHT;//右侧在边沿内 } else { mScrollEdge = EDGE_NONE;//两边都不在边沿内 } //最后,将平移给mSuppMatrix 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();//获得ImageView if (null != imageView) { checkImageViewScaleType();//检查缩放类型,必须为Matrix,否则抛异常 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());//获得Drawable尺寸,初始化原始矩形 matrix.mapRect(mDisplayRect);//将矩阵的变换映照给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) { //不允许父布局拦截ACTION_DOWN事件 parent.requestDisallowInterceptTouchEvent(true); } else { LogManager.getLogger().i(LOG_TAG, "onTouch getParent() returned null"); } cancelFling(); //取消Fling事件 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; } //如果mScaleDragDetector(缩放、拖拽)不为空,让它处理事件 if (null != mScaleDragDetector) { //获得状态 boolean wasScaling = mScaleDragDetector.isScaling(); boolean wasDragging = mScaleDragDetector.isDragging(); handled = mScaleDragDetector.onTouchEvent(ev); //mScaleDragDetector处理事件过后的状态,如果前后都不在缩放和拖拽,就允许父布局拦截 boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling(); boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging(); mBlockParentIntercept = didntScale && didntDrag;//禁止父类拦截的标识 } // 如果mGestureDetector(双击,长按)不为空,交给它处理事件 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();//获得ImageView //如果OnPhotoTapListener不为null时回调 if (null != photoViewAttacher.getOnPhotoTapListener()) { final RectF displayRect = photoViewAttacher.getDisplayRect();//获得当前的显示矩形 if (null != displayRect) { final float x = e.getX(), y = e.getY();//获得第1次敲击时的坐标 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(); } } } //如果OnViewTapListener不为null时回调,不管在不在图片里外 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()) { //如果之前的缩放小于中等值,现在就缩放到中等值,缩放锚点就是当前的敲击事件坐标,true表示需要动画缩放。 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; } @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();//获得ImageView //.. //省略了部份源码 //是不是需要动画 if (animate) { imageView.post(new AnimatedZoomRunnable(getScale(), scale, focalX, focalY)); } else { //设置给mSuppMatrix矩阵 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();//获得缩放比,大于1表示在放大,小于1在缩小。deltaScale * getScale() = scale //回调出去,deltaScale表示相对上次要缩放的比例 onScale(deltaScale, mFocalX, mFocalY); if (t < 1f) {//插值小于1表示没有缩放完成,通过不停post进行履行动画 Compat.postOnAnimation(imageView, this);//Compat根据版本做了兼容处理,小于4.2用了 view.postDelayed,大于等于4.2用了view.postOnAnimation。 } } } //计算当前时间的插值 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) { //小于Android 2.0 detector = new CupcakeGestureDetector(context); } else if (sdkVersion < Build.VERSION_CODES.FROYO) { //小于Android 2.2 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); //Fling时回调 void onFling(float startX, float startY, float velocityX, float velocityY); //缩放时回调,`onScale`在双击动画缩放时已介绍过了,scaleFactor表示相对上次的缩放比 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) { //如果手指移动的距离大于mTouchSlop,表示在拖拽 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;
------分隔线----------------------------
------分隔线----------------------------

最新技术推荐