Android图片处理二:PhotoView源码解析
PhotoView
是一个用于处理图片手势的控件,其源码设计很不错,高内聚低耦合,值得我们深入学习下。
1 基本结构
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 attachersuper.setScaleType(ScaleType.MATRIX);//apply the previously applied scale typeif (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@Overridepublic void onLongPress(MotionEvent e) {}@Overridepublic boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {}@Overridepublic boolean onSingleTapConfirmed(MotionEvent e) {}@Overridepublic boolean onDoubleTap(MotionEvent ev) {}@Overridepublic 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// eventif (parent != null) {parent.requestDisallowInterceptTouchEvent(true);}// If we're flinging, and the user presses down, cancel// flingcancelFling();break;case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:// If the user has zoomed less than min scale, zoom back// to min scaleif (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 detectorif (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 tappedif (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
了。
2 手势监听
我们看下具体的手势监听部分。手势一般包括:双击、单击、长按、双指缩放、拖曳、惯性滚动(fling),对于单击、双击、长按、fling,PhotoView
使用了原生的 GestureDetector
来检测,而对于双指缩放、拖曳,则定义了一个 CustomGestureDetector
来处理,注意 CustomGestureDetector
也会处理 fling
事件。
我们主要看下 CustomGestureDetector
,这个类主要处理缩放和拖曳。缩放的检测使用了原生的 ScaleGestureDetector
来处理。ScaleGestureDetector
的构造方法需要传入一个 OnScaleGestureListener
用于回调缩放相关的值。
先看下 ScaleGestureDetector
的集成。首先在构造方法中定义好回调。
ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() {private float lastFocusX, lastFocusY = 0;@Overridepublic 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;}@Overridepublic boolean onScaleBegin(ScaleGestureDetector detector) {lastFocusX = detector.getFocusX();lastFocusY = detector.getFocusY();return true;}@Overridepublic 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 calledreturn true;}
}
这里有个 processTouchEvent
,拖曳就是在里面处理的。在看代码之前,我先讲下多点触控的基本知识。
触摸事件主要涉及到 MotionEvent
类,这个类存储了手指的移动状态,主要包含:
- ACTION_DOWN 第一个手指按下
- ACTION_POINTER_DOWN 第一个手指按下后其他手指按下
- ACTION_POINTER_UP 多个手指长按时抬起其中一个手指,注意松开后还有手指在屏幕上
- ACTION_UP 最后一个手指抬起
- ACTION_MOVE 手指移动
- ACTION_CANCEL 父View收到ACTION_DOWN后会把事件传给子View,如果后续的ACTION_MOVE和ACTION_UP等事件被父View拦截掉,那子View就会收到ACTION_CANCEL事件
可以通过 getAction()
方法获取到一个动作,这里的返回值,对于单指而言,就是动作的状态,含义跟上面这些常量一样,但是如果是多指按下或者抬起,返回值是包含动作的索引的,多指的滑动返回值不包含索引,还是状态。
动作的状态和索引可以分开获取,getActionMasked()
可以只获取状态,getActionIndex()
可以只获取索引。
对于多指操作,要关注两个属性,触摸点id(PointerId)和索引(PointerIndex),触摸点索引可以通过刚刚说的 getActionIndex()
获取到,也可以通过 findPointerIndex(int pointerId)
获取到,触摸点id可以通过 getPointerId(int pointerIndex)
方法来获取,这个方法需要传入触摸点索引。值得注意的是 PointerId
和 PointerIndex
的取值。
_
- PointerId 手指按下时生成,手指抬起时回收,注意多点触摸时,抬起任何一个手指,其他手指的
PointerId
不变,PointerId
赋值后不会变更。 - PointerIndex 手指按下时生成,从0开始计数,多点触摸抬起其中一个手指时,后面的手指
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 一般会记录触摸点位置,初始化一些变量。
- ACTION_MOVE 一般会获取当前触摸点位置,跟上次记录的位置取差值,进行缩放、拖动等操作。
- ACTION_CANCEL 事件中断,重置状态。
- ACTION_UP 重置状态,如果这时处于拖动的状态,会判断滑动的速度,速度超过一定的值会触发惯性滑动。
- ACTION_POINTER_UP 多指中的一个手指抬起,需要更新参考触摸点的位置。
再完整地看下代码,先看 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 slopmIsDragging = 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
(<dimen name="config_viewConfigurationTouchSlop">8dp</dimen>
) 。
如果当前可以拖动,则会触发拖动回调,并且记录当前x、y坐标,给 VelocityTracker
添加事件。
注意回调 onFling 方法时速度加了负号,因为这个回调是给 OverScroller 用的,OverScroller 的坐标系(向左向上为正)跟正常的坐标系(向右向下为正)是反的。
继续看 ACTION_UP
。
if (mIsDragging) {if (null != mVelocityTracker) {mLastTouchX = ev.getX();mLastTouchY = ev.getY();// Compute velocity within the last 1000msmVelocityTracker.addMovement(ev);mVelocityTracker.computeCurrentVelocity(1000);final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker.getYVelocity();// If the velocity is greater than minVelocity, call// listenerif (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
在之前一直存的是第一个手指的坐标,所以这里只要判断是不是第一个手指抬起,如果是第一个手指抬起,就更新到下一个手指的坐标。
至此核心的触摸事件捕获看完了,主要处理了拖动和惯性滑动。
2 手势处理
在讲具体处理之前,先看下三个基本变量,mBaseMatrix
、mSuppMatrix
、mDrawMatrix
。
- mBaseMatrix 基础矩阵,记录的是图片根据
ScaleType
缩放移动到适应ImageView
的变化,不记录手势操作 - mSuppMatrix 额外矩阵,记录的是手势操作
- mDrawMatrix 实际设置给
ImageView
的矩阵,由mBaseMatrix
和mSuppMatrix
相乘得到
在给 ImageView
设置矩阵和获取边界时,是要用 mDrawMatrix
的。
2.1 缩放
缩放分为双击缩放和多指缩放。先看下双击缩放的回调处理。
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 boundsif (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;}@Overridepublic 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 againif (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 matrixmSuppMatrix.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
方法,处理跟上面一样。
2.2 拖动
@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
赋值,如果矫正边界了并且位移是往超出边界的方向就会触发请求父布局拦截事件,不再传递下来。
2.3 Fling
@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 animationCompat.postOnAnimation(mImageView, this);}
}
首先调用 computeScrollOffset()
更新当前的位置,后面就可以使用 getCurrX()
和 getCurrY()
来获取位置了。然后更新到 mSuppMatrix
上并展示,不断触发这个 Runnable
直到 fling 停止。
mBaseMatrix
的更新在 updateBaseMatrix(Drawable drawable)
方法中,主要是根据 ScaleType
来调整 Drawable
的缩放和移动,有了前面的详细讲解,这里应该很容易看懂,就不细说了。
至此,PhotoView
的核心逻辑都分析完了。
Android图片处理二:PhotoView源码解析相关推荐
- Android Glide图片加载框架(二)源码解析之into()
文章目录 一.前言 二.源码解析 1.into(ImageView) 2.GlideContext.buildImageViewTarget() 3.RequestBuilder.into(Targe ...
- Android Glide图片加载框架(二)源码解析之load()
文章目录 一.前言 二.源码分析 1.load() Android Glide图片加载框架系列文章 Android Glide图片加载框架(一)基本用法 Android Glide图片加载框架(二)源 ...
- Android Glide图片加载框架(二)源码解析之with()
文章目录 一.前言 二.如何阅读源码 三.源码解析 1.with() Android Glide图片加载框架系列文章 Android Glide图片加载框架(一)基本用法 Android Glide图 ...
- Android Glide 3.7.0 源码解析(八) , RecyclableBufferedInputStream 的 mark/reset 实现
个人博客传送门 一.mark / reset 的作用 Android Glide 3.7.0 源码解析(七) , 细说图形变换和解码有提到过RecyclableBufferedInputStream ...
- Android多线程之ArrayBlockingQueue源码解析
阻塞队列系列 Android多线程之LinkedBlockingQueue源码解析 Android多线程之SynchronousQueue源码解析 Andorid多线程之DelayQueue源码分析 ...
- Android多线程之IntentService源码解析
想要了解 IntentService 的工作原理需要先对 Android 系统中以 Handler.Looper.MessageQueue 组成的异步消息处理机制以及 HandlerThread 有所 ...
- 【Android 控件使用及源码解析】 GridView规则显示图片仿微信朋友圈发图片
今天闲下来想用心写一点东西,发现没什么可写的,就写一下最近项目上用到的一些东西吧.最近项目要求上传多图并且多图显示,而且要规则的显示,就像微信朋友圈的图片显示一样. 想了一下用GridView再适合不 ...
- 【Android应用开发】EasyDialog 源码解析
示例源码下载 : http://download.csdn.net/detail/han1202012/9115227 EasyDialog 简介 : -- 作用 : 用于在界面进行一些介绍, 说明; ...
- Volley 图片加载相关源码解析
转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/47721631: 本文出自:[张鸿洋的博客] 一 概述 最近在完善图片加载方面的 ...
- Android 图片裁剪 (附源码)
Android 图片裁剪 前言 正文 一.创建并配置项目 二.权限申请 三.获取图片Uri 四.图片裁剪 五.源码 尾声 运行效果图 前言 图片裁剪是对图片进行区域选定,然后裁剪选定的区域,形成一 ...
最新文章
- jQuery选择器之动态列表显示Demo
- C# 入门经典 第三版 下载。
- 2018python培训-参加python培训要多少钱?
- 当你和你女朋友闹矛盾时......
- AuthenticationManager验证原理分析
- pythonqt项目_python GUI编程 QT5开发项目实战
- php逻辑分析,PHP – 字符串逻辑分析 – “X和Y或Z”
- 使用中值滤波器对图像降噪
- 高斯赛德尔潮流计算c语言编程,高斯赛德尔法潮流计算
- 敏捷与CMMI的同与不同
- 边界路由linux,路由表构成简介(Destination/Gateway/Genmask/Iface)
- 为什么我们会有假期一结束,快乐就终止的感觉?
- unit怎么发音_unitl是什么意思
- 一件程序猿T恤的故事
- JAVA 之POI导入批量新增、批量检查、日志记录、失败原因、失败条数、数据库映射
- 非常全的 matlab 函数
- OpenLayers应用一(转自http://www.cnblogs.com/lzlynn/)
- EPICS -- areaDetector URL驱动程序
- C#中跳过循环continue与break
- JAVA Spring Security对接QQ快速登录(web应用)