简介
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实现了。