前言
原理
PhotoDraweeView是基于PhotoView的设计思路实现的,其存在的意义是弥补PhotoView不支持Fresco的不足。PhotoDraweeView继承自SimpleDraweeView个,实现IAttacher接口,并重写了onDraw来更新视图,通过Matrix来实现图片的变换,以及通过重写onTouch来处理手势。我们可以看一下其构造函数:
public PhotoDraweeView(Context context) {
super(context);
init();
}
……
protected void init() {
if (mAttacher == null || mAttacher.getDraweeView() == null) {
mAttacher = new Attacher(this);
}
}
其实例化了一个Attacher,该类负责对图片和手势的处理相关操作,我们主要关注在于以下两点:
- 手势处理
- 图片处理
手势处理
在PhotoDraweeView中,手势的相关处理交由了ScaleDragDetector和GestureDetectorCompat两个类来管理,前者负责对多点触发比例缩放和滑动拖拽等操作做处理,后者主要接管了单击和双击事件的处理。
相关的类的关系图如下:
对于手势的处理,根据对View的事件方法机制的学习,我们可以知道,一个手势事件MotionEvent最终会onTouch的相关方法中处理,而在Attacher中,对DraweeView设置了OnTouchListener,其接口的onTouch在PhotoDraweeView中实现:
@Override public boolean onTouch(View v, MotionEvent event) {
int action = MotionEventCompat.getActionMasked(event);
//避免父类直接拦截事件
switch (action) {
case MotionEvent.ACTION_DOWN: {
ViewParent parent = v.getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
cancelFling();
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
ViewParent parent = v.getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(false);
}
}
break;
}
……
//比例控制工具
boolean handled = mScaleDragDetector.onTouchEvent(event);
……
if (mGestureDetector.onTouchEvent(event)) {
handled = true;
}
return handled;
}
其在方法中,主要先屏蔽父类的Intercept,这样可以可以最大限度的获取到事件,是考虑多个View/ViewGroup事件冲突的时候的相关操作。然后进一步调用了ScaleDragDetector和GestureDetector两个对象来对手势事件进行处理。
ScaleDragDetector
该类实现了OnScaleGestureListener,同时持有OnScaleDragGestureListener接口(该接口在PhotoDraweeView中实现),其负责了对手势MotionEvent的初步拆解,并记录一些点的数据。所有的事件处理都集中在了该类的onTouchEvent(手势操作必经之路):
public boolean onTouchEvent(MotionEvent ev) {
//数据传递到OnScaleGestureListener接口,最终还是传递到OnScaleDragGestureListener中
mScaleDetector.onTouchEvent(ev);
final int action = MotionEventCompat.getActionMasked(ev);
//获取触摸点的位置,多点取第一个点位置
onTouchActivePointer(action, ev);
//对手势进行拆解,分配到OnScaleDragGestureListener里的相应方法中处理
onTouchDragEvent(action, ev);
return true;
}
所以事件的最后还是传递到了OnScaleDragGestureListener中。在经过对手势的处理判断,就可以根据结果来分别触发该接口中的方法。该接口包含以下方法:
public interface OnScaleDragGestureListener {
//拖拽的时候
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 onScaleEnd();
}
接下来的事就变的简单了,主要在个接口方法中做些简单处理,然后传递到Matrix的对应方法中即可实现图像的变换。以onScale方法为例,其会先做些判断,然后讲变换的数据传递给其它回调接口,最后通过Matrix更新,代码如下:
@Override public void onScale(float scaleFactor, float focusX, float focusY) {
if (getScale() < mMaxScale || scaleFactor < 1.0F) {
if (mScaleChangeListener != null) {
mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY);
}
mMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
checkMatrixAndInvalidate();
}
}
GestureDetector
在实际使用过程中,用户可以通过双击屏幕来进行对图片的缩小和放大,该接口就是用于响应做些点击事件的。在PhotoDraweeView中提供了一个默认实现类DefaultOnDoubleTapListener,该类主要处理了如下两个接口方法:
//单击事件
@Override public boolean onSingleTapConfirmed(MotionEvent e) {
……
//根据点的位置来确定点击触发的是图片还是整个View(存在图片区域小于View的情况),然后取调用不同的接口取处理。
if (mAttacher.getOnPhotoTapListener() != null) {
final RectF displayRect = mAttacher.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();
mAttacher.getOnPhotoTapListener().onPhotoTap(draweeView, xResult, yResult);
return true;
}
}
}
if (mAttacher.getOnViewTapListener() != null) {
mAttacher.getOnViewTapListener().onViewTap(draweeView, e.getX(), e.getY());
return true;
}
return false;
}
//双击事件处理
@Override public boolean onDoubleTap(MotionEvent event) {
if (mAttacher == null) {
return false;
}
try {
float scale = mAttacher.getScale();
float x = event.getX();
float y = event.getY();
// 根据当前状态,来进行对应的比例变换,默认有三中状态-->min, mid, max
if (scale < mAttacher.getMediumScale()) {
mAttacher.setScale(mAttacher.getMediumScale(), x, y, true);
} else if (scale >= mAttacher.getMediumScale() && scale < mAttacher.getMaximumScale()) {
mAttacher.setScale(mAttacher.getMaximumScale(), x, y, true);
} else {
mAttacher.setScale(mAttacher.getMinimumScale(), x, y, true);
}
} catch (Exception e) {
// Can sometimes happen when getX() and getY() is called
}
return true;
}
图片处理
一个View的图像变化,肯定离不开onDraw方法,在PhotoDraweeView中,重写了该方法,主要根据Matrix来更新界面,而对于比例缩放,平移等操作,都可以通过invalidate()方法来更新视图,以下是PhotoDraweeView中重写的onDraw方法:
protected void onDraw(@NonNull Canvas canvas) {
int saveCount = canvas.save();
if (mEnableDraweeMatrix) {
canvas.concat(mAttacher.getDrawMatrix());
}
super.onDraw(canvas);
canvas.restoreToCount(saveCount);
}
其在图像发送变化的时候,会在重绘过程中调用canvas.concat(mAttacher.getDrawMatrix())来完成canvas的更新,因此,在如何对Matrix做了处理的方法中调用invalidate()即可实现更新。
对图片的处理,我们需要使用矩阵Matrix来进行操作。通过Matrix的3*3矩阵,我们可以对图片进行scale(缩放),skew(错切),trans(平移),persp(透视)等操作。而在PhotoDaweeView中,需要处理scale和trans两个操作,对应着postScale和postTranslate两个方法。由手势处理部分可知,最后的缩放会在onScale中执行,并通过Matrix实现操作,然后执行checkMatrixAndInvalidate方法:
mMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
checkMatrixAndInvalidate();
进一步查看checkMatrixAndInvalidate的代码:
public void checkMatrixAndInvalidate() {
DraweeView<GenericDraweeHierarchy> draweeView = getDraweeView();
if (draweeView == null) {
return;
}
if (checkMatrixBounds()) {
draweeView.invalidate();
}
}
在checkMatrixBounds会判断边界,并控制平移的值,最终通过invalidate()方法去通知更新视图。这就是一个完整的图片处理过程。
这里在强调下其带动画的变换,在图片被缩小到比最小尺寸还小时,会有一个恢复的动画,而该动画的实现机制,是通过一个View去post一个runable实现的,如checkMinScale:
private void checkMinScale() {
DraweeView<GenericDraweeHierarchy> draweeView = getDraweeView();
if (draweeView == null) {
return;
}
if (getScale() < mMinScale) {
RectF rect = getDisplayRect();
if (null != rect) {
draweeView.post(new AnimatedZoomRunnable(getScale(), mMinScale, rect.centerX(),
rect.centerY()));
}
}
}
而在AnimatedZoomRunnable的run中,会不断去post该请求直到变换结束,类似一个while循环:
@Override public void run() {
……
onScale(deltaScale, mFocalX, mFocalY);();
if (t < 1f) {
postOnAnimation(draweeView, this);
}
}
//post最终通过View的hanlder去执行
private void postOnAnimation(View view, Runnable runnable) {
if (Build.VERSION.SDK_INT >= 16) {
view.postOnAnimation(runnable);
} else {
view.postDelayed(runnable, 16L);
}
}