Android 開源專案原始碼解析 -->PhotoView 原始碼解析(七)

Wei_Leng發表於2016-09-27
專案: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 原始碼:

 /**
 * @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 中英文註釋:

     * 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).

對應的程式碼:

    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 介面回撥的實現方法.

實際完成縮放效果.

核心程式碼:

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 狀態:

    mSuppMatrix.postTranslate(dx, dy);
內部類 AnimatedZoomRunnable

實現雙擊時的 縮放動畫.

作用機制基本同上.

區別是 AnimatedZoomRunnable 的執行進度由 AccelerateDecelerateInterpolator 控制.

對 Interpolator 沒有概念的可以參閱官方 Demo Interpolator

你也可以簡單認為這就是一個動畫進度控制器.

核心邏輯依然很簡單,根據動畫進度縮小/放大圖片

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

原理

如果你對矩陣變換背後的數學原理感興趣且線性代數的內容沒忘光的話,推薦這篇 文章.



相關文章