PhotoView
是一个用于处理图片手势的控件,其源码设计很不错,高内聚低耦合,值得我们深入学习下。
PhotoView
类代码很简单,看下构造就行了。
public PhotoView(Context context, AttributeSet attr, int defStyle) {
super(context, attr, defStyle);
init();
}
private void init() {
attacher = new PhotoViewAttacher(this);
//We always pose as a Matrix scale type, though we can change to another scale type
//via the attacher
super.setScaleType(ScaleType.MATRIX);
//apply the previously applied scale type
if (pendingScaleType != null) {
setScaleType(pendingScaleType);
pendingScaleType = null;
}
}
初始化了一个 PhotoViewAttacher
类,把 ScaleType
设置为 ScaleType.MATRIX
,因为 PhotoView
的手势操作都是通过设置 matrix
生效的。
PhotoView
的核心代码都在 PhotoViewAttacher
中,PhotoViewAttacther
可以看做是 PhotoView
的一个代理。先从 PhotoViewAttacher
的构造看起。
public PhotoViewAttacher(ImageView imageView) {
mImageView = imageView;
imageView.setOnTouchListener(this);
imageView.addOnLayoutChangeListener(this);
if (imageView.isInEditMode()) {
return;
}
mBaseRotation = 0.0f;
// Create Gesture Detectors...
mScaleDragDetector = new CustomGestureDetector(imageView.getContext(), onGestureListener);
mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() {
// forward long click listener
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
}
@Override
public boolean onDoubleTap(MotionEvent ev) {
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
}
});
}
构造函数的参数是一个 ImageView
,在这个类里主要用途是获取 ImageView
的边界,获取 drawable
,更新 ImageView
的矩阵,作为回调参数等。接着设置 **OnTouchListener**
,触摸处理都是在它的回调 boolean onTouch(View v, MotionEvent ev)
方法中做的, OnLayoutChangeListener
主要用于在外面布局发生变化的时候更新图片默认的矩阵。
这里说下 isInEditMode()
方法,这个方法是用于 Android Studio
布局编辑器预览的,在预览环境拿到的 context
是 com.android.layoutlib.bridge.android.BridgeContext
,这里面的方法获取到的一些对象是和 Android
系统环境不太一样的。还有在 RecyclerView
的源码中我们可以看到这么几行代码:
private void createLayoutManager(Context context, String className, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
ClassLoader classLoader;
if (isInEditMode()) {
// Stupid layoutlib cannot handle simple class loaders.
classLoader = this.getClass().getClassLoader();
} else {
classLoader = context.getClassLoader();
}
}
这里可以看到一句注释:Stupid layoutlib cannot handle simple class loaders.
,里面的 layoutlib
应该说的就是 BridgeContext
所在的包。
我们也可以在 onDraw
的时候根据这个方法来在预览环境和真实环境区别绘制。
回到 PhotoViewAttacher
的代码,这里判断 isInEditMode
后就直接返回了,可能是因为预览环境不需要监听触摸事件,也就不会走到相关的方法了。接着初始化了 CustomGestureDetector
类,这里传入了 OnGestureListener
,OnGestureListener
是个手势监听回调接口。
interface OnGestureListener {
void onDrag(float dx, float dy);
void onFling(float startX, float startY, float velocityX,
float velocityY);
void onScale(float scaleFactor, float focusX, float focusY);
void onScale(float scaleFactor, float focusX, float focusY, float dx, float dy);
}
接着初始化了 GestureDetector
,监听单击、双击、长按、fling
的回调。
我们重点看下 boolean onTouch(View v, MotionEvent ev)
方法。
@Override
public boolean onTouch(View v, MotionEvent ev) {
boolean handled = false;
if (mZoomEnabled && Util.hasDrawable((ImageView) v)) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
ViewParent parent = v.getParent();
// First, disable the Parent from intercepting the touch
// event
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
// If we're flinging, and the user presses down, cancel
// fling
cancelFling();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// If the user has zoomed less than min scale, zoom back
// to min scale
if (getScale() < mMinScale) {
RectF rect = getDisplayRect();
if (rect != null) {
v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
rect.centerX(), rect.centerY()));
handled = true;
}
} else if (getScale() > mMaxScale) {
RectF rect = getDisplayRect();
if (rect != null) {
v.post(new AnimatedZoomRunnable(getScale(), mMaxScale,
rect.centerX(), rect.centerY()));
handled = true;
}
}
break;
}
// Try the Scale/Drag detector
if (mScaleDragDetector != null) {
boolean wasScaling = mScaleDragDetector.isScaling();
boolean wasDragging = mScaleDragDetector.isDragging();
handled = mScaleDragDetector.onTouchEvent(ev);
boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling();
boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging();
mBlockParentIntercept = didntScale && didntDrag;
}
// Check to see if the user double tapped
if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) {
handled = true;
}
}
return handled;
}
mZoomEnabled
是外部可设置的属性,只有允许缩放并且 ImageView
有 drawable
的情况下才会处理手势操作。在 ACTION_DOWN
时取消 fling
,并且阻止父 View
拦截触摸事件,这里用了 parent.requestDisallowInterceptTouchEvent(true);
,requestDisallowInterceptTouchEvent(boolean disallowIntercept)
方法在自定义 View
的场景里还是用的挺多的。在 ACTION_CANCEL
、ACTION_UP
时校正缩放,把过度缩放的操作通过 Animation
拉回指定范围。
下面就把事件传递给 CustomGestureDetector
和 GestureDetector
了。
我们看下具体的手势监听部分。手势一般包括:双击、单击、长按、双指缩放、拖曳、惯性滚动(fling),对于单击、双击、长按、fling,PhotoView
使用了原生的 GestureDetector
来检测,而对于双指缩放、拖曳,则定义了一个 CustomGestureDetector
来处理,注意 CustomGestureDetector
也会处理 fling
事件。
我们主要看下 CustomGestureDetector
,这个类主要处理缩放和拖曳。缩放的检测使用了原生的 ScaleGestureDetector
来处理。ScaleGestureDetector
的构造方法需要传入一个 OnScaleGestureListener
用于回调缩放相关的值。
先看下 ScaleGestureDetector
的集成。首先在构造方法中定义好回调。
ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() {
private float lastFocusX, lastFocusY = 0;
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor))
return false;
if (scaleFactor >= 0) {
mListener.onScale(scaleFactor,
detector.getFocusX(),
detector.getFocusY(),
detector.getFocusX() - lastFocusX,
detector.getFocusY() - lastFocusY
);
lastFocusX = detector.getFocusX();
lastFocusY = detector.getFocusY();
}
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
lastFocusX = detector.getFocusX();
lastFocusY = detector.getFocusY();
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
// NO-OP
}
};
mDetector = new ScaleGestureDetector(context, mScaleListener);
这里逻辑很简单,定义了两个成员变量分别记录x轴和y轴的中心点,两次回调的中心点差值就是中心点移动的距离。scaleFactor
是缩放因子,相对于当前大小的缩放比例。
然后在 onTouchEvent
中,把 event
传给 ScaleGestureDetector
。
public boolean onTouchEvent(MotionEvent ev) {
try {
mDetector.onTouchEvent(ev);
return processTouchEvent(ev);
} catch (IllegalArgumentException e) {
// Fix for support lib bug, happening when onDestroy is called
return true;
}
}
这里有个 processTouchEvent
,拖曳就是在里面处理的。在看代码之前,我先讲下多点触控的基本知识。
触摸事件主要涉及到 MotionEvent
类,这个类存储了手指的移动状态,主要包含:
可以通过 getAction()
方法获取到一个动作,这里的返回值,对于单指而言,就是动作的状态,含义跟上面这些常量一样,但是如果是多指按下或者抬起,返回值是包含动作的索引的,多指的滑动返回值不包含索引,还是状态。
动作的状态和索引可以分开获取,getActionMasked()
可以只获取状态,getActionIndex()
可以只获取索引。
对于多指操作,要关注两个属性,触摸点id(PointerId)和索引(PointerIndex),触摸点索引可以通过刚刚说的 getActionIndex()
获取到,也可以通过 findPointerIndex(int pointerId)
获取到,触摸点id可以通过 getPointerId(int pointerIndex)
方法来获取,这个方法需要传入触摸点索引。值得注意的是 PointerId
和 PointerIndex
的取值。
_
PointerId
不变,PointerId
赋值后不会变更。PointerIndex
会更新,取值范围是0~触摸点个数-1。_
现在来看下代码,这里的 processTouchEvent
有些代码感觉多余了,下面代码是我改过的(不喜勿喷~)
先看下整体结构:
private boolean processTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_POINTER_UP:
break;
}
return true;
}
这里面的方法和常量上面都讲过了,这里主要讲下这些常量case下的常用操作。
再完整地看下代码,先看 ACTION_DOWN
的处理:
mVelocityTracker = VelocityTracker.obtain();
if (null != mVelocityTracker) {
mVelocityTracker.addMovement(ev);
}
mLastTouchX = ev.getX();
mLastTouchY = ev.getY();
mIsDragging = false;
首先,初始化 VelocityTracker
,VelocityTracker
是一个速度检测类,内部存了个 SynchronizedPool
,obtain()
方法会优先从池子里取 VelocityTracker
的实例,取不到再创建。addMovement
用于跟踪移动事件,一般会在 ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
中调用。然后记录事件的x、y坐标,获取事件坐标有两种方法,一种是无参数的 float getX()
,这个方法获取的是索引为0的点的坐标,一种是带参数的 float getX(int pointerIndex)
,这个需要传入索引值,用于多指操作,这里是第一个手指按下,我觉得使用无参数的就够了。
继续看 ACTION_MOVE
事件。
final float x = ev.getX();
final float y = ev.getY();
final float dx = x - mLastTouchX, dy = y - mLastTouchY;
if (!mIsDragging) {
// Use Pythagoras to see if drag length is larger than
// touch slop
mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;
}
if (mIsDragging) {
mListener.onDrag(dx, dy);
mLastTouchX = x;
mLastTouchY = y;
if (null != mVelocityTracker) {
mVelocityTracker.addMovement(ev);
}
}
首先得出移动距离dx、dy,这个距离用于拖动手势,刚刚 ACTION_DOWN
事件中把 mIsDragging
初始化为false,这里是否拖动的判断条件是滑动距离大于最小滑动距离,这里的最小滑动距离在构造函数中已经赋值了:
final ViewConfiguration configuration = ViewConfiguration.get(context);
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mTouchSlop = configuration.getScaledTouchSlop();
getScaledTouchSlop
是按根据设备密度(density)来获取的最小滑动距离,默认是 8dp
(
) 。
如果当前可以拖动,则会触发拖动回调,并且记录当前x、y坐标,给 VelocityTracker
添加事件。
注意回调 onFling 方法时速度加了负号,因为这个回调是给 OverScroller 用的,OverScroller 的坐标系(向左向上为正)跟正常的坐标系(向右向下为正)是反的。
继续看 ACTION_UP
。
if (mIsDragging) {
if (null != mVelocityTracker) {
mLastTouchX = ev.getX();
mLastTouchY = ev.getY();
// 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);
}
}
}
// Recycle Velocity Tracker
if (null != mVelocityTracker) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
这里主要处理松开手后的惯性滑动以及释放 VelocityTracker
,判断是否要惯性滑动,要看 x 轴和 y 轴的速度,VelocityTracker
在获取速度前要先调用 computeCurrentVelocity(int units)
计算速度,computeCurrentVelocity(int units)
方法的参数是单位,1表示1ms,1000表示1s,这个值决定了下面 getXVelocity()
和 getYVelocity()
的单位,如果传入的是1000,那速度单位就是 px/s
,只要 x 轴或者 y 轴有任何一个大于最小速度的,就会触发惯性滑动。这个最小速度跟上面的 TouchSlop
类似,也是从 ViewConfiguration
中获取的:
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
<dimen name="config_viewMinFlingVelocity">50dp</dimen>
默认值是50dp。
在 ACTION_UP
事件的最后,释放 VelocityTracker
。
继续看 ACTION_POINTER_UP
事件。
final int pointerIndex = ev.getActionIndex();
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
多指触摸抬起其中一个手指,因为 mLastTouchX
在之前一直存的是第一个手指的坐标,所以这里只要判断是不是第一个手指抬起,如果是第一个手指抬起,就更新到下一个手指的坐标。
至此核心的触摸事件捕获看完了,主要处理了拖动和惯性滑动。
在讲具体处理之前,先看下三个基本变量,mBaseMatrix
、mSuppMatrix
、mDrawMatrix
。
ScaleType
缩放移动到适应 ImageView
的变化,不记录手势操作ImageView
的矩阵,由 mBaseMatrix
和 mSuppMatrix
相乘得到
在给 ImageView
设置矩阵和获取边界时,是要用 mDrawMatrix
的。
缩放分为双击缩放和多指缩放。先看下双击缩放的回调处理。
PhotoView
定义了三档默认缩放大小,1.0f、1.75f、3.0f,分别对应 mMinScale
、mMidScale
、mMaxScale
,下面看下是怎么切换的。
@Override
public boolean onDoubleTap(MotionEvent ev) {
try {
float scale = getScale();
float x = ev.getX();
float y = ev.getY();
if (scale < getMediumScale()) {
setScale(getMediumScale(), x, y, true);
} else if (scale >= getMediumScale() && scale < getMaximumScale()) {
setScale(getMaximumScale(), x, y, true);
} else {
setScale(getMinimumScale(), x, y, true);
}
} catch (ArrayIndexOutOfBoundsException e) {
// Can sometimes happen when getX() and getY() is called
}
return true;
}
public float getScale() {
return (float) Math.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + (float) Math.pow
(getValue(mSuppMatrix, Matrix.MSKEW_Y), 2));
}
private float getValue(Matrix matrix, int whichValue) {
matrix.getValues(mMatrixValues);
return mMatrixValues[whichValue];
}
# Matrix类
public void getValues(float[] values) {
if (values.length < 9) {
throw new ArrayIndexOutOfBoundsException();
}
nGetValues(native_instance, values);
}
首先去拿了存在 mSuppMatrix
里的缩放比例,这里源码里的 getScale()
方法有个 bug ,Matrix.MSKEW_Y
应该换成 Matrix.MSCALE_Y
,注意 Matrix.getValues
方法可能会抛出 ArrayIndexOutOfBoundsException
异常,所以 onDoubleTap
中 catch
了一下。继续看 setScale
方法。
public void setScale(float scale) {
setScale(scale, false);
}
public void setScale(float scale, boolean animate) {
setScale(scale,
(mImageView.getRight()) / 2,
(mImageView.getBottom()) / 2,
animate
);
}
public void setScale(float scale, float focalX, float focalY, boolean animate) {
// Check to see if the scale is within bounds
if (scale < mMinScale || scale > mMaxScale) {
throw new IllegalArgumentException("Scale must be within the range of minScale and maxScale");
}
if (animate) {
mImageView.post(new AnimatedZoomRunnable(getScale(), scale, focalX, focalY));
} else {
mSuppMatrix.setScale(scale, scale, focalX, focalY);
checkAndDisplayMatrix();
}
}
这里缩放中心点取的是 ImageView
的中点,双击缩放走的是 AnimatedZoomRunnable
。
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() {
float t = interpolate();
float scale = mZoomStart + t * (mZoomEnd - mZoomStart);
float deltaScale = scale / getScale();
onGestureListener.onScale(deltaScale, mFocalX, mFocalY);
// We haven't hit our target scale yet, so post ourselves again
if (t < 1f) {
Compat.postOnAnimation(mImageView, this);
}
}
private float interpolate() {
float t = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration;
t = Math.min(1f, t);
t = mInterpolator.getInterpolation(t);
return t;
}
}
这个缩放动画主要用到了一个 AccelerateDecelerateInterpolator
,加减速插值器,根据当前动画执行时间占比去拿到当前的插值(0-1),再据此拿到对应的缩放比例,走 OnGestureListener
回调执行缩放。
@Override
public void onScale(float scaleFactor, float focusX, float focusY) {
onScale(scaleFactor, focusX, focusY, 0, 0);
}
@Override
public void onScale(float scaleFactor, float focusX, float focusY, float dx, float dy) {
if (getScale() < mMaxScale || scaleFactor < 1f) {
if (mScaleChangeListener != null) {
mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY);
}
mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
mSuppMatrix.postTranslate(dx, dy);
checkAndDisplayMatrix();
}
}
矩阵的相关知识在之前的文章讲过了,不懂的可以去复习下:Android图片处理一:Matrix与手势链接
这里重点看 checkAndDisplayMatrix()
。
private void checkAndDisplayMatrix() {
if (checkMatrixBounds()) {
setImageViewMatrix(getDrawMatrix());
}
}
private boolean checkMatrixBounds() {
final RectF rect = getDisplayRect(getDrawMatrix());
if (rect == null) {
return false;
}
final float height = rect.height(), width = rect.width();
float deltaX = 0, deltaY = 0;
final int viewHeight = getImageViewHeight(mImageView);
if (height <= viewHeight) {
switch (mScaleType) {
case FIT_START:
deltaY = -rect.top;
break;
case FIT_END:
deltaY = viewHeight - height - rect.top;
break;
default:
deltaY = (viewHeight - height) / 2 - rect.top;
break;
}
mVerticalScrollEdge = VERTICAL_EDGE_BOTH;
} else if (rect.top > 0) {
mVerticalScrollEdge = VERTICAL_EDGE_TOP;
deltaY = -rect.top;
} else if (rect.bottom < viewHeight) {
mVerticalScrollEdge = VERTICAL_EDGE_BOTTOM;
deltaY = viewHeight - rect.bottom;
} else {
mVerticalScrollEdge = VERTICAL_EDGE_NONE;
}
final int viewWidth = getImageViewWidth(mImageView);
if (width <= viewWidth) {
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;
}
mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH;
} else if (rect.left > 0) {
mHorizontalScrollEdge = HORIZONTAL_EDGE_LEFT;
deltaX = -rect.left;
} else if (rect.right < viewWidth) {
deltaX = viewWidth - rect.right;
mHorizontalScrollEdge = HORIZONTAL_EDGE_RIGHT;
} else {
mHorizontalScrollEdge = HORIZONTAL_EDGE_NONE;
}
// Finally actually translate the matrix
mSuppMatrix.postTranslate(deltaX, deltaY);
return true;
}
private RectF getDisplayRect(Matrix matrix) {
Drawable d = mImageView.getDrawable();
if (d != null) {
mDisplayRect.set(0, 0, d.getIntrinsicWidth(),
d.getIntrinsicHeight());
matrix.mapRect(mDisplayRect);
return mDisplayRect;
}
return null;
}
checkMatrixBounds
算是这个类的一个核心方法了,用于矫正偏差。getDisplayRect
会对当前的 drawable
边界执行变换,变换矩阵就是前文说的 mDrawMatrix
,拿到了显示区域后会跟 ImageView
区域对比,把超出边界的部分拉回去,拉回去的位置会参考 ScaleType
,这里只有位移变换,mHorizontalScrollEdge
、mVerticalScrollEdge
主要记录当前的手势操作 matrix
需要往哪个边界矫正。
多指缩放最后回调的也是 onScale
方法,处理跟上面一样。
@Override
public void onDrag(float dx, float dy) {
if (mScaleDragDetector.isScaling()) {
return; // Do not drag if we are already scaling
}
if (mOnViewDragListener != null) {
mOnViewDragListener.onDrag(dx, dy);
}
mSuppMatrix.postTranslate(dx, dy);
checkAndDisplayMatrix();
/*
* Here we decide whether to let the ImageView's parent to start taking
* over the touch event.
*
* First we check whether this function is enabled. We never want the
* parent to take over if we're scaling. We then check the edge we're
* on, and the direction of the scroll (i.e. if we're pulling against
* the edge, aka 'overscrolling', let the parent take over).
*/
ViewParent parent = mImageView.getParent();
if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) {
if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH
|| (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 1f)
|| (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -1f)
|| (mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f)
|| (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -1f)) {
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(false);
}
}
} else {
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
}
前面主要是移动,矫正边界, checkAndDisplayMatrix
走完会对 mHorizontalScrollEdge
、mVerticalScrollEdge
赋值,如果矫正边界了并且位移是往超出边界的方向就会触发请求父布局拦截事件,不再传递下来。
@Override
public void onFling(float startX, float startY, float velocityX, float velocityY) {
mCurrentFlingRunnable = new FlingRunnable(mImageView.getContext());
mCurrentFlingRunnable.fling(getImageViewWidth(mImageView),
getImageViewHeight(mImageView), (int) velocityX, (int) velocityY);
mImageView.post(mCurrentFlingRunnable);
}
跟双击缩放类似,这里定义了一个 FlingRunnable
,AnimatedZoomRunnable
的插值依赖了一个加减速插值器, FlingRunnable
则依赖了一个 OverScroller
类,滚动时其内部位置的更新其实也是借助了插值器实现,插值器是个内部类 ViscousFluidInterpolator
,变化图形我写了个demo演示。
这里 fling
没有用到这个插值器, FlingRunnable
的 fling
方法其实是调用了 OverScroller
的 fling
方法。整个流程是:fling调用后,OverScroller
会记录一个当前时间,后面调用 computeScrollOffset
时,会计算出时间差,根据时间差计算出当前速度和滑动距离,记录当前位置。这里看下 run
方法。
@Override
public void run() {
if (mScroller.isFinished()) {
return; // remaining post that should not be handled
}
if (mScroller.computeScrollOffset()) {
final int newX = mScroller.getCurrX();
final int newY = mScroller.getCurrY();
mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY);
checkAndDisplayMatrix();
mCurrentX = newX;
mCurrentY = newY;
// Post On animation
Compat.postOnAnimation(mImageView, this);
}
}
首先调用 computeScrollOffset()
更新当前的位置,后面就可以使用 getCurrX()
和 getCurrY()
来获取位置了。然后更新到 mSuppMatrix
上并展示,不断触发这个 Runnable
直到 fling 停止。
mBaseMatrix
的更新在 updateBaseMatrix(Drawable drawable)
方法中,主要是根据 ScaleType
来调整 Drawable
的缩放和移动,有了前面的详细讲解,这里应该很容易看懂,就不细说了。
至此,PhotoView
的核心逻辑都分析完了。