Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> PhotoView 源碼解讀

PhotoView 源碼解讀

編輯:關於Android編程

開源庫地址:https://github.com/chrisbanes/PhotoView

PhotoView是一個用來幫助開發者輕松實現ImageView縮放的庫。開發者可以輕易控制對圖片的縮放旋等等操作。

PhotoView的使用極其簡單,而且提供了兩種方案。可以使用普通的ImageView,也可以使用該庫中提供的ImageView(PhotoView)。

使用PhotoView
只需如下引用該庫中的ImageView,無需關心其它實現細節,你的ImageView便可擁有縮放效果。

針對普通ImageView
有的時候,可能因為一些歷史原因,使得你不得不用原來的ImageView。幸運的是該庫也提供了一種解決方案。只需用PhotoViewAttacher包裝即可。
PhotoViewAttacher mAttacher=new PhotoViewAttacher(mImageView);//用PhotoViewAttacher包裝

mAttacher.update();//當圖片改變時需調用update();

mAttacher.cleanup();//當ImageView不再使用時回收資源(可在onDestory中 調用)。PhotoView已經實現了這個功能不需要自己管理。

PhotoView真的很神奇,接下來我們去源碼裡一探究竟吧。順便多說一句,圖片的縮放大量運用到了Matrix相關知識,不了解的務必要先查閱相關資料哦。

源碼解讀

這次源碼解讀我們從使用普通ImageView入手,普通的ImageView如果想縮放,必須依賴於PhotoViewAttacher,而PhotoViewAttacher又實現了IPhotoView接口。IPhotoView主要定義了一些常用的操作和默認值,由於方法實在太多了,就不一一列舉了,直接上圖。
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 WeakReference 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。


本期解讀到此結束,如有錯誤之處,歡迎指出。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved