Android 开源项目PhotoView源码分析

时间:2023-03-09 04:18:01
Android 开源项目PhotoView源码分析

https://github.com/chrisbanes/PhotoView/tree/master/library

这个就是项目地址,相信很多人都用过,我依然不去讲怎么使用。只讲他的原理和具体实现。

具体会讲到:

1.如何实现pinch手势 放大缩小图片。

2.如何实现的拖动图片。

3.如何实现的惯性拖动。

4.如何控制与父view的 事件监听

主要就是这三点。

具体的调用方法 主要是下面这样:

   ImageView mImageView = (ImageView) findViewById(R.id.iv_photo);
mCurrMatrixTv = (TextView) findViewById(R.id.tv_current_matrix); Drawable bitmap = getResources().getDrawable(R.drawable.wallpaper);
mImageView.setImageDrawable(bitmap); //将imageview和PhotoViewAttacher 这个控制器关联起来
mAttacher = new PhotoViewAttacher(mImageView);

可以看出来 主要的工作都是在这个PhotoViewAttacher里做的。

我们来跟着他的构造函数 看

  //默认构造函数是可以被放大缩小的 zoomable 为true
public PhotoViewAttacher(ImageView imageView) {
this(imageView, true);
} public PhotoViewAttacher(ImageView imageView, boolean zoomable) {
//这个是防止内存泄露的一个技巧,注意你在activity里的imageview 对象如果把引用传进来的话
//那这里的mImageView 也是指向外层的ImageView那个对象,那么就相当于有2个引用指向同一块地址!
//当你外层的imageview对象被销毁的时候,因为这里还有一个引用指向那个对象,所以实际上对象不会被销毁
//只是最外层的那个imageview对象的引用成了null而已,但是如果你在这里用了弱引用的话,当外层的强引用
//为NUll的话 imageview对象会立刻被销毁掉。
mImageView = new WeakReference<>(imageView); //這個draswingcache 我們做截屏的時候會經常用到,只需要理解成我們可以通過getDrawingCache拿到view里的內容(這個內容被轉成了bitmap)
imageView.setDrawingCacheEnabled(true);
imageView.setOnTouchListener(this); //这里就是监听imageview的 layout变化用的 imageview发生变化就会调用这个回调接口
ViewTreeObserver observer = imageView.getViewTreeObserver();
if (null != observer)
observer.addOnGlobalLayoutListener(this); // 设置绘制时这个imageview 可以随着matrix矩阵进行变换
setImageViewScaleTypeMatrix(imageView);
//这个是让你在可视化界面能看到预览效果的,大家自定义控件时 也可以使用这个技巧
if (imageView.isInEditMode()) {
return ;
}
// Create Gesture Detectors...
//根据版本不同 取得需要的mScaleDragDetector 主要就是监听pinch手势的
mScaleDragDetector = VersionedGestureDetector.newInstance(
imageView.getContext(), this); //这个dectecor 就是用来监听双击和长按事件的
mGestureDetector = new GestureDetector(imageView.getContext(),
new GestureDetector.SimpleOnGestureListener() { // forward long click listener
@Override
public void onLongPress(MotionEvent e) {
if (null != mLongClickListener) {
mLongClickListener.onLongClick(getImageView());
}
}
});
//监听双击手势的
mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this)); // Finally, update the UI so that we're zoomable
setZoomable(zoomable);

我们先看19-21行 这个里面加了一个监听layout变化的一个回调,我们来看看这个回调做了什么:

  @Override
public void onGlobalLayout() {
ImageView imageView = getImageView(); if (null != imageView) {
if (mZoomEnabled) {
//这个地方要注意imageview的 四个坐标点是永远不会变化的。
final int top = imageView.getTop();
final int right = imageView.getRight();
final int bottom = imageView.getBottom();
final int left = imageView.getLeft(); /**
* We need to check whether the ImageView's bounds have changed.
* This would be easier if we targeted API 11+ as we could just use
* View.OnLayoutChangeListener. Instead we have to replicate the
* work, keeping track of the ImageView's bounds and then checking
* if the values change.
*/
if (top != mIvTop || bottom != mIvBottom || left != mIvLeft
|| right != mIvRight) {
// Update our base matrix, as the bounds have changed
updateBaseMatrix(imageView.getDrawable()); // Update values as something has changed
mIvTop = top;
mIvRight = right;
mIvBottom = bottom;
mIvLeft = left;
}
} else {
updateBaseMatrix(imageView.getDrawable());
}
}
}

然后跟进去发现是这个函数:

 /**
* Calculate Matrix for FIT_CENTER
*
* @param d - Drawable being displayed
*/
private void updateBaseMatrix(Drawable d) {
ImageView imageView = getImageView();
if (null == imageView || null == d) {
return;
} final float viewWidth = getImageViewWidth(imageView);
final float viewHeight = getImageViewHeight(imageView);
//这个是取原始图片大小的 永远不会变化的
final int drawableWidth = d.getIntrinsicWidth();
final int drawableHeight = d.getIntrinsicHeight(); mBaseMatrix.reset(); final float widthScale = viewWidth / drawableWidth;
final float heightScale = viewHeight / drawableHeight; //根据传进去的scaletype的值来确定 基础的matrix大小
if (mScaleType == ScaleType.CENTER) {
mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F,
(viewHeight - drawableHeight) / 2F); } else if (mScaleType == ScaleType.CENTER_CROP) {
float scale = Math.max(widthScale, heightScale);
mBaseMatrix.postScale(scale, scale);
mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
(viewHeight - drawableHeight * scale) / 2F); } else if (mScaleType == ScaleType.CENTER_INSIDE) {
float scale = Math.min(1.0f, Math.min(widthScale, heightScale));
mBaseMatrix.postScale(scale, scale);
mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
(viewHeight - drawableHeight * scale) / 2F); } else {
RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight);
RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight); 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();
}

其实这边代码很容易懂,大家都知道 imageview里面是一个图片对吧,你这个图片有大有小,那你是怎么放置在imageview里面就有讲究了,你在imageview里是可以设置的他的scaletype的属性的,

那当然了你设置完毕以后 肯定相对于原图片来说 你已经做了一次matrix变换了,所以你要记录这次matrix的值。这里你只要记住对于一个imageview 来说 他本身容器的大小是固定的,

容器里面的drawable的原图大小也是固定的,但是现实效果是通过matrix来控制的,所以我们要记录每一次图片发生变化的时候matrix的值,这是很关键的。

然后回到我们的构造函数看31-32行。这里构造了一个

uk.co.senab.photoview.gestures.GestureDetector mScaleDragDetector

我们也继续看看这个是如何构造出来的
 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) {
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;
} }

我们发现这个地方是一个单例,实际上这边代码就是根据sdk的版本号不同 提供不一样的功能,比如说pinch手势 在4.0以下就没有支持 那当然了,我们现在百分之95以上的机器都是4.0以上的,

我们只要分析12-13行就可以。

 /*******************************************************************************
* Copyright 2011, 2012 Chris Banes.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package uk.co.senab.photoview.gestures; import android.annotation.TargetApi;
import android.content.Context;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector; @TargetApi(8)
//能看出来 低于这个版本的 都不支持pinch 放大缩小功能
public class FroyoGestureDetector extends EclairGestureDetector { //用于检测缩放的手势
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; mListener.onScale(scaleFactor,
detector.getFocusX(), detector.getFocusY());
return true;
} //这个函数返回true的时候 onScale函数才会真正调用
@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);
} }

33行-58行 才是真正系统提供给我们的监听pinch手势的地方。就是我们印象中 两指放大缩小图片的那个手势的监听。看42-43行

这里发现使用了一个回调,回过头去看我们的构造函数 我们就知道这个回调 实际上就是在控制器里他自己实现的。

 public class PhotoViewAttacher implements IPhotoView, View.OnTouchListener,
OnGestureListener,
ViewTreeObserver.OnGlobalLayoutListener {

所以我们就到控制器里去找这个手势监听的实现代码:

 //这个就是处理pinch 手势的,放大 缩小图片的处理函数
@Override
public void onScale(float scaleFactor, float focusX, float focusY) {
if (DEBUG) {
LogManager.getLogger().d(
LOG_TAG,
String.format("onScale: scale: %.2f. fX: %.2f. fY: %.2f",
scaleFactor, focusX, focusY));
}
if (getScale() < mMaxScale || scaleFactor < 1f) {
//这个回调接口 你可以不用set的,通常来说 我们很少实现这个接口,有需要的可以自己去看注释这接口的意思
if (null != mScaleChangeListener) {
mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY);
}
//这个地方要注意 这个放大 并不是固定的以图片中心放大的。他是以你两个手指做pinch手势的
//的时候 取点来放大的,这么做的一个好的地方是 你可以放大图片中某一部分,而不是只能从
//从图片中间部分开始放大缩小。但是你可以想一下,这么做的弊端就是 很容易因为你的放大缩小
//因为点不在中间,所以图片很有可能就不在imageview这个控件的中间,会让imageview边缘或者其他地方
//有留白
mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
//而这个函数就是解决上述弊端的,
checkAndDisplayMatrix();
}
}
  /**
* Helper method that simply checks the Matrix, and then displays the result
*/
private void checkAndDisplayMatrix() {
//实际上在matrix里就解决了上述的弊端
if (checkMatrixBounds()) {
setImageViewMatrix(getDrawMatrix());
}
}
  //检查当前显示范围是否在边界上  然後對圖片進行平移(垂直或水平方向) 防止出現留白的現象
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);
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;
}
} else if (rect.top > 0) {
deltaY = -rect.top;
} else if (rect.bottom < viewHeight) {
deltaY = viewHeight - rect.bottom;
} final int viewWidth = getImageViewWidth(imageView);
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;
}
mScrollEdge = EDGE_BOTH;
} else if (rect.left > 0) {
mScrollEdge = EDGE_LEFT;
deltaX = -rect.left;
} else if (rect.right < viewWidth) {
deltaX = viewWidth - rect.right;
mScrollEdge = EDGE_RIGHT;
} else {
mScrollEdge = EDGE_NONE;
} // Finally actually translate the matrix
mSuppMatrix.postTranslate(deltaX, deltaY);
return true;
}

这个地方有的人可能会对最后那个检测是否在边界的那个函数不太明白,其实还是挺好理解的,对于容器imageview来说 他的范围是固定的。里面的drawable是不断的变化的,

但是这个drawable 可以和 RectF来关联起来,这个rectF 就是描述出一个矩形,这个矩形就恰好是drawable的大小范围。他有四个值 分别是top left right和bottom。

其中2个值表示矩形的左上面ed点的坐标 另外2个表示右下角的坐标。一个矩形由这2个点即可确定位置以及大小。我用下图来表示:

Android 开源项目PhotoView源码分析

所以那个函数你要想理解的话 就是自己去画个图。就能知道如何判断是否到边缘了!实际上就是drawbl---matrix---rectF的一个转换。
另外一定要自己画图 才能真正理解 里面的逻辑 很简单 并不难! 那这个onscale 手势我们过了一遍以后 看看这个函数是怎么被调用的
很简单 还是通过onTouch事件
  @SuppressLint("ClickableViewAccessibility")
@Override
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:
// First, disable the Parent from intercepting the touch
// event
if (null != parent) {
//阻止父层的View截获touch事件
parent.requestDisallowInterceptTouchEvent(true);
} else {
LogManager.getLogger().i(LOG_TAG, "onTouch getParent() returned null");
} // If we're flinging, and the user presses down, cancel
// fling
cancelFling();
break; case ACTION_CANCEL:
case ACTION_UP:
//放大缩小都得有一个度,这个地方就是说 如果你缩的太小了,比如我们定义的是缩小到原图的百分之25
//如果你缩小到百分之10了,那么当你手指松开的时候 就要自动将图片还原到百分之25,当然这个过程
//你得使用动画慢慢从10回复到25,因为一下子回复到25 实在是太难看了
//在下一帧绘制前,系统会执行该 Runnable,这样我们就可以在 runnable 中更新 UI 状态.
//原理上类似一个递归调用,每次 UI 绘制前更新 UI 状态,并指定下次 UI 更新前再执行自己.
//这种写法 与 使用循环或 Handler 每隔 16ms 刷新一次 UI 基本等价,但是更为方便快捷
if (getScale() < mMinScale) {
RectF rect = getDisplayRect();
if (null != rect) {
v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
rect.centerX(), rect.centerY()));
handled = true;
}
}
break;
} // Try the Scale/Drag detector
if (null != mScaleDragDetector) {
boolean wasScaling = mScaleDragDetector.isScaling();
boolean wasDragging = mScaleDragDetector.isDragging();
//这行代码是最终交给pinch手势监听的代码
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 (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) {
handled = true;
} }
return handled;
}

好 到这个地方 相信大家对 pinch事件就理解的差不多了 包括是怎么被调用的这个过程 以及中间的缺陷处理 都明白了。我们继续看drag 也就是拖动是怎么处理的。

 /能看出来 低于这个版本的 都不支持pinch 放大缩小功能
public class FroyoGestureDetector extends EclairGestureDetector {

看的到 他是继承自这个类的

 @TargetApi(5)
public class EclairGestureDetector extends CupcakeGestureDetector {
 /*******************************************************************************
* Copyright 2011, 2012 Chris Banes.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package uk.co.senab.photoview.gestures; import android.content.Context;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration; import uk.co.senab.photoview.log.LogManager; public class CupcakeGestureDetector implements GestureDetector { //惯性滑动拖动处理
protected OnGestureListener mListener;
private static final String LOG_TAG = "CupcakeGestureDetector";
float mLastTouchX;
float mLastTouchY;
final float mTouchSlop;
final float mMinimumVelocity; @Override
public void setOnGestureListener(OnGestureListener listener) {
this.mListener = listener;
} public CupcakeGestureDetector(Context context) {
final ViewConfiguration configuration = ViewConfiguration
.get(context);
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mTouchSlop = configuration.getScaledTouchSlop();
} private VelocityTracker mVelocityTracker;
private boolean mIsDragging; float getActiveX(MotionEvent ev) {
return ev.getX();
} float getActiveY(MotionEvent ev) {
return ev.getY();
} public boolean isScaling() {
return false;
} public boolean isDragging() {
return mIsDragging;
} @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: {
//这个拖动事件其实也很好理解,就是确定你的手指在滑动的时候坐标点的变化
//这个变化要理解好 就是你可以把屏幕左上角的点 想象成一个坐标系的原点。
//那你如果要计算某个坐标点和这个原点的直线距离 实际上就是 这个坐标点的
// x*x+y*y 然后把这个值开根号即可,初中数学问题!只要你每次这个直线距离
//有变化 那就肯定是拖动事件了
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
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);
}
}
} // Recycle Velocity Tracker
if (null != mVelocityTracker) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
} return true;
}
}

应该都能明白,我们看看那个接口到底是啥

 /*******************************************************************************
* Copyright 2011, 2012 Chris Banes.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package uk.co.senab.photoview.gestures; public interface OnGestureListener { public void onDrag(float dx, float dy); public void onFling(float startX, float startY, float velocityX,
float velocityY); public void onScale(float scaleFactor, float focusX, float focusY); }

最后 再看看控制器里这个接口是怎么实现onDrag的,onFling就不分析了 差不多其实

  @Override
public void onDrag(float dx, float dy) {
if (mScaleDragDetector.isScaling()) {
return; // Do not drag if we are already scaling
} if (DEBUG) {
LogManager.getLogger().d(LOG_TAG,
String.format("onDrag: dx: %.2f. dy: %.2f", dx, dy));
} ImageView imageView = getImageView();
mSuppMatrix.postTranslate(dx, dy);
//滑动的时候也不要忘记检测边缘 防止留白
checkAndDisplayMatrix(); //这个地方就是做了一个巧妙的判断,他要实现的功能就是:
//如果你拖拽到了边缘,还继续拖拽的话 那就交给父view来处理,如果没有到边缘 那我们就继续自己处理 继续拖拽图片了
//想象一下我们把photoview放到viewpager的时候 就是这样处理的
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);
}
}
}
到这我们的这篇博客就基本结束了,其实这个开源photoview真的挺值得大家好好分析的,如果分析的好,你对android里面 各种手势监听 matrix rectF
等等应该都能了如指掌了,以后遇到类似的问题 应该不会没有头绪了!