前言
原理
PhotoDraweeView是基于PhotoView的設(shè)計(jì)思路實(shí)現(xiàn)的牺丙,其存在的意義是彌補(bǔ)PhotoView不支持Fresco的不足症概。PhotoDraweeView繼承自SimpleDraweeView個钢猛,實(shí)現(xiàn)IAttacher接口蒙畴,并重寫了onDraw來更新視圖胁附,通過Matrix來實(shí)現(xiàn)圖片的變換酪耕,以及通過重寫onTouch來處理手勢导梆。我們可以看一下其構(gòu)造函數(shù):
public PhotoDraweeView(Context context) {
super(context);
init();
}
……
protected void init() {
if (mAttacher == null || mAttacher.getDraweeView() == null) {
mAttacher = new Attacher(this);
}
}
其實(shí)例化了一個Attacher,該類負(fù)責(zé)對圖片和手勢的處理相關(guān)操作迂烁,我們主要關(guān)注在于以下兩點(diǎn):
- 手勢處理
- 圖片處理
手勢處理
在PhotoDraweeView中看尼,手勢的相關(guān)處理交由了ScaleDragDetector和GestureDetectorCompat兩個類來管理,前者負(fù)責(zé)對多點(diǎn)觸發(fā)比例縮放和滑動拖拽等操作做處理盟步,后者主要接管了單擊和雙擊事件的處理藏斩。
相關(guān)的類的關(guān)系圖如下:
對于手勢的處理,根據(jù)對View的事件方法機(jī)制的學(xué)習(xí)却盘,我們可以知道狰域,一個手勢事件MotionEvent最終會onTouch的相關(guān)方法中處理,而在Attacher中黄橘,對DraweeView設(shè)置了OnTouchListener兆览,其接口的onTouch在PhotoDraweeView中實(shí)現(xiàn):
@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事件沖突的時候的相關(guān)操作。然后進(jìn)一步調(diào)用了ScaleDragDetector和GestureDetector兩個對象來對手勢事件進(jìn)行處理描孟。
ScaleDragDetector
該類實(shí)現(xiàn)了OnScaleGestureListener驶睦,同時持有OnScaleDragGestureListener接口(該接口在PhotoDraweeView中實(shí)現(xiàn)),其負(fù)責(zé)了對手勢MotionEvent的初步拆解匿醒,并記錄一些點(diǎn)的數(shù)據(jù)。所有的事件處理都集中在了該類的onTouchEvent(手勢操作必經(jīng)之路):
public boolean onTouchEvent(MotionEvent ev) {
//數(shù)據(jù)傳遞到OnScaleGestureListener接口缠导,最終還是傳遞到OnScaleDragGestureListener中
mScaleDetector.onTouchEvent(ev);
final int action = MotionEventCompat.getActionMasked(ev);
//獲取觸摸點(diǎn)的位置廉羔,多點(diǎn)取第一個點(diǎn)位置
onTouchActivePointer(action, ev);
//對手勢進(jìn)行拆解,分配到OnScaleDragGestureListener里的相應(yīng)方法中處理
onTouchDragEvent(action, ev);
return true;
}
所以事件的最后還是傳遞到了OnScaleDragGestureListener中僻造。在經(jīng)過對手勢的處理判斷憋他,就可以根據(jù)結(jié)果來分別觸發(fā)該接口中的方法孩饼。該接口包含以下方法:
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);
//變化結(jié)束
void onScaleEnd();
}
接下來的事就變的簡單了,主要在個接口方法中做些簡單處理竹挡,然后傳遞到Matrix的對應(yīng)方法中即可實(shí)現(xiàn)圖像的變換镀娶。以onScale方法為例,其會先做些判斷揪罕,然后講變換的數(shù)據(jù)傳遞給其它回調(diào)接口梯码,最后通過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
在實(shí)際使用過程中好啰,用戶可以通過雙擊屏幕來進(jìn)行對圖片的縮小和放大轩娶,該接口就是用于響應(yīng)做些點(diǎn)擊事件的。在PhotoDraweeView中提供了一個默認(rèn)實(shí)現(xiàn)類DefaultOnDoubleTapListener框往,該類主要處理了如下兩個接口方法:
//單擊事件
@Override public boolean onSingleTapConfirmed(MotionEvent e) {
……
//根據(jù)點(diǎn)的位置來確定點(diǎn)擊觸發(fā)的是圖片還是整個View(存在圖片區(qū)域小于View的情況)鳄抒,然后取調(diào)用不同的接口取處理。
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();
// 根據(jù)當(dāng)前狀態(tài)椰弊,來進(jìn)行對應(yīng)的比例變換许溅,默認(rèn)有三中狀態(tài)-->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中贤重,重寫了該方法,主要根據(jù)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);
}
其在圖像發(fā)送變化的時候借卧,會在重繪過程中調(diào)用canvas.concat(mAttacher.getDrawMatrix())來完成canvas的更新,因此筛峭,在如何對Matrix做了處理的方法中調(diào)用invalidate()即可實(shí)現(xiàn)更新铐刘。
對圖片的處理,我們需要使用矩陣Matrix來進(jìn)行操作影晓。通過Matrix的3*3矩陣镰吵,我們可以對圖片進(jìn)行scale(縮放),skew(錯切)挂签,trans(平移)疤祭,persp(透視)等操作。而在PhotoDaweeView中饵婆,需要處理scale和trans兩個操作勺馆,對應(yīng)著postScale和postTranslate兩個方法。由手勢處理部分可知,最后的縮放會在onScale中執(zhí)行草穆,并通過Matrix實(shí)現(xiàn)操作灌灾,然后執(zhí)行checkMatrixAndInvalidate方法:
mMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
checkMatrixAndInvalidate();
進(jìn)一步查看checkMatrixAndInvalidate的代碼:
public void checkMatrixAndInvalidate() {
DraweeView<GenericDraweeHierarchy> draweeView = getDraweeView();
if (draweeView == null) {
return;
}
if (checkMatrixBounds()) {
draweeView.invalidate();
}
}
在checkMatrixBounds會判斷邊界,并控制平移的值悲柱,最終通過invalidate()方法去通知更新視圖锋喜。這就是一個完整的圖片處理過程。
這里在強(qiáng)調(diào)下其帶動畫的變換豌鸡,在圖片被縮小到比最小尺寸還小時嘿般,會有一個恢復(fù)的動畫,而該動畫的實(shí)現(xiàn)機(jī)制直颅,是通過一個View去post一個runable實(shí)現(xiàn)的博个,如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該請求直到變換結(jié)束功偿,類似一個while循環(huán):
@Override public void run() {
……
onScale(deltaScale, mFocalX, mFocalY);();
if (t < 1f) {
postOnAnimation(draweeView, this);
}
}
//post最終通過View的hanlder去執(zhí)行
private void postOnAnimation(View view, Runnable runnable) {
if (Build.VERSION.SDK_INT >= 16) {
view.postOnAnimation(runnable);
} else {
view.postDelayed(runnable, 16L);
}
}