开源库地址:https://github.com/chrisbanes/PhotoView
PhotoView是一个用来帮助开发者轻松实现ImageView缩放的库。开发者可以轻易控制对图片的缩放旋等等操作。
PhotoView的使用极其简单,而且提供了两种方案。可以使用普通的ImageView,也可以使用该库中提供的ImageView(PhotoView)。
- 使用PhotoView
只需如下引用该库中的ImageView,无需关心其它实现细节,你的ImageView便可拥有缩放效果。
<uk.co.senab.photoview.PhotoView
android:id="@+id/iv_photo"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
- 针对普通ImageView
有的时候,可能因为一些历史原因,使得你不得不用原来的ImageView。幸运的是该库也提供了一种解决方案。只需用PhotoViewAttacher包装即可。
PhotoViewAttacher mAttacher=new PhotoViewAttacher(mImageView);//用PhotoViewAttacher包装
mAttacher.update();//当图片改变时需调用update();
mAttacher.cleanup();//当ImageView不再使用时回收资源(可在onDestory中 调用)。PhotoView已经实现了这个功能不需要自己管理。
PhotoView真的很神奇,接下来我们去源码里一探究竟吧。顺便多说一句,图片的缩放大量运用到了Matrix相关知识,不了解的务必要先查阅相关资料哦。强烈推荐Android Matrix 这篇文章,当然也可以看我的这篇Android Matrix矩阵详解。
源码解读
这次源码解读我们从使用普通ImageView入手,普通的ImageView如果想缩放,必须依赖于PhotoViewAttacher,而PhotoViewAttacher又实现了IPhotoView接口。IPhotoView主要定义了一些常用的操作和默认值,由于方法实在太多了,就不一一列举了,直接上图。
IPhotoView定义的所有抽象方法如下。
IPhotoView的部分源码如下。
public interface IPhotoView {
float DEFAULT_MAX_SCALE = 3.0f;//默认最大缩放倍数为3倍
float DEFAULT_MID_SCALE = 1.75f;//默认中间缩放倍数为1.75倍
float DEFAULT_MIN_SCALE = 1.0f;//默认最小缩放倍数为1倍
int DEFAULT_ZOOM_DURATION = 200;//默认的缩放间隔为200ms
boolean canZoom();//可以缩放
RectF getDisplayRect();//获取显示矩形
boolean setDisplayMatrix(Matrix finalMatrix);//设置显示矩阵
Matrix getDisplayMatrix();//获取显示矩阵
//..
//省略了部分源码
介绍完IPhotoView接口后,现在改来看看PhotoViewAttacher了,PhotoViewAttacher的属性也比较多,如下:
private Interpolator mInterpolator = new AccelerateDecelerateInterpolator();//插值器,用于缩放动画
int ZOOM_DURATION = DEFAULT_ZOOM_DURATION;//默认的缩放间隔
static final int EDGE_NONE = -1;//图片两边都不在边缘内
static final int EDGE_LEFT = 0;//图片左边显示在View的左边缘内
static final int EDGE_RIGHT = 1;//图片右边显示在View的右边缘内
static final int EDGE_BOTH = 2;//图片两边都在边缘内
static int SINGLE_TOUCH = 1;//单指
private float mMinScale = DEFAULT_MIN_SCALE;//最小缩放倍数
private float mMidScale = DEFAULT_MID_SCALE;//中间缩放倍数
private float mMaxScale = DEFAULT_MAX_SCALE;//最大缩放倍数
private boolean mAllowParentInterceptOnEdge = true;//当在边缘操作时,允许父布局拦截事件。
private boolean mBlockParentIntercept = false;//阻止父布局拦截事件
private WeakReference<ImageView> mImageView;//弱引用
//手势探测器
private GestureDetector mGestureDetector;//单击,长按,Fling
private uk.co.senab.photoview.gestures.GestureDetector mScaleDragDetector;//缩放和拖拽
private final Matrix mBaseMatrix = new Matrix();//基础矩阵,用来保存初始的显示矩阵
private final Matrix mDrawMatrix = new Matrix();//绘画矩阵,用来计算最后显示区域的矩阵,是在mBaseMatrix和mSuppMatrix的基础上计算出来的。
private final Matrix mSuppMatrix = new Matrix();//这个矩阵我也不知道怎么称呼,也不知道是不是Supply的意思,暂且叫作供应矩阵吧,用来保存旋转平移和缩放的矩阵。
private final RectF mDisplayRect = new RectF();//显示矩形
private final float[] mMatrixValues = new float[9];//用来保存矩阵的值。3*3
// 各类监听
private OnMatrixChangedListener mMatrixChangeListener;
private OnPhotoTapListener mPhotoTapListener;
private OnViewTapListener mViewTapListener;
private OnLongClickListener mLongClickListener;
private OnScaleChangeListener mScaleChangeListener;
private OnSingleFlingListener mSingleFlingListener;
//保存ImageView的top,right,bottom,left
private int mIvTop, mIvRight, mIvBottom, mIvLeft;
//Fling时的Runable
private FlingRunnable mCurrentFlingRunnable;
private int mScrollEdge = EDGE_BOTH;//两边边缘
private float mBaseRotation;//基础旋转角度
private boolean mZoomEnabled;//是否可以缩放
private ScaleType mScaleType = ScaleType.FIT_CENTER;//默认缩放类型
此外PhotoViewAttacher中还定义了以下几个接口。
public interface OnMatrixChangedListener {
/**
* 当用来显示Drawable的Matrix改变时回调
* @param rect - 显示Drawable的新边界
*/
void onMatrixChanged(RectF rect);
}
public interface OnScaleChangeListener {
/**
* 当ImageView改变缩放时回调
*
* @param scaleFactor 小于1表示缩小,大于1表示放大
* @param focusX 缩放焦点X
* @param focusY 缩放焦点Y
*/
void onScaleChange(float scaleFactor, float focusX, float focusY);
}
public interface OnPhotoTapListener {
/**
*
*当用户敲击在照片上时回调,如果在空白区域不会回调
* @param view - ImageView
* @param x -用户敲击的位置(在图片中从左往右的位置)占图片宽度的百分比
* @param y -用户敲击的位置(在图片中从上往下的位置)占图片高度的百分比
*/
void onPhotoTap(View view, float x, float y);
/**
* 在图片外部的空白区域敲击回调
* */
void onOutsidePhotoTap();
}
public interface OnViewTapListener {
/**
* 只要用户敲击ImageView就会回调,不管是不是在图片上。
* @param view - View the user tapped.
* @param x -敲击View的x坐标
* @param y -敲击View的y坐标
*/
void onViewTap(View view, float x, float y);
}
public interface OnSingleFlingListener {
/**
* 用户使用单指在ImageView上快速滑动时回调,不管是不是在图片上。
* @param e1 - 第一次触摸事件
* @param e2 - 第二次触摸事件
* @param velocityX - 水平滑过的速度.
* @param velocityY - 竖直滑过的素组.
*/
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}
在看完PhotoViewAttacher的一些属性和接口外,现在就来看PhotoViewAttacher的构造方法。即new PhotoViewAttacher(mImageView)
这一句。
public PhotoViewAttacher(ImageView imageView) {
this(imageView, true);
}
public PhotoViewAttacher(ImageView imageView, boolean zoomable) {
mImageView = new WeakReference<>(imageView);//弱引用
imageView.setDrawingCacheEnabled(true);//开启绘制缓存区,用于获取可见区的bitmap
imageView.setOnTouchListener(this);//设置Touch监听,用于添加手势监听
ViewTreeObserver observer = imageView.getViewTreeObserver();
if (null != observer)
observer.addOnGlobalLayoutListener(this);//用于监听ImageView的大小
// 确保ImageView的ScaleType为Matrix
setImageViewScaleTypeMatrix(imageView);
if (imageView.isInEditMode()) {
return;
}
//初始化多指缩放/拖拽手势探测器
mScaleDragDetector = VersionedGestureDetector.newInstance(
imageView.getContext(), this);
//初始化其它手势监听(长按,Fling)
mGestureDetector = new GestureDetector(imageView.getContext(),
new GestureDetector.SimpleOnGestureListener() {
//长按
@Override
public void onLongPress(MotionEvent e) {
if (null != mLongClickListener) {
mLongClickListener.onLongClick(getImageView());
}
}
//Fling
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
if (mSingleFlingListener != null) {
if (getScale() > DEFAULT_MIN_SCALE) {
return false;
}
if (MotionEventCompat.getPointerCount(e1) > SINGLE_TOUCH
|| MotionEventCompat.getPointerCount(e2) > SINGLE_TOUCH) {
return false;
}
return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY);
}
return false;
}
});
//设置默认的双击处理方案。
mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this));
//基础旋转角度
mBaseRotation = 0.0f;
//设置是否可缩放
setZoomable(zoomable);
}
构造方法主要做了一些初始化工作,比如添加了手势监听(双指缩放,拖拽,双击,长按)等等。而且,如果希望图片具备缩放功能,还得设置ImageView的scaleType为matrix,下面我们就一步步剖析。
默认设置
为了理解起来更连贯一点,我们先看setZoomable
中的源码。
@Override
public void setZoomable(boolean zoomable) {
mZoomEnabled = zoomable;
update();
}
public void update() {
ImageView imageView = getImageView();//获取ImageView
if (null != imageView) {
if (mZoomEnabled) {
//再次确保ImageView的ScaleType为MATRIX
setImageViewScaleTypeMatrix(imageView);
//更新基础矩阵mBaseMatrix
updateBaseMatrix(imageView.getDrawable());
} else {
//重置矩阵
resetMatrix();
}
}
}
可以看出,除了赋值mZoomEnabled外,还调用了update()
方法,前面我们说了,每次更换图片时需调用update()
刷新。在update()
中,如果是可缩放的,就更新mBaseMatrix
,否则重置矩阵。
updateBaseMatrix
的源码如下:
private void updateBaseMatrix(Drawable d) {
ImageView imageView = getImageView();//获取ImageView
if (null == imageView || null == d) {
return;
}
//获取ImageView的宽高
final float viewWidth = getImageViewWidth(imageView);
final float viewHeight = getImageViewHeight(imageView);
//获取Drawable的固有的宽高
final int drawableWidth = d.getIntrinsicWidth();
final int drawableHeight = d.getIntrinsicHeight();
mBaseMatrix.reset();//重置mBaseMatrix矩阵
//获取宽的缩放比,drawableWidth * widthScale = viewWidth
final float widthScale = viewWidth / drawableWidth;
//获取高的缩放比,drawableHeight * heightScale = viewHeight
final float heightScale = viewHeight / drawableHeight;
//注意,这里的ScaleType不是ImageView的ScaleType,因为ImageView的ScaleType已被强制设为Matrix。这里的ScaleType是PhotoViewAttacher的ScaleType,因此可以通过设置PhotoViewAttacher的setScaleType来模拟原ImageView的效果,以满足实际需求。
if (mScaleType == ScaleType.CENTER) {//如果缩放类型为ScaleType.CENTER
//基础矩阵就平移两者的宽度差一半,以保持居中
mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F,
(viewHeight - drawableHeight) / 2F);
} else if (mScaleType == ScaleType.CENTER_CROP) {//如果缩放类型为ScaleType.CENTER_CROP
float scale = Math.max(widthScale, heightScale);//取最大值
mBaseMatrix.postScale(scale, scale);//使最小的那一边也缩放到View的尺寸
//平移到中间
mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
(viewHeight - drawableHeight * scale) / 2F);
} else if (mScaleType == ScaleType.CENTER_INSIDE) {
//如果缩放类型为ScaleType.CENTER_INSIDE
//计算缩放值
float scale = Math.min(1.0f, Math.min(widthScale, heightScale));
//当图片宽高超出View宽高时调用,否则缩放还是1
mBaseMatrix.postScale(scale, scale);
//平移到中间
mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
(viewHeight - drawableHeight * scale) / 2F);//平移
} else {
//如果是FIT_XX相关的缩放类型
RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight);
RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight);
if ((int) mBaseRotation % 180 != 0) {
mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth);
}
//直接根据Matrix提供的setRectToRect来设置
switch (mScaleType) {
case FIT_CENTER:
mBaseMatrix
.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER);
break;
case FIT_START:
mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START);
break;
case FIT_END:
mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END);
break;
case FIT_XY:
mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL);
break;
default:
break;
}
}
//重置矩阵
resetMatrix();
}
可以看出updateBaseMatrix
,主要是在根据ScaleType来调整显示位置和缩放级别,使其达到ImageView的ScaleType效果。为什么需要这个功能?由于ImageView已被强制设置ScaleType为Matrix,但是如果我们仍然需要ScaleType的显示效果怎么办?于是PhotoViewAttacher提供了setScaleType来模拟相关效果。从上面的源码应该不难看出,mBaseMatrix
用来保存根据ScaleType调整过的的原始矩阵。默认的ScaleType为ScaleType.FIT_CENTER。
接下来,我们来看resetMatrix()
。
private void resetMatrix() {
mSuppMatrix.reset();//重置供应矩阵
setRotationBy(mBaseRotation);//设置初始的旋转角度
setImageViewMatrix(getDrawMatrix());//把最mDrawMatrix设置给ImageView,以对图片进行变化。
checkMatrixBounds();//检查Matrix边界
}
设置旋转角度的源码如下,mSuppMatrix后乘了旋转角度。然后进行检查边界,最后进行显示。
public void setRotationBy(float degrees) {
mSuppMatrix.postRotate(degrees % 360);//后乘旋转角度
checkAndDisplayMatrix();//检查Matrix边界,然后显示
}
//检查Matrix边界和显示
private void checkAndDisplayMatrix() {
if (checkMatrixBounds()) {
//调整效果进行显示
setImageViewMatrix(getDrawMatrix());
}
}
checkMatrixBounds()
用来检查Matrix边界。相关源码如下。
private boolean checkMatrixBounds() {
final ImageView imageView = getImageView();
if (null == imageView) {
return false;
}
//获取最终的显示区域矩形
final RectF rect = getDisplayRect(getDrawMatrix());
if (null == rect) {
return false;
}
//获取显示矩形的宽高
final float height = rect.height(), width = rect.width();
float deltaX = 0, deltaY = 0;//计算调整边界时要平移的距离
//以下根据缩放类型来调整显示区域
final int viewHeight = getImageViewHeight(imageView);//获取View的高
if (height <= viewHeight) {//如果图片的高小于等于View,说明图片的垂直方向可以完全显示在View里面
//于是根据缩放类型进行边界调整
switch (mScaleType) {
case FIT_START:
deltaY = -rect.top;//向上移动到View的顶部
break;
case FIT_END:
deltaY = viewHeight - height - rect.top;//向下移动到View的底部
break;
default:
deltaY = (viewHeight - height) / 2 - rect.top;//否则就居中显示
break;
}
} else if (rect.top > 0) {
//如果图片高度超出来View的高,但是rect.top > 0说明ImageView上边还有空余的区域。
deltaY = -rect.top;//于是计算偏移距离
} else if (rect.bottom < viewHeight) {
//同理。底部也有空余
deltaY = viewHeight - rect.bottom;
}
//获取ImageView的宽,同理进行边界调整。
final int viewWidth = getImageViewWidth(imageView);
if (width <= viewWidth) {//如果宽度小于View的宽,进行相应调整
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;
}
mScrollEdge = EDGE_BOTH;//图片宽度小于View的宽度,说明两边显示在边缘内
} else if (rect.left > 0) {
mScrollEdge = EDGE_LEFT;//rect.left > 0表示显示在左边边缘内
deltaX = -rect.left;
} else if (rect.right < viewWidth) {
deltaX = viewWidth - rect.right;
mScrollEdge = EDGE_RIGHT;//右边在边缘内
} else {
mScrollEdge = EDGE_NONE;//两边都不在边缘内
}
//最后,将平移给mSuppMatrix
mSuppMatrix.postTranslate(deltaX, deltaY);
return true;
}
为什么要检查边界呢?那是因为当你进行旋转或缩放变换后,由于缩放的锚点是以手指为中心的,有时候会发现显示的区域不对,比如说,当图片大于View的宽高时,但是矩阵的边界与View之间居然还有空白区,显然不太合理。这时需要进行平移对齐View的宽高。
在检查显示边界时,我们需要获取图片的显示矩形,那么怎么获取Drawable的最终显示矩形呢?
getDrawMatrix()
用来获取mDrawMatrix最终矩阵,mDrawMatrix其实是在mBaseMatrix基础矩阵上后乘mSuppMatrix供应矩阵产生的。
public Matrix getDrawMatrix() {
mDrawMatrix.set(mBaseMatrix);
mDrawMatrix.postConcat(mSuppMatrix);
return mDrawMatrix;
}
通过setImageViewMatrix
将最终的矩阵应用到ImageView中,这时我们就能看到显示效果了。
private void setImageViewMatrix(Matrix matrix) {
ImageView imageView = getImageView();//获取ImageView
if (null != imageView) {
checkImageViewScaleType();//检查缩放类型,必须为Matrix,否则抛异常
imageView.setImageMatrix(matrix);//应用矩阵
//回调监听
if (null != mMatrixChangeListener) {
RectF displayRect = getDisplayRect(matrix);//获取显示矩形
if (null != displayRect) {
mMatrixChangeListener.onMatrixChanged(displayRect);
}
}
}
}
此外,通过如下的源码可以获取显示矩形,matrix.mapRect
用来映射最新的变换到原始的矩形。
private RectF getDisplayRect(Matrix matrix) {
ImageView imageView = getImageView();
if (null != imageView) {
Drawable d = imageView.getDrawable();
if (null != d) {
mDisplayRect.set(0, 0, d.getIntrinsicWidth(),
d.getIntrinsicHeight());//获取Drawable尺寸,初始化原始矩形
matrix.mapRect(mDisplayRect);//将矩阵的变换映射给mDisplayRect,得到最终矩形
return mDisplayRect;
}
}
return null;
}
看完以上的源码,相信流程已经非常清楚了,当设置图片时,通过update()
我们可以初始化一个mBaseMatrix,然后如果想缩放、旋转等,进行设置应用到mSuppMatrix,最终通过对mBaseMatrix和mSuppMatrix计算得到mDrawMatrix,然后应用到ImageView中,便完成了我们的使命了。
既然一切的变换都会应用到mSuppMatrix中。那么接下来我们回到PhotoViewAttacher
的构造方法中继续阅读其他源码,以了解这个过程到底是怎么实现的。
Touch事件监听
Touch事件中,主要让手势探测器进行处理事件。核心源码如下。
public boolean onTouch(View v, MotionEvent ev) {
boolean handled = false;
//可以缩放且有图片时才能处理手势监听
if (mZoomEnabled && hasDrawable((ImageView) v)) {
ViewParent parent = v.getParent();
switch (ev.getAction()) {
case ACTION_DOWN:
if (null != parent) {
//不允许父布局拦截ACTION_DOWN事件
parent.requestDisallowInterceptTouchEvent(true);
} else {
LogManager.getLogger().i(LOG_TAG, "onTouch getParent() returned null");
}
cancelFling(); //取消Fling事件
break;
case ACTION_CANCEL:
case ACTION_UP:
//当手指抬起时
if (getScale() < mMinScale) {//如果小于最小值
RectF rect = getDisplayRect();//获取显示矩阵
if (null != rect) {
//恢复到最小
v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
rect.centerX(), rect.centerY()));
handled = true;
}
}
break;
}
//如果mScaleDragDetector(缩放、拖拽)不为空,让它处理事件
if (null != mScaleDragDetector) {
//获取状态
boolean wasScaling = mScaleDragDetector.isScaling();
boolean wasDragging = mScaleDragDetector.isDragging();
handled = mScaleDragDetector.onTouchEvent(ev);
//mScaleDragDetector处理事件过后的状态,如果前后都不在缩放和拖拽,就允许父布局拦截
boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling();
boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging();
mBlockParentIntercept = didntScale && didntDrag;//阻止父类拦截的标识
}
// 如果mGestureDetector(双击,长按)不为空,交给它处理事件
if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) {
handled = true;
}
}
return handled;
}
双击缩放
我们来看一下双击缩放mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this));
这种实现方案。DefaultOnDoubleTapListener
实现了GestureDetector.OnDoubleTapListener
接口。
public interface OnDoubleTapListener {
/**
* 当单击时回调,不同于OnGestureListener.onSingleTapUp(MotionEvent),这个回调方法只在确信用户不会发生第二次敲击时调用
* @param e MotionEvent.ACTION_DOWN
* @return true if the event is consumed, else false
*/
boolean onSingleTapConfirmed(MotionEvent e);
/**
* 当双击时调用.
* @param e MotionEvent.ACTION_DOWN
* @return true if the event is consumed, else false
*/
boolean onDoubleTap(MotionEvent e);
/**
*当两次敲击间回调,回调 MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP事件
* @param e The motion event that occurred during the double-tap gesture.
* @return true if the event is consumed, else false
*/
boolean onDoubleTapEvent(MotionEvent e);
}
既然知道DefaultOnDoubleTapListener
实现了GestureDetector.OnDoubleTapListener
接口,那么直接去看DefaultOnDoubleTapListener
中是怎么实现的。
//单击事件
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (this.photoViewAttacher == null)
return false;
ImageView imageView = photoViewAttacher.getImageView();//获取ImageView
//如果OnPhotoTapListener不为null时回调
if (null != photoViewAttacher.getOnPhotoTapListener()) {
final RectF displayRect = photoViewAttacher.getDisplayRect();//获取当前的显示矩形
if (null != displayRect) {
final float x = e.getX(), y = e.getY();//获取第一次敲击时的坐标
if (displayRect.contains(x, y)) {//判断是不是敲击在显示矩阵内
//如果是的,就计算敲击百分比
float xResult = (x - displayRect.left)
/ displayRect.width();
float yResult = (y - displayRect.top)
/ displayRect.height();
//敲击图片内回调
photoViewAttacher.getOnPhotoTapListener().onPhotoTap(imageView, xResult, yResult);
return true;
}else{
//如果敲击在图片外回调
photoViewAttacher.getOnPhotoTapListener().onOutsidePhotoTap();
}
}
}
//如果OnViewTapListener不为null时回调,不管在不在图片里外
if (null != photoViewAttacher.getOnViewTapListener()) {
photoViewAttacher.getOnViewTapListener().onViewTap(imageView, e.getX(), e.getY());
}
return false;
}
//双击事件,在这里实现缩放
@Override
public boolean onDoubleTap(MotionEvent ev) {
if (photoViewAttacher == null)
return false;
try {
float scale = photoViewAttacher.getScale();//获取当前缩放比
float x = ev.getX();//获取敲击的坐标
float y = ev.getY();//获取敲击的坐标
if (scale < photoViewAttacher.getMediumScale()) {
//如果之前的缩放小于中等值,现在就缩放到中等值,缩放锚点就是当前的敲击事件坐标,true表示需要动画缩放。
photoViewAttacher.setScale(photoViewAttacher.getMediumScale(), x, y, true);
} else if (scale >= photoViewAttacher.getMediumScale() && scale < photoViewAttacher.getMaximumScale()) {
//如果之前的缩放大于中等值,现在就缩放到最大值,缩放锚点就是当前的敲击事件坐标
photoViewAttacher.setScale(photoViewAttacher.getMaximumScale(), x, y, true);
} else {
//否则缩放到最小值,缩放锚点就是当前的敲击事件坐标
photoViewAttacher.setScale(photoViewAttacher.getMinimumScale(), x, y, true);
}
} catch (ArrayIndexOutOfBoundsException e) {
// Can sometimes happen when getX() and getY() is called
}
return true;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
//由于不需要处理两次敲击间的其他事件,故这里不做处理
return false;
}
从这里可以看出,在单击时,会回调OnPhotoTapListener和OnViewTapListener,然后将坐标回调出去,如果是双击,则根据当前缩放比来判定现在的缩放比然后通过setScale
设置缩放比以及敲击的坐标。单击操作我们并不怎么关心,我们更关心双击的缩放操作,于是,查看setScale
源码。
@Override
public void setScale(float scale, float focalX, float focalY,
boolean animate) {
ImageView imageView = getImageView();//获取ImageView
//..
//省略了部分源码
//是否需要动画
if (animate) {
imageView.post(new AnimatedZoomRunnable(getScale(), scale,
focalX, focalY));
} else {
//设置给mSuppMatrix矩阵
mSuppMatrix.setScale(scale, scale, focalX, focalY);
checkAndDisplayMatrix();
}
}
}
setScale
的源码还是比较简单的,如果不需要动画,直接设置给mSuppMatrix,然后进行检查显示。如果需要动画的话,就执行AnimatedZoomRunnable
。AnimatedZoomRunnable
实现了Runnable
接口,主要实现代码如下。
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() {
ImageView imageView = getImageView();
if (imageView == null) {
return;
}
float t = interpolate();//获取当前的时间插值
float scale = mZoomStart + t * (mZoomEnd - mZoomStart);//根据插值,获取当前时间的缩放值
float deltaScale = scale / getScale();//获取缩放比,大于1表示在放大,小于1在缩小。deltaScale * getScale() = scale
//回调出去,deltaScale表示相对上次要缩放的比例
onScale(deltaScale, mFocalX, mFocalY);
if (t < 1f) {//插值小于1表示没有缩放完成,通过不停post进行执行动画
Compat.postOnAnimation(imageView, this);//Compat根据版本做了兼容处理,小于4.2用了 view.postDelayed,大于等于4.2用了view.postOnAnimation。
}
}
}
//计算当前时间的插值
private float interpolate() {
float t = 1.0F * (float)(System.currentTimeMillis() - this.mStartTime) / (float)PhotoViewAttacher.this.ZOOM_DURATION;
t = Math.min(1.0F, t);
t = PhotoViewAttacher.sInterpolator.getInterpolation(t);
return t;
}
}
onScale
的相关源码如下,可以看出,调用了mSuppMatrix.postScale
和checkAndDisplayMatrix()
来进行显示缩放。
@Override
public void onScale(float scaleFactor, float focusX, float focusY) {
if ((getScale() < mMaxScale || scaleFactor < 1f) && (getScale() > mMinScale || scaleFactor > 1f)) {
if (null != mScaleChangeListener) {
//监听
mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY);
}
//缩放
mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
checkAndDisplayMatrix();
}
}
双击缩放中的动画缩放的流程是这样的,首先会记录一个开始时间mStartTime
,然后根据当前时间来获取插值interpolate()
以便了解当前应该处于的进度,根据插值求出当前的缩放值scale
,然后与上次相比求出缩放比差值deltaScale
,然后通过onScale
回调出去,最终通过Compat.postOnAnimation来执行这个Runable,如此反复直到插值为1,缩放到目标值为止。
双指缩放及拖拽
双击缩放的相关源码到此为止,接下来看看通过双指缩放与拖拽的实现源码。即VersionedGestureDetector.newInstance(imageView.getContext(), this);
这句。
VersionedGestureDetector看名字便知道又做了版本兼容处理。里面只有一个静态方法newInstance
,源码如下。
//根据版本进行了控制
public final class VersionedGestureDetector {
public static GestureDetector newInstance(Context context,
OnGestureListener listener) {
final int sdkVersion = Build.VERSION.SDK_INT;
GestureDetector detector;
if (sdkVersion < Build.VERSION_CODES.ECLAIR) {
//小于Android 2.0
detector = new CupcakeGestureDetector(context);
} else if (sdkVersion < Build.VERSION_CODES.FROYO) {
//小于Android 2.2
detector = new EclairGestureDetector(context);
} else {
detector = new FroyoGestureDetector(context);
}
detector.setOnGestureListener(listener);
return detector;
}
}
newInstance
中传入了OnGestureListener,这个OnGestureListener
是自定义的接口,源码如下。
public interface OnGestureListener {
//拖拽时回调
void onDrag(float dx, float dy);
//Fling时回调
void onFling(float startX, float startY, float velocityX,
float velocityY);
//缩放时回调,`onScale`在双击动画缩放时已经介绍过了,scaleFactor表示相对于上次的缩放比
void onScale(float scaleFactor, float focusX, float focusY);
}
可以看出,回调了缩放、Fling和拖拽三种情况。现在我们回到newInstance
相关源码,可以看出有三种探测器CupcakeGestureDetector
、EclairGestureDetector
和FroyoGestureDetector
。且三者是相互继承的关系,FroyoGestureDetector
继承于EclairGestureDetector
,EclairGestureDetector
继承于CupcakeGestureDetector
。
其中CupcakeGestureDetector和EclairGestureDetector不支持双指缩放。由于Android2.0以下不支持多点触控,于是CupcakeGestureDetector
核心源码如下:
float getActiveX(MotionEvent ev) {
return ev.getX();
}
float getActiveY(MotionEvent ev) {
return ev.getY();
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
//添加速度探测器
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) {
//如果手指移动的距离大于mTouchSlop,表示在拖拽
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: {
if (null != mVelocityTracker) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
case MotionEvent.ACTION_UP: {
//手指抬起时,如果之前在拖拽
if (mIsDragging) {
if (null != mVelocityTracker) {
mLastTouchX = getActiveX(ev);
mLastTouchY = getActiveY(ev);
//计算滑动速度
mVelocityTracker.addMovement(ev);
mVelocityTracker.computeCurrentVelocity(1000);
final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker
.getYVelocity();
//如果大于最小的Fling速度,就回调出去
if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) {
mListener.onFling(mLastTouchX,、mLastTouchY, -vX,-vY);
}
}
}
//回收速度探测器
if (null != mVelocityTracker) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
}
return true;
}
从源码可以看出CupcakeGestureDetector
实现了拖拽和Fling效果。EclairGestureDetector
用于Android 2.2以下,主要修正了多点触控的问题,因为当双指触控时,我们需要获取的是最后一个手指离开屏幕时的坐标,因此需要使getActiveX/getActiveY指向正确的点。源码如下:
@Override
float getActiveX(MotionEvent ev) {
try {
return ev.getX(mActivePointerIndex);//mActivePointerIndex为手指的索引。根据当前手指的索引获取坐标
} catch (Exception e) {
return ev.getX();
}
}
@Override
float getActiveY(MotionEvent ev) {
try {
return ev.getY(mActivePointerIndex);
} catch (Exception e) {
return ev.getY();
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = ev.getPointerId(0);//第一根手指的id
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mActivePointerId = INVALID_POINTER_ID;
break;
case MotionEvent.ACTION_POINTER_UP:
//获取某一根手指抬起时的索引
final int pointerIndex = Compat.getPointerIndex(ev.getAction());
//根据索引获取id
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {//如果是抬起的是第一根手指
//那么对应获取第二点
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = ev.getPointerId(newPointerIndex);//将id指向第二根手指
//获取第二根手指的当前坐标
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
}
break;
}
//将索引指向后抬起的手指
mActivePointerIndex = ev
.findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId
: 0);
try {
return super.onTouchEvent(ev);//按照`CupcakeGestureDetector`的逻辑处理
} catch (IllegalArgumentException e) {
return true;
}
}
FroyoGestureDetector
用于Android 2.2以上,此时系统已经提供了一个缩放探索器,于是在拖拽和Fling的基础上,添加了双指缩放功能,核心源码如下。
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;
//回调出去。
mListener.onScale(scaleFactor,
detector.getFocusX(), detector.getFocusY());
return true;
}
//..
//省略了部分源码
图片的缩放与拖拽并没有在探测器中实现,而是回调到了PhotoViewAttacher中,PhotoViewAttacher实现了OnGestureListener接口,相关处理如下。
//拖拽回调
@Override
public void onDrag(float dx, float dy) {
if (mScaleDragDetector.isScaling()) {
return; // 如果正在缩放,不许做其他操作
}
//根剧拖拽进行平移
ImageView imageView = getImageView();
mSuppMatrix.postTranslate(dx, dy);
checkAndDisplayMatrix();
//判断父布局是不是可以拦截这一拖拽行为
ViewParent parent = imageView.getParent();
if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) {
//如果没有阻止父布局拦截且图片已显示在相关边缘内,就允许拦截
if (mScrollEdge == EDGE_BOTH
|| (mScrollEdge == EDGE_LEFT && dx >= 1f)
|| (mScrollEdge == EDGE_RIGHT && dx <= -1f)) {
if (null != parent) {
parent.requestDisallowInterceptTouchEvent(false);
}
}
} else {
//否则不允许拦截
if (null != parent) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
}
//Fling回调
@Override
public void onFling(float startX, float startY, float velocityX,
float velocityY) {
ImageView imageView = getImageView();
mCurrentFlingRunnable = new FlingRunnable(imageView.getContext());
//传入fling的速度与View的宽高
mCurrentFlingRunnable.fling(getImageViewWidth(imageView),getImageViewHeight(imageView), (int) velocityX, (int) velocityY);
imageView.post(mCurrentFlingRunnable);
}
其中FlingRunnable
的源码如下。
private class FlingRunnable implements Runnable {
private final ScrollerProxy mScroller;//Scroller这里做了版本兼容处理,API小于9时用了PreGingerScroller(内部用了Scroller),小于14用了GingerScroller(内部用了OverScroller),其他用了IcsScroller(内部用了OverScroller)。
private int mCurrentX, mCurrentY;//当前坐标
public FlingRunnable(Context context) {
mScroller = ScrollerProxy.getScroller(context);
}
public void cancelFling() {
mScroller.forceFinished(true);//停止
}
public void fling(int viewWidth, int viewHeight, int velocityX, int velocityY) {
final RectF rect = getDisplayRect();//获取图片的显示区域
if (null == rect) {
return;
}
//水平方向上
final int startX = Math.round(-rect.left);//四舍五入,左边的x坐标
final int minX, maxX, minY, maxY;//Fling的边界值
if (viewWidth < rect.width()) {//如果图片的宽度大于View宽时就计算X的边界。
minX = 0;
maxX = Math.round(rect.width() - viewWidth);
} else {
minX = maxX = startX;//如果图片宽小于View宽,就将三者设为一样。
}
//竖直方向上
final int startY = Math.round(-rect.top);
if (viewHeight < rect.height()) {//如果显示矩形高大于View的高。就计算边界
minY = 0;
maxY = Math.round(rect.height() - viewHeight);
} else {
minY = maxY = startY;
}
mCurrentX = startX;
mCurrentY = startY;
//调用 mScroller.fling
if (startX != maxX || startY != maxY) {
mScroller.fling(startX, startY, velocityX, velocityY, minX,
maxX, minY, maxY, 0, 0);
}
}
@Override
public void run() {
if (mScroller.isFinished()) {
return;
}
ImageView imageView = getImageView();
if (null != imageView && mScroller.computeScrollOffset()) {
//获取当前的位置
final int newX = mScroller.getCurrX();
final int newY = mScroller.getCurrY();
//将平移差值应用到mSuppMatrix
mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY);
setImageViewMatrix(getDrawMatrix());//应用到矩阵
mCurrentX = newX;
mCurrentY = newY;
Compat.postOnAnimation(imageView, this);
}
}
}
同样,利用了Compat.postOnAnimation
不停执行Runable来实现Fling惯性滚动效果。
关于PhotoViewAttacher的相关源码已经解读完毕,而该库中的控件PhotoView的实现也是依赖于PhotoViewAttacher,在onDetachedFromWindow
中会自动回收资源,核心源码如下,其他就不做详细介绍了。
public PhotoView(Context context, AttributeSet attr, int defStyle) {
super(context, attr, defStyle);
super.setScaleType(ScaleType.MATRIX);
this.init();
}
//初始化
protected void init() {
if(null == this.mAttacher || null == this.mAttacher.getImageView()) {
this.mAttacher = new PhotoViewAttacher(this);
}
if(null != this.mPendingScaleType) {
this.setScaleType(this.mPendingScaleType);
this.mPendingScaleType = null;
}
}
//调用回收cleanup
protected void onDetachedFromWindow() {
this.mAttacher.cleanup();
super.onDetachedFromWindow();
}
//初始化
protected void onAttachedToWindow() {
this.init();
super.onAttachedToWindow();
}
最后
感觉最近写东西越来越啰嗦了,需要练习着把话讲的简练一点,下一期源码解读:Gson。
本期解读到此结束,如有错误之处,欢迎指出。