PhotoView 源码解读

开源库地址:https://github.com/chrisbanes/PhotoView

PhotoView是一个用来帮助开发者轻松实现ImageView缩放的库。开发者可以轻易控制对图片的缩放旋等等操作。

PhotoView的使用极其简单,而且提供了两种方案。可以使用普通的ImageView,也可以使用该库中提供的ImageView(PhotoView)。

  • 使用PhotoView

    只需如下引用该库中的ImageView,无需关心其它实现细节,你的ImageView便可拥有缩放效果。

<uk.co.senab.photoview.PhotoView
            android:id="@+id/iv_photo"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent" />
  • 针对普通ImageView

    有的时候,可能因为一些历史原因,使得你不得不用原来的ImageView。幸运的是该库也提供了一种解决方案。只需用PhotoViewAttacher包装即可。

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的部分源码如下。

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 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中还定义了以下几个接口。

    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        - 第一次触摸事件
         * @param e2        - 第二次触摸事件
         * @param velocityX - 水平滑过的速度.
         * @param velocityY - 竖直滑过的素组.
         */
        boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
    }

在看完PhotoViewAttacher的一些属性和接口外,现在就来看PhotoViewAttacher的构造方法。即new PhotoViewAttacher(mImageView)这一句。

  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);
    }

构造方法主要做了一些初始化工作,比如添加了手势监听(双指缩放,拖拽,双击,长按)等等。而且,如果希望图片具备缩放功能,还得设置ImageView的scaleType为matrix,下面我们就一步步剖析。

默认设置

为了理解起来更连贯一点,我们先看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
            //基础矩阵就平移两者的宽度差一半,以保持居中
            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()

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()我们可以初始化一个mBaseMatrix,然后如果想缩放、旋转等,进行设置应用到mSuppMatrix,最终通过对mBaseMatrix和mSuppMatrix计算得到mDrawMatrix,然后应用到ImageView中,便完成了我们的使命了。

既然一切的变换都会应用到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;
    }

双击缩放

我们来看一下双击缩放mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this));这种实现方案。DefaultOnDoubleTapListener实现了GestureDetector.OnDoubleTapListener接口。

    public 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中是怎么实现的。

    //单击事件
    @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();//获取第一次敲击时的坐标

                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,然后进行检查显示。如果需要动画的话,就执行AnimatedZoomRunnableAnimatedZoomRunnable实现了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.postScalecheckAndDisplayMatrix()来进行显示缩放。

    @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();
        }
    }

双击缩放中的动画缩放的流程是这样的,首先会记录一个开始时间mStartTime,然后根据当前时间来获取插值interpolate()以便了解当前应该处于的进度,根据插值求出当前的缩放值scale,然后与上次相比求出缩放比差值deltaScale,然后通过onScale回调出去,最终通过Compat.postOnAnimation来执行这个Runable,如此反复直到插值为1,缩放到目标值为止。

双指缩放及拖拽

双击缩放的相关源码到此为止,接下来看看通过双指缩放与拖拽的实现源码。即VersionedGestureDetector.newInstance(imageView.getContext(), this);这句。

VersionedGestureDetector看名字便知道又做了版本兼容处理。里面只有一个静态方法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和拖拽三种情况。现在我们回到newInstance相关源码,可以看出有三种探测器CupcakeGestureDetectorEclairGestureDetectorFroyoGestureDetector。且三者是相互继承的关系,FroyoGestureDetector继承于EclairGestureDetectorEclairGestureDetector继承于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;
            }

            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指向正确的点。源码如下:


 @Override
    float getActiveX(MotionEvent ev) {
        try {
            return ev.getX(mActivePointerIndex);//mActivePointerIndex为手指的索引。根据当前手指的索引获取坐标
        } catch (Exception e) {
            return ev.getX();
        }
    }

    @Override
    float getActiveY(MotionEvent ev) {
        try {
            return ev.getY(mActivePointerIndex);
        } catch (Exception e) {
            return ev.getY();
        }
    }

 @Override
    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的基础上,添加了双指缩放功能,核心源码如下。

 ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() {

            @Override
            public boolean onScale(ScaleGestureDetector detector) {
                float scaleFactor = detector.getScaleFactor();//获取相比于当其缩放值的缩放比例

                if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor))
                    return false;
                 //回调出去。
                mListener.onScale(scaleFactor,
                        detector.getFocusX(), detector.getFocusY());
                return true;
            }
         //..
         //省略了部分源码

图片的缩放与拖拽并没有在探测器中实现,而是回调到了PhotoViewAttacher中,PhotoViewAttacher实现了OnGestureListener接口,相关处理如下。

   //拖拽回调
   @Override
    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回调
    @Override
    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的源码如下。

    private 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);
            }
        }

        @Override
      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中会自动回收资源,核心源码如下,其他就不做详细介绍了。

   public 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。



本期解读到此结束,如有错误之处,欢迎指出。

时间: 2024-10-27 11:07:45

PhotoView 源码解读的相关文章

QCustomplot使用分享(二) 源码解读

一.头文件概述 从这篇文章开始,我们将正式的进入到QCustomPlot的实践学习中来,首先我们先来学习下QCustomPlot的类图,如果下载了QCustomPlot源码的同学可以自己去QCustomPlot的目录下documentation/qcustomplot下寻找一个名字叫做index.html的文件,将其在浏览器中打开,也是可以找到这个库的类图.如图1所示,是组成一个QCustomPlot类图的可能组成形式. 一个图表(QCustomPlot):包含一个或者多个图层.一个或多个ite

vue源码解读预热-0

vueJS的源码解读 vue源码总共包含约一万行代码量(包括注释)特别感谢作者Evan You开放的源代码,访问地址为Github 代码整体介绍与函数介绍预览 代码模块分析 代码整体思路 总体的分析 从图片中可以看出的为采用IIFE(Immediately-Invoked Function Expression)立即执行的函数表达式的形式进行的代码的编写 常见的几种插件方式: (function(,){}(,))或(function(,){})(,)或!function(){}()等等,其中必有

SpringMVC源码解读 - RequestMapping注解实现解读 - RequestCondition体系

一般我们开发时,使用最多的还是@RequestMapping注解方式. @RequestMapping(value = "/", param = "role=guest", consumes = "!application/json") public void myHtmlService() { // ... } 台前的是RequestMapping ,正经干活的却是RequestCondition,根据配置的不同条件匹配request. @Re

jdk1.8.0_45源码解读——HashMap的实现

jdk1.8.0_45源码解读——HashMap的实现 一.HashMap概述 HashMap是基于哈希表的Map接口实现的,此实现提供所有可选的映射操作.存储的是<key,value>对的映射,允许多个null值和一个null键.但此类不保证映射的顺序,特别是它不保证该顺序恒久不变.  除了HashMap是非同步以及允许使用null外,HashMap 类与 Hashtable大致相同. 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能.迭代col

15、Spark Streaming源码解读之No Receivers彻底思考

在前几期文章里讲了带Receiver的Spark Streaming 应用的相关源码解读,但是现在开发Spark Streaming的应用越来越多的采用No Receivers(Direct Approach)的方式,No Receiver的方式的优势: 1. 更强的控制自由度 2. 语义一致性 其实No Receivers的方式更符合我们读取数据,操作数据的思路的.因为Spark 本身是一个计算框架,他底层会有数据来源,如果没有Receivers,我们直接操作数据来源,这其实是一种更自然的方式

jdk1.8.0_45源码解读——Set接口和AbstractSet抽象类的实现

jdk1.8.0_45源码解读——Set接口和AbstractSet抽象类的实现 一. Set架构 如上图: (01) Set 是继承于Collection的接口.它是一个不允许有重复元素的集合.(02) AbstractSet 是一个抽象类,它继承于AbstractCollection.AbstractCollection实现了Set中的绝大部分函数,为Set的实现类提供了便利.(03) HastSet 和 TreeSet 是Set的两个实现类.        HashSet依赖于HashMa

线程本地变量ThreadLocal源码解读

  一.ThreadLocal基础知识   原始线程现状: 按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步.但是Spring中的各种模板类并未采用线程同步机制,因为线程同步会影响并发性和系统性能,而且实现难度也不小. ThreadLocal在Spring中发挥着重要的作用.在管理request作用域的bean,事务管理,任务调度,AOP等模块中都出现了它的身影. ThreadLocal介绍: 它不是一个线程,而是线程的一个本地化

hadoop源码解读namenode高可靠:HA;web方式查看namenode下信息;dfs/data决定datanode存储位置

点击browserFilesystem,和命令查看结果一样 当我们查看hadoop源码时,我们看到hdfs下的hdfs-default.xml文件信息 我们查找${hadoop.tmp.dir}这是引用变量,肯定在其他文件有定义,在core-default.xml中查看到,这两个配置文件有个共同点: 就是不要修改此文件,但可以复制信息到core-site.xml和hdfs-site.xml中修改 usr/local/hadoop 是我存放hadoop文件夹的地方 几个关于namenode的重要文

Jfinal启动源码解读

本文对Jfinal的启动源码做解释说明. PS:Jfinal启动容器可基于Tomcat/Jetty等web容器启动,本文基于Jetty的启动方式做启动源码的解读和分析,tomcat类似. 入口  JFinalConfig的继承类的Main方法为入口,实例代码继承类为:DemoConfig,Main方法如下: public static void main(String[] args) { /** * 特别注意:Eclipse 之下建议的启动方式 */ JFinal.start("WebRoot&