簡介
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> </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實現了。