PhotoView源碼分析(1)

簡介

PhotoView屬性:
可以用於查看圖片,並對圖片進行拖動縮放,拖動過程中不會出現邊緣空白;
雙擊縮小放大,Fling移動,並支持上述過程的漸變;
在放大情況下也支持viewpager等的拖動切換;
支持多擊事件檢測,單機,雙擊事件;
支持各種回調給調用者;

準備知識

Log

public int v(String tag, String msg, Throwable tr) {
    // tr: An exception to log
    return Log.v(tag, msg, tr);
}

    // let debug flag be dynamic, but still Proguard can be used to remove from
    // release builds
    private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);

ScrollerProxy代理類

抽象類,裏面根據系統版本會提供對應的Scroller對象

public static ScrollerProxy getScroller(Context context) {
        if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD) {
            return new PreGingerScroller(context);
        } else if (VERSION.SDK_INT < VERSION_CODES.ICE_CREAM_SANDWICH) {
            return new GingerScroller(context);
        } else {
            return new IcsScroller(context);
        }
}

9 2.3
PreGingerScroller:
Scroller mScroller = new Scroller(context);

14 4.0
GingerScroller:

mScroller = new OverScroller(context);
    public boolean computeScrollOffset() {
        // Workaround for first scroll returning 0 for the direction of the edge it hits.
        // Simply recompute values. // Workaround : 工作區
        if (mFirstScroll) { // 暫時沒看到設置爲true的地方
            mScroller.computeScrollOffset();
            mFirstScroll = false;
        }
        return mScroller.computeScrollOffset();
    }

4.0+
IcsScroller: extends GingerScroller

 public boolean computeScrollOffset() {
        return mScroller.computeScrollOffset();
 }

VersionedGestureDetector 手勢檢測

根據系統版本返回對應的手勢檢測算法對象, 返回的對象都實現了自定義的GestureDetector接口,可見,其自身只是維護了手勢檢測的算法,需要真正的使用者傳參數過來調用的。
mListener.onDrag(dx, dy);和mListener.onFling(mLastTouchX, mLastTouchY, -vX, -vY);以及
onScale(float scaleFactor, float focusX, float focusY);會回調出去的

public static GestureDetector newInstance(Context context,
                                              OnGestureListener listener) {
        final int sdkVersion = Build.VERSION.SDK_INT;
        GestureDetector detector;
        if (sdkVersion < Build.VERSION_CODES.ECLAIR) {
            detector = new CupcakeGestureDetector(context);
        } else if (sdkVersion < Build.VERSION_CODES.FROYO) {
            detector = new EclairGestureDetector(context);
        } else {
            detector = new FroyoGestureDetector(context);
        }
        detector.setOnGestureListener(listener);
        return detector;
}

版本 5 2點0 CupcakeGestureDetector

onDrag和onFling都在此處回調

    public CupcakeGestureDetector(Context context) {
        final ViewConfiguration configuration = ViewConfiguration.get(context);
        // Minimum velocity to initiate a fling, as measured in pixels per second.
        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
        // Distance in pixels a touch can wander before we think the user is scrolling
        mTouchSlop = configuration.getScaledTouchSlop();
    }

    public boolean isScaling() { // !!!
        return false;
    }

    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                // Helper for tracking the velocity of touch events
                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) {
                    // Use Pythagoras to see if drag length is larger than
                    // touch slop // Pythagoras:勾股定理
                    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: {
                // Recycle Velocity Tracker
                if (null != mVelocityTracker) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                if (mIsDragging) {
                    if (null != mVelocityTracker) {
                        mLastTouchX = getActiveX(ev);
                        mLastTouchY = getActiveY(ev);
                        // Compute velocity within the last 1000ms
                        mVelocityTracker.addMovement(ev);
                        mVelocityTracker.computeCurrentVelocity(1000);
                        final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker.getYVelocity();
                        // If the velocity is greater than minVelocity, call
                        // listener
                        if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) {
                            mListener.onFling(mLastTouchX, mLastTouchY, -vX,
                                    -vY);
                            // 注意傳過去的速度值的符號!假設手向左滑動了,那麼內容區域應該也向左Fling
                            // 此時傳過去的值爲-vX爲正數,而回調出去的使用者利用的是Scroll,因此-vX
                            // 是合理的,爲正數的話,內容區域纔會scroll左邊的。
                        }
                    }
                }

                // Recycle Velocity Tracker
                if (null != mVelocityTracker) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                break;
            }
        }
        return true;
    }

版本 8 2點2 EclairGestureDetector

繼承:EclairGestureDetector extends CupcakeGestureDetector
處理了多手指情況下getActiveXY的值

    private int mActivePointerIndex = 0;
    float getActiveX(MotionEvent ev) {
        try {
            return ev.getX(mActivePointerIndex);
        } catch (Exception e) {
            return ev.getX();
        }
    }
    float getActiveY(MotionEvent ev) {
        try {
            return ev.getY(mActivePointerIndex);
        } catch (Exception e) {
            return ev.getY();
        }
    }

onTouchEvent事件,說明:
1 a手指按下觸發down,之後b手指按下沒有down,然後b手指擡起,觸發ACTION_POINTER_UP,接着a手指擡起觸發ACTION_UP;如果a手指先擡起,b後擡起,觸發順序一樣,最後還是ACTION_UP,當存在三個以上手指時,效果還是一樣的,即多次ACTION_POINTER_UP,最後是ACTION_UP。

2 pointerId的規則:當多個手指依次按下時,他們的順序編號爲0,1,2等等此時如果0擡起了,之後有按下一個手指,那麼最新按的那個手指編號是0,即編號是重複利用的,並且每次從0開始檢測,如果沒有手指使用,那麼就給當前手指了,否則就查找下一個編號。ID是按下時確定的,之後便不變了。

3 pointerIndex的規則:根據當前按下的手指ID大小排序,計算出pointerIndex的大小,比如按下了三個手指,之後中間那個1擡起了,那麼第三個手指的ID還是2,但是它的index變成了1. INDEX是手指個數發生變化時,根據ID排序重新計算出來的。比如如果第一個手指擡起了,那麼之後的所有手指的index都會減1.

    public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                // 當有多手指按下時,只會響應第一個手指按下,其餘不響應,並且只有手指全部擡起時纔會再次進入這裏
                mActivePointerId = ev.getPointerId(0);
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                // 最後一個手指擡起時會調用
                mActivePointerId = INVALID_POINTER_ID;
                break;
            case MotionEvent.ACTION_POINTER_UP:
                // deprecation 棄用,反對。註釋意思是說Compat裏面的過期變量的
                // Ignore deprecation, ACTION_POINTER_ID_MASK and
                // ACTION_POINTER_ID_SHIFT has same value and are deprecated
                // You can have either deprecation or lint target api warning
                // 根據action找到當前手指的index值
                final int pointerIndex = Compat.getPointerIndex(ev.getAction());
                // 根據index值找到當前手指的ID值
                final int pointerId = ev.getPointerId(pointerIndex);
                if (pointerId == mActivePointerId) { // 比較是否是設定的那個有效的ID手指,因爲ID是不變的
                    // This was our active pointer going up. Choose a new
                    // active pointer and adjust accordingly.
                    // pointerIndex的值肯定是從0開始的,因此如果當前是0,那麼就以下一個點作爲
                    // 基準,否則,以index爲0的點爲基準,而這個點有可能是第一個手指擡起後,最新
                    // 按下的那個手指,因爲ID是重複使用的,index是根據id排出來的。
                    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                    mActivePointerId = ev.getPointerId(newPointerIndex);
                    mLastTouchX = ev.getX(newPointerIndex);
                    mLastTouchY = ev.getY(newPointerIndex);
                }
                break;
        }
        mActivePointerIndex = ev.findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId: 0); // 根據id找index
        return super.onTouchEvent(ev); // 調用CupcakeGestureDetector的on方法
    }

版本 2點2 equal or more

onScale在此處回調了

public class FroyoGestureDetector extends EclairGestureDetector {
    // scale手勢檢測器,高版本就是好啊
    protected final ScaleGestureDetector mDetector;
    public FroyoGestureDetector(Context context) {
        super(context);
        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; // NaN:not a NO
                    // 回調,將手勢操作引起的縮放比例和中心點
                mListener.onScale(scaleFactor, detector.getFocusX(), detector.getFocusY());
                return true;
            }
            @Override
            public boolean onScaleBegin(ScaleGestureDetector detector) {
                return true;
            }
            @Override
            public void onScaleEnd(ScaleGestureDetector detector) {
                // NO-OP
            }
        };
        mDetector = new ScaleGestureDetector(context, mScaleListener);
    }
    @Override
    public boolean isScaling() {
        return mDetector.isInProgress();
    }
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        mDetector.onTouchEvent(ev);
        return super.onTouchEvent(ev);
    }
}

Compat類

    private static final int SIXTY_FPS_INTERVAL = 1000 / 60;
    public static void postOnAnimation(View view, Runnable runnable) {
        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
            postOnAnimationJellyBean(view, runnable);
        } else {
            view.postDelayed(runnable, SIXTY_FPS_INTERVAL);
        }
    }
    @TargetApi(16)
    private static void postOnAnimationJellyBean(View view, Runnable runnable) {
        view.postOnAnimation(runnable);
        //Causes the Runnable to execute on the next animation time step. The runnable will be run on the user interface thread.
    }
    // 上面手勢檢測使用的,根據action獲取index,根據系統調用不同方法
    public static int getPointerIndex(int action) {
        if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB)
            return getPointerIndexHoneyComb(action);
        else
            return getPointerIndexEclair(action);
    }
    @SuppressWarnings("deprecation")
    @TargetApi(Build.VERSION_CODES.ECLAIR)
    private static int getPointerIndexEclair(int action) {
        return (action & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT;
    }
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    private static int getPointerIndexHoneyComb(int action) {
        return (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
    }

註釋

@Deprecated
@param midScale medium scale preset
<p>&nbsp;</p>  // 空白區域
<p/>
{@link #setMediumScale(float mediumScale)}
{@link android.widget.ImageView.ScaleType}
<br/>
<p>This <strong>must</strong> be used if you need to set the page before
  the views are drawn on screen (e.g., default start page).</p>
   override的註釋
      /*
     * (non-Javadoc)
     *
     * @see android.view.View#onDraw(android.graphics.Canvas)
     */
    <pre>ssfsd</pre>防止被換行

PhotoView說明

繼承自Imageview,裏面包含了PhotoViewAttacher變量
以及
ScaleType mPendingScaleType;

注意

    // 覆蓋了Imageview的setScaleType,當在xml中配置了matrix後,下面代碼會先於PhotoView的初始化init()函數執行
    public void setScaleType(ScaleType scaleType) { 
        if (null != mAttacher) {
            mAttacher.setScaleType(scaleType);
        } else {
            mPendingScaleType = scaleType;
        }
    }
    // init中
    if (null != mPendingScaleType) {
            setScaleType(mPendingScaleType);
            mPendingScaleType = null;
    }
      // 繼而
     mAttacher.setScaleType(scaleType);
     // 接着
     throw new IllegalArgumentException(scaleType.name() + " is not supported in PhotoView");
     // 因此不要配置matrix屬性

    @Override
    protected void onDetachedFromWindow() { // 良好的習慣,不用時
        mAttacher.cleanup();
        super.onDetachedFromWindow();
    }
    @Override
    protected void onAttachedToWindow() {
        init();
        super.onAttachedToWindow();
    }     

而PhotoView的所有功能基本上由PhotoViewAttacher實現了。

發佈了20 篇原創文章 · 獲贊 5 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章