Android图片处理二:PhotoView源码解析
Posted Jadyli1
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 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
了。
2 手势监听
我们看下具体的手势监听部分。手势一般包括:双击、单击、长按、双指缩放、拖曳、惯性滚动(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
类,这个类存储了手指的移动状态,主要包含:
- 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 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
(<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 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
在之前一直存的是第一个手指的坐标,所以这里只要判断是不是第一个手指抬起,如果是第一个手指抬起,就更新到下一个手指的坐标。
至此核心的触摸事件捕获看完了,主要处理了拖动和惯性滑动。
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))android 图解 PhotoView,从‘百草园’到‘三味书屋’!