1. 程式人生 > >打造開源第一 iOS 圖片瀏覽器 (支援視訊)

打造開源第一 iOS 圖片瀏覽器 (支援視訊)

iOS圖片瀏覽器 (支援視訊)

本文主要講述 YBImageBrowser 的一些功能技術細節,程式碼架構思路,設計模式選擇等,希望對元件原理感興趣的朋友有所幫助,也可以作為如何高效構建圖片瀏覽器的參考資料。

概覽

  • 一、元件的檢視層次

  • 二、面向協議的設計模式

  • 三、迪米特設計原則

  • 四、當多執行緒遇上覆用機制

  • 五、非同步任務的重複請求

  • 六、巧用觀察者設計模式

  • 七、螢幕旋轉的處理

  • 八、三方圖片處理框架的選擇

  • 九、非同步解壓的思考

  • 十、意外釋放的危機處理

  • 十一、何時將變數放入全域性區

  • 十二、巧用區域性 Block

  • 十三、手勢互動動效的技術細節

  • 十四:分頁間距的優化

閒談

圖片瀏覽器在移動端資訊流業務中有著重要的地位,它的功能設計和互動體驗都在不斷演化。知名 APP 裡的圖片瀏覽器往往能引領潮流,比如“微信”、“微博”、“今日頭條”、“知乎”、“QQ”等,它們的實現有很多相似之處,也有些設計上的瑕疵,其中互動和功能做得比較好的是“微信”和“微博”。

這裡不得不吐槽“掘金” iOS APP 蹩腳的圖片瀏覽器了,稀土掘金作為一個新興的技術分享平臺在這一點上確實讓人失望,挺久之前筆者還提過建議,但迭代了 n 次版本都未進行優化,互動體驗極差,BUG 滿天飛,讓筆者有多次想要解除安裝的衝動。

話說回來,開源社群有不少的圖片瀏覽器,不過不管是從功能上,還是程式碼質量上都不能讓筆者滿意,所以幾個月前筆者自己做了一個,開源社群的反饋還行,收穫了不少 star,不過也發現了一些問題,比如臃腫的程式碼設計難以維護,嚴重的耦合難以自定義和拓展。

所以,筆者花了挺多時間重做圖片瀏覽器,從功能、技術細節、程式碼架構都做了大量改進和優化,儘可能保證程式碼質量、提高可維護性和拓展性。

YBImageBrowser 2.x 版本已更新,如果專案中的圖片瀏覽器過於蹩腳,替換掉它吧。筆者會抽時間維護和升級,打造開源第一是追求也是激勵。

一、元件的檢視層次

考慮到螢幕旋轉的適配,筆者使用 UIViewController 作為圖片瀏覽器的主體類,同時也方便做自定義的轉場效果。內容的載體是 UICollectionView ,可以避免手動實現複用機制,並且可以優雅的管理佈局。UICollectionViewCell 作為主要顯示內容的載體,元件實現了兩個,一個支援影象,一個支援視訊。

除此之外,元件有兩個概念,一個是工具欄 (ToolBar) ,一個是彈出檢視 (SheetView)。”TooBar” 檢視層級是在內容載體 UICollectionView 之上的,元件中預設實現了一個顯示頁碼的 “TooBar”;”SheetView” 是需要的時候新增到 UIViewController 上,它的層級可以理解為元件內部最高。至於它們如何架構和自定義後文會闡述。

二、面向協議的設計模式

顯示內容的載體目前有影象和視訊,筆者先是考慮過寫一個 UICollectionViewCell 的基類,利用多型來做子類的自定義,然而這樣會帶來問題:一是若元件使用者想要拓展內容載體但卻不便於繼承這個基類;二是繼承本身帶來的問題,雖然子類之間不直接接觸,但是它們有同一個父類,若想元件和這些子類之間不直接耦合,必然要頻繁的對這個基類做更改,牽一髮而動全身,並且對於方法過載來說,不好準確的限定是否必須過載,是否需要呼叫父類方法。

繼承往往是災難的開端,所以,多型的解決方案被淘汰。

換個思路來思考,元件主體對內容載體也就是 UICollectionViewCell 的關係應該是無耦合的,就像上面多型的思路,元件只關心這個基類,而不直接和子類互動。我們無非是想遵守依賴倒置原則,既然想到這個設計原則,很容易想到面向協議的設計模式。

所以,筆者在元件中建立了數個協議:

YBImageBrowserCellDataProtocol.hYBImageBrowserCellProtocol.hYBImageBrowserToolBarProtocol.hYBImageBrowserSheetViewProtocol.h

正如你所見,對於 “ToolBar” 和 “SheetView” 都有獨自的協議。元件主體和這些檢視都與協議耦合而不依賴對方,筆者可以優雅的移除或者新增檢視元素,使用者也可以輕鬆的實現這些協議來自定義介面。

“我不關心你是不是鴨子,只要你會‘嘎嘎’叫並且有兩隻腳我把你當做鴨子”。

三、迪米特設計原則

在元件設計中,應該儘量遵循迪米特原則,在 OC 程式設計中會存在一個問題,屬性和方法沒有 protect,寫在 .h 中的是公開的,寫在 .m 中是私有的,所以對於某個物件來說,其子類和其它類的訪問許可權可以說是一樣的。

解決這個問題的方案有幾種,最簡單的是將兩個類的實現寫在同一個檔案,但是很多時候不希望這麼做;筆者之前的版本中使用過objc_msgSend直接傳送訊息,也使用過 KVC 直接訪問例項變數,雖然從效率的角度來看無傷大雅,使用 Runtime 甚至更快,但是程式碼卻有些晦澀。

最終筆者選擇了一種比較優雅的方式,使用獨立檔案的延展 (Extension) 來做“知識”隔離控制:

檔案:[email protected] YBImageBrowser ()@property (nonatomic, strong) YBImageBrowserView *browserView;@end

YBImageBrowser+Internal.h 延展雖然是一個獨立的檔案,但是仍然是 YBImageBrowser 類的一部分,裡面的方法和屬性都是在編譯期決議的,所以延展裡面的屬性是會自動生成例項變數的。這不同於分類 (Category) ,分類是執行期動態注入類中,所以只能新增方法而不能新增例項變數。

那麼,在需要呼叫這些方法的類中匯入 YBImageBrowser+Internal.h 就能訪問了。

四、當多執行緒遇上覆用機制

多執行緒和複用機制看似互不相干,卻會碰撞出意外的 BUG。

舉個例子,一個 Cell 中的 UIImageView 在非同步執行緒發起一個下載圖片的網路請求,UITableView 在這期間滑動,觸發了複用機制,該 Cell 的資料來源更換,它的 UIImageView 又發起了另外的一個下載圖片請求,當第一次網路請求成功返回圖片的時候,已經不是這個 Cell 的 UIImageView 期望的圖片了。

因為複用機制的問題,檢視不能作為可信的非同步回撥接收者,但是資料卻可以:

id tmpData = self.datanetworkAsyc^{    if (tmpData == self.data) {        update UI.    }}

在 UITableView 滑動的時候,會不斷的為 Cell 更新資料來源 data,所以 cell.data 表示的就是 Cell 當前的資料狀態,建立一個臨時變數讓 Block 持有它,這個臨時變數就是非同步網路請求所對應的資料。

這應該是最簡單的處理方案。SDWebImage 是為 UIImageView 動態關聯一個請求標識來判定最新的網路請求 URL,YYWebImage 是為 UIImageView 計數,通過非同步回調回來的計數和區域性計數變數比較來判定。

但是元件中並沒有使用這種方法,而是使用了觀察者設計模式來巧妙解決,後文會講解。

五、非同步任務的重複請求

對於圖片瀏覽器每一個影象,都有一個數據模型 data,當非同步操作回撥過後,雖然可以通過對比 cell.data 和 block 持有的 data 來判斷是否需要進行 UI 重新整理,但是卻不能解決另外一些問題:

1、當 Cell 進入複用池的時候,是否需要放棄它發起的未完成的非同步操作?

當然,並不是所有非同步任務都是可以中斷的,發起的非同步操作消耗了一定資源,筆者認為不應該放棄掉,而是將結果儲存在非同步回撥 Block 持有的 data 中,至於 UI 重新整理與否按照之前說的方法判斷。

那麼就帶來了另外一個問題:

2、當來回滑動 ScrollView,如何避免 Cell 反覆發起非同步請求?

這種情況經常出現,如果脫離業務來思考,對於一個同一個非同步請求多次呼叫,應該使用一個數組來將所有發起請求的 Block 回撥儲存起來,並且若正在非同步請求要及時返回,當非同步請求完成,遍歷陣列中的回撥 Block 分別呼叫。

實際上關於網路的框架都有類似的處理,比如 AFNetworking、SDWebImage 之類,它們可以通過 URL 來判斷是否是重複的請求。

落地到圖片瀏覽器中,若想判斷某個非同步請求是否是同一個,通過請求引數來判斷有些複雜,最直接的方法就是把非同步請求都寫在 data 中,比如圖片壓縮非同步請求,對於同一個 data 就很好判斷是否正在壓縮,只需要一個 BOOL 值。

在圖片瀏覽器的功能設計中,筆者加入了預載入的功能,也就是說,data 中的這些非同步操作並不都是在顯示介面的時候由 cell 來呼叫,而是在建立 data 的時候就會呼叫。

比如在建立網路圖片 data 的時候,就要發起非同步請求下載圖片,而當圖片瀏覽器展示當前 data 對應的 cell 的時候,非同步請求還未完成,cell 又呼叫 data 發起了相同的非同步請求。這時候在非同步請求中就要用一個指標儲存這個 cell 發起非同步請求的回撥 Block,在非同步請求成功的時候呼叫這個 Block,這帶來了潛在的迴圈引用問題,並且程式碼觀感非常差。

並且實際情況比這個更為複雜,在筆者的圖片瀏覽器中,一個 data 需要進行的非同步請求可能有好幾個,比如非同步查詢快取、非同步解壓、非同步下載、非同步壓縮、非同步裁剪,若統統使用這種方式處理,將會是程式碼維護的災難。

六、巧用觀察者設計模式

問題的本質就是,data 中的非同步任務結果要在 cell 需要的時候通知它,而在 cell 不需要的時候默默執行。

筆者最終決定採用觀察者模式,考慮到業務的特殊性,對於同一個 data,基本上非同步操作是串聯的,也就是說,不會在下載的同時非同步壓縮,不會在非同步查詢快取的時候下載。所以,基本上同一時刻,data 的狀態是唯一的,如此,對於元件中的 YBImageBrowseCellData,定製了一系列的狀態:

typedef NS_ENUM(NSInteger, YBImageBrowseCellDataState) {    YBImageBrowseCellDataStateInvalid,    YBImageBrowseCellDataStateImageReady,    ...    YBImageBrowseCellDataStateIsDownloading,    YBImageBrowseCellDataStateDownloadProcess,    YBImageBrowseCellDataStateDownloadSuccess,    YBImageBrowseCellDataStateDownloadFailed,};

在非同步請求的過程中,更新這些狀態。

而對於 cell,只需要在賦值 data 的時候觀察這個 state,在進入複用池等情況移除就行了。state 改變的時候,就做一些 UI 操作,比如 YBImageBrowseCellDataStateDownloadProcess 更新下載進度條,在YBImageBrowseCellDataStateDownloadFailed 顯示下載失敗文案。

這是觀察者模式比較好的實踐,但有一點需要注意,若有某些非同步任務不是串聯的,需要設定另外一個 state 列舉。

七、螢幕旋轉的處理

有兩個概念,一個是裝置的方向通過 UIDeviceOrientationDidChangeNotification 新增通知,一個是狀態列的方向通過 UIApplicationDidChangeStatusBarOrientationNotification 新增通知。

通常情況下,狀態列的方向可以確定當前控制器的佈局方向,所以通過監聽狀態列的方向更新子檢視的佈局。

元件採用 UIViewController 作為主體,通過重寫如下方法自定義旋轉方向:

- (BOOL)shouldAutorotate {    return YES;}- (UIInterfaceOrientationMask)supportedInterfaceOrientations {    return self.supportedOrientations;}

但其實當前控制器實際允許旋轉的方向受很多因素控制。一是 general -> deployment info -> Device Orientation 中勾選的裝置支援的旋轉方向,它的優先順序是最高的;二是在 AppDelegate 中實現的 代理方法 -application:supportedInterfaceOrientationsForWindow:,它的優先順序次之;三是若當前控制器是棧內的,它的旋轉方向由 UINavagationController 過載的 -shouldAutorotate 和 -supportedInterfaceOrientations 方法控制,若存在 UITabBarController,它將控制它管理的那些控制器的旋轉方向。

所以,實際上元件內部可以說無法準確的獲取到 YBImageBrowser 這個控制器實際支援的方向,這些邏輯需要開發者自行去解決。

TODO

關於自定義轉場,需要設定如下程式碼:

self.transitioningDelegate = ...;self.modalPresentationStyle = UIModalPresentationCustom;

UIModalPresentationCustom 模式下,才能做到完美的出場和入場動效,但是有個非常蛋疼的地方,若在該模式下,圖片瀏覽器旋轉的時候,它的 presentingViewController 會跟著旋轉,不管 presentingViewController 是否支援這個方向。然後在圖片瀏覽器 dismiss 的時候,presentingViewController 方向並不會恢復。

這個問題筆者未找到完美的解決方案,看了一下“微博”的圖片瀏覽器貌似也是類似的實現方式,在橫屏的時候出場是立即觸發的,猜測可能是此刻將螢幕旋轉回來。

所以,嘗試了一下,若當前圖片瀏覽器的方向和 presentingViewController 起始的方向不同,將取消手勢互動動效,直接 dimiss 轉場,並且在轉場的同時強制旋轉螢幕。

然而預期的效果和“微博”並不一樣,強制轉場有一定的延時。若讀者朋友有解決方案還望指點一下,目前就採用這個處理方案,作為一個待完成的優化吧。

八、三方圖片處理框架的選擇

上一個版本是使用 SDWebImage + FLAnimatedImage 來處理的,但是感覺使用體驗不太好,在建立本地圖片的時候需要使用者判斷當前圖片是不是 gif,所以後來筆者選擇了功能更強、程式碼質量很高的 YYImage 做為 GIF 的處理框架,它還支援 APNG、WebP 等格式,使用也很簡單,完全相容 UIImage。YYImage 原理可看筆者的一篇部落格:YYImage 原始碼剖析:圖片處理技巧。

吐槽一下 SDWebImage 蹩腳的快取設計

它的記憶體快取就是一個 hash 容器,沒有快取策略,不及基於 LRU 淘汰演算法的 YYMemeryCache。

SDWebImage 快取策略中有一個邏輯,在磁碟快取中查詢到了快取,會解壓過後放入記憶體快取,若這個圖片是 GIF 的,它就會解壓為第一幀圖片,不能滿足我們的需求。

從解壓過後是否放入快取說起:它是由 [SDImageCache sharedImageCache].config.shouldCacheImagesInMemory 決定的,所以一開始我想要在框架生命週期內禁止它。然而 shouldCacheImagesInMemory 同時決定了呼叫 -stroreImage:imageData:forKey:toDisk 的時候是否快取到記憶體,所以這個屬性是不能設定為 NO 的,否則記憶體快取永遠存不進去。

發現了麼,死迴圈,要想 -stroreImage:imageData:forKey:toDisk 支援記憶體快取,就要 shouldCacheImagesInMemory 為 YES,而它為 YES 就會錯誤的同步 GIF 的第一幀到記憶體快取。

以 SD 的思路,最好的解決方案就是使用 SDWebImage 的 GIF 分類 + FLAnimatedImage 顯示了,SD 解壓的 GIF 圖片型別可以由 FLAnimatedImageView 解析。這個設計讓我有些無語,有種捆綁銷售的感覺,在這個需求下,SD 的拓展性做得不太友好。

之所以選擇 SDWebImage 是因為它的人氣最高,並且長期有人維護,然而我又捨不得放棄強大的 YYImage,所以目前的處理方式就是放棄記憶體快取,每次從磁碟查詢。

然而筆者在 SD 新增快取的原始碼中又看到了這樣一個出其不意的判斷:

- (void)storeImage:(nullable UIImage *)image         imageData:(nullable NSData *)imageData            forKey:(nullable NSString *)key            toDisk:(BOOL)toDisk        completion:(nullable SDWebImageNoParamsBlock)completionBlock {    if (!image || !key) {        if (completionBlock) {            completionBlock();        }        return;    }...

在 image 不存在的時候,居然直接返回,這不得不讓元件在下載完成的時候同時傳入 NSData 和 UIImage 物件,然後 SD 就會做磁碟和記憶體快取。然而,元件內部暫時又不需要記憶體快取。

SDWebImage 快取方面的拓展性確實不能讓人滿意,也堅定了筆者替換掉它的想法,在後面的版本中,考慮的是用 YYWebImage 替換它,雖然 YYWebImage 很久不維護了,使用的時候可能需要做一些原始碼調整,但是能逃脫 SDWebImage 的魔爪這個成本還是可以接受的。

快取共享問題

元件用到了快取,而開發者自己的業務中同樣用到了快取,它們之間如何共享是一個問題,若是用的同一個快取框架還好說,若不是就比較麻煩了。因為不同的圖片處理框架對快取的處理或多或少有些差別,很多時候通過上層的 API 做不到聯合查詢快取,所以關於這個待優化的功能,筆者還需要考慮一些時間。

下載框架的替換問題

用 SDWebImage 或 YYWebImage 的開發者總是看不上另一個框架,這也是個惱人的問題,若筆者自己實現卻又感覺成本太高,這個問題同樣需要考量一下。

九、非同步解壓的思考

另外值得一提的是,圖片瀏覽器做了高清圖的壓縮和裁剪,所以只要框架使用者不去改變這個臨界值,圖片繪製基本上不會消耗 CPU 和 GPU 格外的資源去裁剪高清圖,而且圖片瀏覽器同時最多有兩張圖片在介面上,解壓的壓力不大(並且高清圖元件已經解壓了圖片),所以對圖片的解壓和繪製並不會成為效能瓶頸。

那麼,對於業界提高圖片繪製效能的常用做法:非同步解壓,圖片瀏覽器就不再需要,當資料模型都被圖片瀏覽器持有,且圖片都比較大時,非同步解壓快取的記憶體無法及時釋放,甚至還會造成記憶體的過多負擔。由此,筆者取消了 SDWebImage 所有非同步解壓操作;將 YYImage 複製到 YBImage,把非同步解壓邏輯取消掉,並且便於以後的自定義。

十、意外釋放的危機處理

就比如 UIViewController,它並不是每次釋放都會走 -viewWillAppear: 方法,可能記憶體強制清理或者閃退等導致意外釋放。只要是釋放,理論上就會走 -dealloc 方法,所以在這個方法中需要做一些危機處理。

在元件的設計中,應該儘量避免對外部業務的直接操作,但是有的時候又不可避免,比如圖片瀏覽器要做這個效果:

圖片瀏覽器當前展示哪張圖片就將業務外的哪張圖片隱藏,為了方便使用者使用,元件不得不操作外部檢視變數使其隱藏或者顯示。那麼,考慮到意外釋放等問題,對外部操作的復位應該寫在 -dealloc 中:

- (void)dealloc {    // If the current instance is released (possibly uncontrollable release), we need to restore the changes to external business.    [YBIBWebImageManager restoreOutsideConfiguration];    self.hiddenSourceObject = nil;}

-restoreOutsideConfiguration 方法是恢復對三方元件的修改,-setHiddenSourceObject 方法就是對外部隱藏的圖片的復位。

十一、何時將變數放入全域性區

YBImageBrowseCellData 是元件處理圖片的資料來源,它不應該和 YBImageBrowser 耦合,甚至 YBImageBrowser 都不應該知道它的存在,那麼,對於 YBImageBrowseCellData 的全域性配置如何做?答案就是使用全域性變數:

@property (nonatomic, class) YBImageBrowseFillType globalVerticalfillType;@property (nonatomic, assign) YBImageBrowseFillType verticalfillType;

對於縱向的填充型別,同時包含例項變數和全域性變數,全域性變數針對所有的 YBImageBrowseCellData 例項,而例項變數針對某一個,這是元件內部常用的伎倆。

值得注意的是,全域性區變數生命週期會延長到程式結束,所以對於記憶體佔用比較高的變數需要慎重考慮是否放入全域性區,或者手動管理它的記憶體釋放。

十二、巧用區域性 Block

經常會有一些需求,比如某段動畫可以選擇是否執行,可以如下處理:

    void (^animationsBlock)(void) = ^{        ...    };    void (^completionBlock)(BOOL) = ^(BOOL x){        ...    };    if (duration <= 0) {        animationsBlock();        completionBlock(YES);    } else {        [UIView animateWithDuration:duration animations:animationsBlock completion:completionBlock];    }

建立兩個棧區的 Block,若需要動畫就傳入 -animateWithDuration: 系列方法,若不需要動畫 Block 就不用被拷貝到堆區,而是直接呼叫。這樣處理還有一個好處就是不用重複寫兩個 Block 中的業務邏輯了,避免格外的方法封裝。

十三、手勢互動動效的技術細節

圖片瀏覽器的手勢互動並非看起來的那麼簡單,圖片的放大狀態、UIScrollView 的回彈和減速機制、巢狀 UIScrollView 的手勢衝突,這些都可能會導致一些難以控制的情況出現。

手勢互動效果的實現載體

“微博”的圖片瀏覽器在手勢互動的時候應該是藉助了其它的檢視,因為每次對 GIF 的拖動都會回到第一幀,這樣體驗並不是非常好;而“今日頭條”的圖片瀏覽器在手勢互動的時候 GIF 會暫停,一開始筆者還以為在 runloopMode 為 UITrackingRunLoopMode 的時候停止了 GIF 動圖播放,然而當手勢互動結束時,GIF 的播放位置發生了變化,可以確定播放 GIF 的 runloopMode 仍然是 NSRunLoopCommonModes,只是藉助了其他檢視做動效。

綜上,“微博”和“今日頭條”的互動設計都不太完美。

一個好的動效應該儘量減少不必要的額外檢視和邏輯,所以筆者通過對 cell.contentView 的操作來實現拖動動效,並且 GIF 的播放 runloopMode 為 NSRunLoopCommonModes ,所以在拖動的時候 GIF 仍然會播放,這樣保證最佳的使用者體驗。對視訊的互動的處理方式基本是一樣的,在拖動的時候視訊仍然能播放。

手勢互動移動縮放的演算法實現

實際上在上個版本的程式碼中,YBImageBrowser 使用了一個稍顯複雜的演算法來實現圖片移動的同時縮放,後來筆者實踐了一種更為簡潔的方法,優雅了許多:

CGRect startFrame = ...;CGFloat anchorX = point.x / startFrame.size.width,anchorY = point.y / startFrame.size.height;self.mainContentView.layer.anchorPoint = CGPointMake(anchorX, anchorY);

實際上就是將觸發互動的那個 point 作為動畫檢視的錨點,然後更新動畫只需要通過觸控點更新 center、藉助 CGAffineTransform 實現縮放就行了,互動移動縮放的效果算是比較完美了。

手勢互動觸發點的優化

手勢互動動效一旦觸發,就要讓兩個 UIScrollView 禁止滑動,所以這個觸發點不能過於靈敏,不然使用者切換圖片的時候會一不小心觸發。

大致的處理如下(虛擬碼):

BOOL can = ABS(currentPoint.x - startPoint.x) > triggerDistance && ABS(currentPoint.y - startPoint.y) < triggerDistance;

可以理解為:當用戶拖動離垂直方向最小角度的絕對值小於 45° 的時候就會允許觸發。這樣也同時解決了超清大圖展示的時候,在邊緣拖動頻繁觸發手勢互動動效的問題。

如此處理過後,當用戶快速滑動切換圖片的時候,還是經常會觸發手勢互動動效,測試發現當拖動速度過快,panGesture 響應的 point 並非絕對的準確,所以筆者索性加入了一個速度判斷(虛擬碼):

CGPoint velocityPoint = [panGesture velocityInView:...];BOOL can = ABS(velocityPoint.x) < 500;

至此,觸發點的問題基本解決。

十四:分頁間距的優化

分頁間距,作者做過好幾次方案,都或多或少有些問題,後來思考了一下,做了一個比較完美的效果:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {    NSArray *layoutAttsArray = [[NSArray alloc] initWithArray:[super layoutAttributesForElementsInRect:rect] copyItems:YES];    CGFloat halfWidth = self.collectionView.bounds.size.width / 2.0;    CGFloat centerX = self.collectionView.contentOffset.x + halfWidth;    [layoutAttsArray enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {        obj.center = CGPointMake(obj.center.x + (obj.center.x - centerX) / halfWidth * self.distanceBetweenPages / 2, obj.center.y);    }];    return layoutAttsArray;}

一句話概括:離螢幕中心越遠,Item 的中心點偏移越多。

實際上對於 UICollectionView 的自定義 layout,只需要時刻記住一個準則就不會出現問題:

佈局的更新一定是線性的,而不能跳躍。

後語

一個看起來簡單的效果並非真的簡單,當你覺得它簡單的時候,思考一下是不是自己太菜,每一個問題深入過後都有很多衍生的東西,周全考慮效能、記憶體、可維護性、可拓展性是對程式碼架構能力的考量。

希望本文能給讀者朋友帶來幫助。

作者:indulge_in