开源库地址:https://github.com/chrisbanes/PhotoView
PhotoView是一个用来帮助开发者轻松实现ImageView缩放的库。开发者可以轻易控制对图片的缩放旋等等操作。
PhotoView的使用极其简单,而且提供了两种方案。可以使用普通的ImageView,也可以使用该库中提供的ImageView(PhotoView)。
- 使用PhotoView 
 只需如下引用该库中的ImageView,无需关心其它实现细节,你的ImageView便可拥有缩放效果。- 1 
 2
 3
 4- <uk.co.senab.photoview.PhotoView 
 android:id="@+id/iv_photo"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent" />
- 针对普通ImageView 
 有的时候,可能因为一些历史原因,使得你不得不用原来的ImageView。幸运的是该库也提供了一种解决方案。只需用PhotoViewAttacher包装即可。- 1 
 2
 3
 4
 5- PhotoViewAttacher mAttacher=new PhotoViewAttacher(mImageView);//用PhotoViewAttacher包装 
 mAttacher.update();//当图片改变时需调用update();
 mAttacher.cleanup();//当ImageView不再使用时回收资源(可在onDestory中 调用)。PhotoView已经实现了这个功能不需要自己管理。
PhotoView真的很神奇,接下来我们去源码里一探究竟吧。顺便多说一句,图片的缩放大量运用到了Matrix相关知识,不了解的务必要先查阅相关资料哦。强烈推荐Android Matrix 这篇文章,当然也可以看我的这篇Android Matrix矩阵详解。
源码解读
这次源码解读我们从使用普通ImageView入手,普通的ImageView如果想缩放,必须依赖于PhotoViewAttacher,而PhotoViewAttacher又实现了IPhotoView接口。IPhotoView主要定义了一些常用的操作和默认值,由于方法实在太多了,就不一一列举了,直接上图。
IPhotoView定义的所有抽象方法如下。
IPhotoView的部分源码如下。1
2
3
4
5
6
7
8
9
10
11public 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的属性也比较多,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46private 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 WeakReference<ImageView> mImageView;//弱引用
//手势探测器
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中还定义了以下几个接口。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59public 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        - 第一次触摸事件
     * @param e2        - 第二次触摸事件
     * @param velocityX - 水平滑过的速度.
     * @param velocityY - 竖直滑过的素组.
     */
    boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}
在看完PhotoViewAttacher的一些属性和接口外,现在就来看PhotoViewAttacher的构造方法。即new PhotoViewAttacher(mImageView)这一句。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65public 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() {
                  //长按
                  
                  public void onLongPress(MotionEvent e) {
                      if (null != mLongClickListener) {
                          mLongClickListener.onLongClick(getImageView());
                      }
                  }
                  
                  //Fling
                   
                  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);
  }
构造方法主要做了一些初始化工作,比如添加了手势监听(双指缩放,拖拽,双击,长按)等等。而且,如果希望图片具备缩放功能,还得设置ImageView的scaleType为matrix,下面我们就一步步剖析。
默认设置
为了理解起来更连贯一点,我们先看setZoomable中的源码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 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的源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79private 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
        //基础矩阵就平移两者的宽度差一半,以保持居中
        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);//使最小的那一边也缩放到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()。1
2
3
4
5
6private void resetMatrix() {
        mSuppMatrix.reset();//重置供应矩阵
        setRotationBy(mBaseRotation);//设置初始的旋转角度
        setImageViewMatrix(getDrawMatrix());//把最mDrawMatrix设置给ImageView,以对图片进行变化。
        checkMatrixBounds();//检查Matrix边界
    }
设置旋转角度的源码如下,mSuppMatrix后乘了旋转角度。然后进行检查边界,最后进行显示。1
2
3
4
5
6
7
8
9
10
11
12 public void setRotationBy(float degrees) {
        mSuppMatrix.postRotate(degrees % 360);//后乘旋转角度
        checkAndDisplayMatrix();//检查Matrix边界,然后显示
}
//检查Matrix边界和显示
private void checkAndDisplayMatrix() {
        if (checkMatrixBounds()) {
           //调整效果进行显示
            setImageViewMatrix(getDrawMatrix());
        }
    }
checkMatrixBounds()用来检查Matrix边界。相关源码如下。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67private 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供应矩阵产生的。1
2
3
4
5public Matrix getDrawMatrix() {
    mDrawMatrix.set(mBaseMatrix);
    mDrawMatrix.postConcat(mSuppMatrix);
    return mDrawMatrix;
}
通过setImageViewMatrix将最终的矩阵应用到ImageView中,这时我们就能看到显示效果了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16private 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用来映射最新的变换到原始的矩形。1
2
3
4
5
6
7
8
9
10
11
12
13
14private 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()我们可以初始化一个mBaseMatrix,然后如果想缩放、旋转等,进行设置应用到mSuppMatrix,最终通过对mBaseMatrix和mSuppMatrix计算得到mDrawMatrix,然后应用到ImageView中,便完成了我们的使命了。
既然一切的变换都会应用到mSuppMatrix中。那么接下来我们回到PhotoViewAttacher的构造方法中继续阅读其他源码,以了解这个过程到底是怎么实现的。
Touch事件监听
Touch事件中,主要让手势探测器进行处理事件。核心源码如下。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58public 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;
}
双击缩放
我们来看一下双击缩放mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this));这种实现方案。DefaultOnDoubleTapListener实现了GestureDetector.OnDoubleTapListener接口。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public interface OnDoubleTapListener {
    /**
     * 当单击时回调,不同于OnGestureListener.onSingleTapUp(MotionEvent),这个回调方法只在确信用户不会发生第二次敲击时调用
     * @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中是怎么实现的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74//单击事件
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();//获取第一次敲击时的坐标
            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;
}
 //双击事件,在这里实现缩放
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;
}
public boolean onDoubleTapEvent(MotionEvent e) {
    //由于不需要处理两次敲击间的其他事件,故这里不做处理
    return false;
}
从这里可以看出,在单击时,会回调OnPhotoTapListener和OnViewTapListener,然后将坐标回调出去,如果是双击,则根据当前缩放比来判定现在的缩放比然后通过setScale设置缩放比以及敲击的坐标。单击操作我们并不怎么关心,我们更关心双击的缩放操作,于是,查看setScale源码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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接口,主要实现代码如下。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    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;
        }
        
        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()来进行显示缩放。1
2
3
4
5
6
7
8
9
10
11
12
13
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();
    }
}
双击缩放中的动画缩放的流程是这样的,首先会记录一个开始时间mStartTime,然后根据当前时间来获取插值interpolate()以便了解当前应该处于的进度,根据插值求出当前的缩放值scale,然后与上次相比求出缩放比差值deltaScale,然后通过onScale回调出去,最终通过Compat.postOnAnimation来执行这个Runable,如此反复直到插值为1,缩放到目标值为止。
双指缩放及拖拽
双击缩放的相关源码到此为止,接下来看看通过双指缩放与拖拽的实现源码。即VersionedGestureDetector.newInstance(imageView.getContext(), this);这句。
VersionedGestureDetector看名字便知道又做了版本兼容处理。里面只有一个静态方法newInstance,源码如下。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//根据版本进行了控制   
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是自定义的接口,源码如下。1
2
3
4
5
6
7
8
9
10public 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和拖拽三种情况。现在我们回到newInstance相关源码,可以看出有三种探测器CupcakeGestureDetector、EclairGestureDetector和FroyoGestureDetector。且三者是相互继承的关系,FroyoGestureDetector继承于EclairGestureDetector,EclairGestureDetector继承于CupcakeGestureDetector。
其中CupcakeGestureDetector和EclairGestureDetector不支持双指缩放。由于Android2.0以下不支持多点触控,于是CupcakeGestureDetector核心源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
float getActiveX(MotionEvent ev) {
    return ev.getX();
}
float getActiveY(MotionEvent ev) {
    return ev.getY();
}
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;
        }
        case MotionEvent.ACTION_CANCEL: {
            if (null != mVelocityTracker) {
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            //手指抬起时,如果之前在拖拽
            if (mIsDragging) {
                if (null != mVelocityTracker) {
                    mLastTouchX = getActiveX(ev);
                    mLastTouchY = getActiveY(ev);
                    //计算滑动速度
                    mVelocityTracker.addMovement(ev);
                    mVelocityTracker.computeCurrentVelocity(1000);
                    final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker
                            .getYVelocity();
                    //如果大于最小的Fling速度,就回调出去
                    if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) {
                        mListener.onFling(mLastTouchX,、mLastTouchY, -vX,-vY);
                    }
                }
            }
            //回收速度探测器
            if (null != mVelocityTracker) {
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            }
            break;
        }
    }
    return true;
}
从源码可以看出CupcakeGestureDetector实现了拖拽和Fling效果。EclairGestureDetector用于Android 2.2以下,主要修正了多点触控的问题,因为当双指触控时,我们需要获取的是最后一个手指离开屏幕时的坐标,因此需要使getActiveX/getActiveY指向正确的点。源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
   float getActiveX(MotionEvent ev) {
       try {
           return ev.getX(mActivePointerIndex);//mActivePointerIndex为手指的索引。根据当前手指的索引获取坐标
       } catch (Exception e) {
           return ev.getX();
       }
   }
   
   float getActiveY(MotionEvent ev) {
       try {
           return ev.getY(mActivePointerIndex);
       } catch (Exception e) {
           return ev.getY();
       }
   }
   public boolean onTouchEvent(MotionEvent ev) {
       final int action = ev.getAction();
       switch (action & MotionEvent.ACTION_MASK) {
           case MotionEvent.ACTION_DOWN:
               mActivePointerId = ev.getPointerId(0);//第一根手指的id
               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()); 
               //根据索引获取id
               final int pointerId = ev.getPointerId(pointerIndex);
               if (pointerId == mActivePointerId) {//如果是抬起的是第一根手指
                   //那么对应获取第二点
                   final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                   mActivePointerId = ev.getPointerId(newPointerIndex);//将id指向第二根手指
                   //获取第二根手指的当前坐标
                   mLastTouchX = ev.getX(newPointerIndex);
                   mLastTouchY = ev.getY(newPointerIndex);
               }
               break;
       }
       //将索引指向后抬起的手指
       mActivePointerIndex = ev
               .findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId
                       : 0);
       try {
           return super.onTouchEvent(ev);//按照`CupcakeGestureDetector`的逻辑处理
       } catch (IllegalArgumentException e) {
           return true;
       }
   }
FroyoGestureDetector用于Android 2.2以上,此时系统已经提供了一个缩放探索器,于是在拖拽和Fling的基础上,添加了双指缩放功能,核心源码如下。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() {
           
           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;
           }
        //..
        //省略了部分源码
图片的缩放与拖拽并没有在探测器中实现,而是回调到了PhotoViewAttacher中,PhotoViewAttacher实现了OnGestureListener接口,相关处理如下。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42  //拖拽回调
  
   public void onDrag(float dx, float dy) {
       if (mScaleDragDetector.isScaling()) {
           return; // 如果正在缩放,不许做其他操作
       }
        //根剧拖拽进行平移
       ImageView imageView = getImageView();
       mSuppMatrix.postTranslate(dx, dy);
       checkAndDisplayMatrix();
      //判断父布局是不是可以拦截这一拖拽行为
       ViewParent parent = imageView.getParent();
       if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) {
           //如果没有阻止父布局拦截且图片已显示在相关边缘内,就允许拦截
           if (mScrollEdge == EDGE_BOTH
                   || (mScrollEdge == EDGE_LEFT && dx >= 1f)
                   || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) {
               if (null != parent) {
                   parent.requestDisallowInterceptTouchEvent(false);
               }
           }
       } else {
           //否则不允许拦截
           if (null != parent) {
               parent.requestDisallowInterceptTouchEvent(true);
           }
       }
   }
//Fling回调
   
   public void onFling(float startX, float startY, float velocityX,
                       float velocityY) {
       ImageView imageView = getImageView();
       mCurrentFlingRunnable = new FlingRunnable(imageView.getContext());
        //传入fling的速度与View的宽高
       mCurrentFlingRunnable.fling(getImageViewWidth(imageView),getImageViewHeight(imageView), (int) velocityX, (int) velocityY);
               
       imageView.post(mCurrentFlingRunnable);
   }
其中FlingRunnable的源码如下。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71private class FlingRunnable implements Runnable {
    private final ScrollerProxy mScroller;//Scroller这里做了版本兼容处理,API小于9时用了PreGingerScroller(内部用了Scroller),小于14用了GingerScroller(内部用了OverScroller),其他用了IcsScroller(内部用了OverScroller)。
    private int mCurrentX, mCurrentY;//当前坐标
    public FlingRunnable(Context context) {
        mScroller = ScrollerProxy.getScroller(context);
    }
    public void cancelFling() {
        mScroller.forceFinished(true);//停止
    }
    public void fling(int viewWidth, int viewHeight, int velocityX, int velocityY) {
        final RectF rect = getDisplayRect();//获取图片的显示区域
        if (null == rect) {
            return;
        }
         //水平方向上
        final int startX = Math.round(-rect.left);//四舍五入,左边的x坐标
        final int minX, maxX, minY, maxY;//Fling的边界值
        if (viewWidth < rect.width()) {//如果图片的宽度大于View宽时就计算X的边界。
            minX = 0;
            maxX = Math.round(rect.width() - viewWidth);
        } else {
            minX = maxX = startX;//如果图片宽小于View宽,就将三者设为一样。
        }
        //竖直方向上
        final int startY = Math.round(-rect.top);
        if (viewHeight < rect.height()) {//如果显示矩形高大于View的高。就计算边界
            minY = 0;
            maxY = Math.round(rect.height() - viewHeight);
        } else {
            minY = maxY = startY;
        }
        mCurrentX = startX;
        mCurrentY = startY;
        //调用 mScroller.fling
        if (startX != maxX || startY != maxY) {
            mScroller.fling(startX, startY, velocityX, velocityY, minX,
                    maxX, minY, maxY, 0, 0);
        }
    }
    
  public void run() {
        if (mScroller.isFinished()) {
            return; 
        }
        ImageView imageView = getImageView();
        if (null != imageView && mScroller.computeScrollOffset()) {
           //获取当前的位置
            final int newX = mScroller.getCurrX();
            final int newY = mScroller.getCurrY();
            //将平移差值应用到mSuppMatrix
            mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY);
            setImageViewMatrix(getDrawMatrix());//应用到矩阵
            mCurrentX = newX;
            mCurrentY = newY;
            Compat.postOnAnimation(imageView, this);
        }
    }
}
同样,利用了Compat.postOnAnimation不停执行Runable来实现Fling惯性滚动效果。
关于PhotoViewAttacher的相关源码已经解读完毕,而该库中的空间PhotoView的实现也是依赖于PhotoViewAttacher,在onDetachedFromWindow中会自动回收资源,核心源码如下,其他就不做详细介绍了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27public PhotoView(Context context, AttributeSet attr, int defStyle) {
     super(context, attr, defStyle);
     super.setScaleType(ScaleType.MATRIX);
     this.init();
 }
 //初始化
 protected void init() {
     if(null == this.mAttacher || null == this.mAttacher.getImageView()) {
         this.mAttacher = new PhotoViewAttacher(this);
     }
     if(null != this.mPendingScaleType) {
         this.setScaleType(this.mPendingScaleType);
         this.mPendingScaleType = null;
     }
 }
 //调用回收cleanup
 protected void onDetachedFromWindow() {
     this.mAttacher.cleanup();
     super.onDetachedFromWindow();
 }
 //初始化
 protected void onAttachedToWindow() {
     this.init();
     super.onAttachedToWindow();
 }
最后
感觉最近写东西越来越啰嗦了,需要练习着把话讲的简练一点,下一期源码解读:Gson。
本期解读到此结束,如有错误之处,欢迎指出。
