Android 开源项目源码解析之PhotoView 源码解析

1. 功能介绍

特性(Features):
  • 支持Pinch手势自由缩放。
  • 支持双击放大/还原。
  • 支持平滑滚动。
  • 在滑动父控件下能够运行良好。(例如:ViewPager)
  • 支持基于Matrix变化(放大/缩小/移动)的事件监听。
    优势:
  • PhotoView是ImageView的子类,自然的支持所有ImageView的源生行为。
  • 任意项目可以非常方便的从ImageView升级到PhotoView,不用做任何额外的修改。
  • 可以非常方便的与ImageLoader/Picasso之类的异步网络图片读取库集成使用。
  • 事件分发做了很好的处理,可以方便的与ViewPager等同样支持滑动手势的控件集成。

2. 总体设计

PhotoView这个库实际上比较简单,关键点其实就是Touch事件处理和Matrix图形变换的应用.

2.1 TouchEvent及手势事件处理

对TouchEvent分发流程不了解的建议先阅读 Android Touch事件传递机制

本库中对Touch事件的处理流程请参考第三部分的流程图,会有一个比较直观的认识。

2.2 Matrix

由于Matrix是Android系统源生API,很多开发者对此都比较熟悉,为了不影响阅读效果,故不在此详细叙述,如果对其不是很了解,可以查看本文档末尾的Matrix补充说明

3. 流程图

Touch及手势事件判定及传递流程:

流程图

如图,从架构上看,干净利落的将事件层层分离,交由不同的Detector处理,最后再将处理结果回调给PhtotViewAttacher中的Matrix去实现图形变换效果。

4. 详细设计

4.1 核心类功能介绍

Core核心类


4.1.1 PhotoView

PhotoView 类负责暴露所有供外部调用的API,其本身直接继承自ImageView,同时实现了IPhotoView接口.
IPhotoView接口提供了缩放相关的设置属性 和操控matrix变化的回调接口.

主要方法说明:

  • public PhotoView(Context context)
  • public PhotoView(Context context, AttributeSet attr)
  • public PhotoView(Context context, AttributeSet attr, int defStyle)

构造函数,完全与ImageView相同,你可以将PhotoView直接当做ImageView使用,完全兼容.

  • public void setPhotoViewRotation(float rotationDegree)

用于设置图片旋转角度.

注意:
例如使用Android相机拍摄的相片,会根据拍摄时手机方向的不同,在EXIF中存储不同的旋转角度信息,显示时往往需要查询EXIF信息并将照片旋转至正确的方向.
通常我们处理这种问题有两种方案:

  • 通过Bitmap.createBitmap方式重建出正确方向的图片,再加载到ImageView中显示。(不建议使用,因为会占用双倍的内存,Bitmap的回收不是立即生效的。)
  • 在ImageView中使用自定义Matrix将图片旋转到正确的方向。

由于PhotoView中对图片的 缩放 操作依赖对Matrix的操作,自定义Matrix会干扰 PhotoView 的缩放行为,所以PhotoView并不支持ScaleType.Matrix.
可参见PhotoViewAttacher源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 /**
* @return true if the ScaleType is supported.
*/

private static boolean isSupportedScaleType(final ScaleType scaleType) {
if (null == scaleType) {
return false;
}

switch (scaleType) {
case MATRIX:
throw new IllegalArgumentException(scaleType.name()
+ " is not supported in PhotoView");

default:
return true;
}
}

这里特意提供了一个额外的setPhotoViewRotation方法即是为了解决这个问题。

  • public boolean canZoom()
  • public void setZoomable(boolean zoomable)

缩放功能开关及状态获取.
关闭后PhotoView将不再响应 缩放 动作.

  • public RectF getDisplayRect()
  • public Matrix getDisplayMatrix()
  • public boolean setDisplayMatrix(Matrix finalRectangle)

获取及设置当前 matrix 状态.

  • public ScaleType getScaleType()

获取缩放模式。使用的源生的ImageView.ScaleType.
在PhotoView中默认值为FIT_CENTER.

  • public void setAllowParentInterceptOnEdge(boolean allow)

设置标志位 是否允许父控件捕获发生在边缘的TouchEvent

这个标志位实际上对应的是
ViewParent.requestDisallowInterceptTouchEvent(boolean flag)

经常做自定义View处理TouchEvent的对这个方法应当都不陌生。

PhotoView中英文注释:

1
2
3
4
5
6
7
* 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).

对应的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
ViewParent parent = imageView.getParent();
if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling()) {
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);
}
}

通过调用setAllowParentInterceptOnEdge(false),可以完全屏蔽父控件的TouchEvent.
这个设置是为了防止父控件响应InterceptTouchEvent.

例如

PhotoView外层是ScrollView,通过requestDisallowInterceptTouchEvent方法可以阻止ScrollView响应滑动手势.

PhotoView本身已做好了相关处理,在PhotoView滚到图片边缘时,Scroll事件由父控件处理,在PhotoView未滚动到边缘时,Scroll事件由PhotoView处理.

除非开发者有特殊的需求,否则不需要自己去调用该方法改变TouchEvent事件的阻断逻辑.

  • public void setImageDrawable(Drawable drawable)
  • public void setImageResource(int resId)
  • public void setImageURI(Uri uri)

重载了ImageView的3个设置图片的方法,以确保图片改变时PhotoViewAttacher及时更新视图和重置matrix状态

  • protected void onDetachedFromWindow()

重载了ImageView的方法,用于在视图被从Window中移除时,通知PhotoViewAttacher清空数据.

4.1.2 IPhotoView

IPhotoView接口定义了缩放相关的一组set/get方法.PhotoView是其实现类.
相关方法已在PhotoView中介绍,这里略过.

4.1.3 PhotoViewAttacher

核心类

  • private static boolean isSupportedScaleType(final ScaleType scaleType)

判断ScaleType是否支持。
这个判断中实际只有ScaleType.Matrix会返回false.

由于PhotoView中 缩放 滑动操作都依赖Matrix,所以并不支持用户再传入自定义Matrix.

  • public void cleanup()

PhotoView不再使用时,可用于释放相关资源。移除Observer, Listener.

  • public boolean setDisplayMatrix(Matrix finalMatrix)

通过Matrix来直接修改ImageView的显示状态。

  • private void cancelFling()

取消惯性滑动。

  • private boolean checkMatrixBounds()

检查当前显示范围是否处于边界上,并更新mScrollEdge标志位。

处理TouchEvent时需要根据mScrollEdge标志位的状态来判断是否允许ViewParent的InterceptTouchEvent接收TouchEvent.

  • private void resetMatrix()

重置Matrix状态,并恢复至FIT_CENTER状态

  • private void updateBaseMatrix(Drawable d)

根据PhotoView的宽高和Drawable的宽高计算FIT_CENTER状态的Matrix.

  • public void onDrag(float dx, float dy)

OnGestureListener接口回调的实现方法.

实际完成拖拽/移动效果.
核心代码:

mSuppMatrix.postTranslate(dx, dy);

通过改代码修改Matrix中View的起始位置,制造出图片被拖拽移动的效果.

  • public void onFling(float startX, float startY, float velocityX, float velocityY)

OnGestureListener接口回调的实现方法.
实际完成惯性滑动效果.

惯性滑动效果分两部分完成.

1) 调用

mScroller.fling(startX, startY, velocityX, velocityY, minX,
                    maxX, minY, maxY, 0, 0);

进行惯性滑动辅助计算.

对Scroller不了解的可以参考官方说明 Scroller

简单来讲,Scroller是一个辅助计算器,它可以帮你计算出某一时刻View的滚动状态及位置,但是它本身不会对View进行任何更改

2) 使用了FlingRunnable和Compat.postOnAnimation(imageView,mFlingRunnable)在每一帧绘制前更新Matrix状态
关于FlingRunnable和Compat.postOnAnimation类的作用机制可以参考下面 4.1.4的说明.

  • public void onScale(float scaleFactor, float focusX, float focusY)

OnGestureListener接口回调的实现方法.

实际完成缩放效果.

核心代码:

1
mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);

对Matrix作用机制不了解的话,可以拉到文档最后,有一个针对Matrix的简略介绍.

内部类 FlingRunnable

实现惯性滑动的动画效果.

这个Runnable必须配合 View.postOnAnimation(view,runnable) 使用.

在下一帧绘制前,系统会执行该Runnable,这样我们就可以在runnable中更新UI状态.

原理上类似一个递归调用,每次UI绘制前更新UI状态,并指定下次UI更新前再执行自己.

这种写法 与 使用循环或Handler每隔16ms刷新一次UI基本等价,但是更为方便快捷.

更新UI的核心逻辑非常简单,根据mScroller计算出的偏移量更新Matrix状态:

1
mSuppMatrix.postTranslate(dx, dy);
内部类 AnimatedZoomRunnable

实现双击时的 缩放动画.

作用机制基本同上.

区别是AnimatedZoomRunnable的执行进度由AccelerateDecelerateInterpolator控制.

对Interpolator没有概念的可以参阅官方Demo
Interpolator

你也可以简单认为这就是一个动画进度控制器.

核心逻辑依然很简单,根据动画进度缩小/放大图片

1
mSuppMatrix.postScale(deltaScale, deltaScale, mFocalX, mFocalY);

接口及工具类


4.1.4 Compat

用于做View.postOnAnimation方法在低版本上的兼容.

注:View.postOnAnimation (Runnable action) 在PhotoView中用于处理 双击 放大/缩小 惯性滑动时的动画效果.

每次系统绘图前都会先执行这个Runnable回调,通过在此时改变视图状态以实现动画效果。该方法仅支持 api >= 16
所以PhotoView中使用了Compat类来做低版本兼容。

实际上也可以使用android.support.v4.view.ViewCompat替代。
对比 android.support.v4.view.ViewCompat 和 uk.co.senab.photoview.Compat
其实现原理完全一致,都是通过view.postDelayed(runnable, frameTime)来实现.

4.1.5 ScrollerProxy

抽象类,主要是为了做不用版本之间的兼容,具体说明见GingerScroller IcsScroller PreGingerScroller 这三个接口实现类的说明.

4.1.6 GingerScroller

ScrollerProxy 接口实现类
适用于 API 9 ~ 14 即 2.3 ~ 4.0 之间的所有Android版本.
其实现主要基于 android.widget.OverScroller

4.1.7 IcsScroller

适用于 API 14 以上 即 4.0 以上的所有Android版本
其实现基于源生 android.widget.OverScroller , 没有任何修改.

4.1.8 PreGingerScroller

适用于 API 9 以下 即 2.3 以下的所有Android版本
其实现主要基于 android.widget.Scroller

4.1.9 GestureDetector

接口,主要是为了做不同版本之间的兼容,具体说明见 CupcakeGestureDetector,EclairGestureDetector,FroyoGestureDetector 三个接口的实现类.

4.1.10 OnGestureListener

手势回调接口

4.1.11 CupcakeGestureDetector

适用于 api < 7 的设备,此时PhotoView不支持双指pinch放大/缩小操作

4.1.12 EclairGestureDetector

适用于 api >= 8 , 用于修正多指操控的问题,使TouchEvent的getActiveX getActiveY指向正确的Pointer,并将事件传递给 CupcakeGestureDetector 处理,此时PhotoView不支持双指pinch放大/缩小操作

4.1.13 FroyoGestureDetector

适用于 api > 9 , 通过android.view.ScaleGestureDetector实现对Pinch手势的支持,并将事件传递给 EclairGestureDetector 处理

注意:
以上3个类并不实际执行 放大/缩小 行为, 判断行为之后会回调给PhtotViewAttacher执行缩放/移动操作

4.1.14 VersionedGestureDetector

提供GestureDetector的实例,由它根据系统版本决定实例化哪一个 GestureDetector ,主要是为了兼容Android的不同版本。
具体调用栈请参考总体设计中调用流程图,注意一点,PhotoViewAttacher本身就实现了OnGestureListener接口,实际的缩放操作是由PhotoViewAttacher完成的,而不是这里声明的各个GestureDetector.

4.2 类关系图

PhotoView

5. 杂谈

该库唯一缺少的可能是 手势旋转 功能(可以参考QQ). 不过由于PhotoView中已将各级事件分开处理,从架构上来看可扩展性良好,自定义一个RotateGestureDetector来捕获旋转手势也可行.
但如何在不与ScaleGestureDetector冲突的情况下完成该功能会稍微有些麻烦.
如果不需要手势旋转的话,该库提供了单独的接口可以用代码设置旋转角度。

6. Matrix补充说明

Matrix是一个 3x3 矩阵,使用Matrix可以对 Bitmap/Canvas 进行4类基本图形变换,使用起来非常简便,如果你对Matrix的抽象变换不熟悉,还可以使用android.graphics.Camera类进行辅助计算。
Camera类可以将矩阵变换抽象成 视点(摄像机) 在三维空间内的移动,更易于直观的理解其效果。

矩阵如下:

tranlate

相关API使用起来非常简单。
效果用文字比较难表述,直接看图好了.
你也可以自己运行Demo Project

虚影为原始位置,实图为变换后位置.

API

  • public void setTranslate(float dx, float dy)

    对目标进行平移dx,dy

    tranlate

  • public void setScale(float sx, float sy, float px, float py)

    以(px,py)为中心,横向上缩放比例sx,纵向缩放比例sy

    scale

  • public void setRotate(float degrees, float px, float py)

    以(px,py)为中心,旋转degrees度

    rotate

  • public void setSkew(float kx, float ky, float px, float py)

    图像的错切实际上是平面景物在投影平面上的非垂直投影。错切使图像中的图形产生扭变。
    这里是以(px,py)为中心,扭曲图片的x轴和y轴.

    这个用文字难以解释,请参考下面的实际效果图片.

    skew

原理

如果你对矩阵变换背后的数学原理感兴趣且线性代数的内容没忘光的话,推荐这篇 文章.

本文为 Android 开源项目源码解析 中 PhotoView 部分
项目地址:PhotoView,分析的版本:48427bf,Demo 地址:PhotoView-demo
分析者:dkmeteor,校对者:cpacm,校对状态:完成