支援點選互動的Lottie-iOS篇

語言: CN / TW / HK

0x0 背景

Hernan Torrisi在2015年創建出Bodymovin外掛,讓AE能夠匯出JSON描述的動畫。2017年Airbnb工程師編寫了可以渲染這個JSON檔案的iOS和Android庫,從此Lottie成為了實現複雜動畫的首選方案,風靡移動客戶端。

一般來說,動畫的元素和樣式由Lottie描述檔案唯一確定,一旦產出交付便不能更改。常見的Lottie使用場景中,畫布組成元素固定,部分元素有動畫效果,例如支付寶在新春紅包、雙十一的首頁氛圍動圖。

營銷活動中,極為常見的就是炫目的紅包彈窗,漂亮的動畫對點選率和核銷率有明顯的提升效果。例如上圖示例中,多元素的位移、縮放、炫光、回彈等複雜的綜合動畫效果,非常精美流暢。這樣複雜的動畫,用UIKit或CoreAnimation直接實現的溝通、編碼成本非常巨大,動效設計師使用AE交付Lottie幾乎是唯一的選擇。

我們首先遇到的問題,就是展示資料由服務端下發,不同使用者展現的內容不一樣。為實現該場景下的動畫需求,本文基於Lottie做了一些探索,希望給讀者一些借鑑。

0x1 方案探索

  • 1.固定元素用動畫,差異元素用原生,硬湊組合
    • 缺點:何時應該顯示原生控制元件,以及如何讓原生控制元件貼合Lottie動畫其它元素的行為軌跡,計算和除錯要花費大量的時間。
  • 2.Lottie檔案對差異資料預留佔位,服務端資料返回後替換檔案中的佔位字串
    • 缺點:需要替換的文字內容比較多,部分需要替換的是圖片,增加了Lottie出稿的設計成本和替換理解成本
  • 3.同層混合渲染
    • 前兩種方案中,第一種方案我們在生產使用過並且深感不便,第二種方案評估後就放棄了。對於同層混合渲染的想法,一方面受到了小程式的啟示,小程式將UI控制元件跟Webkit的DOM元素都能良好的混合,本身就是Native實現的Lottie理論上能跟UI控制元件更好的協同工作。

0x2 同層介面渲染

1.前置知識

  • Lottie動畫控制元件的檢視結構

Lottie動畫將所有元素打平後,繪製在同一個UIView子類中,這個UIView沒有任何subviews。但CALayer層級非常豐富,例如下圖中的小船載入動畫共包含有271個CALayer。

  • LottieSDK對JSON圖層的轉換策略和圖層持有關係

AE的圖層包括舞臺中的普通圖層和不在舞臺中的預合成圖層,客戶端解析後一一對應iOS的CALayer類例項。LottieView的compContainer作為根Layer,直接或間接持有其它所有圖層,一級子圖層存在屬性childLayers和childMap中。而childLayers元素也有childLayers和childMap屬性,去持有引用或包含的其它圖層。

  • iOS檢視控制元件的組成

-w445

iOS原生框架UIKit中,所有的檢視控制元件都繼承自UIView類,UIView由CALayer+UIResponder觸控響應組成。當不需要互動時,CALayer能完成所有UIView能實現的視覺呈現。UIView持有CALayer,CALayer的delegate指向UIView。

2.具體實現

如前所述,CALayer負責UIView中的展示部分。直接將自定義繪製檢視的CALayer,如customedView.layer,使用系統方法-[CALayer addSublayer:]新增到指定要替換的影象所在Layer,即可完成展示內容的混合。

  • LottieSDK能力

http://airbnb.io/lottie/#/ios?id=adding-subviews

存在的問題:

1.由於SDK新增自定義View的方法中,“.”用於表示搜尋深度,如“Layer.Shape Group.Stroke 1.Color”,因此搜尋圖層時,圖層名中的"."後部分被忽略,即圖層命名不能使用"卡.png"

2.搜尋路徑只支援舞臺中的普通圖層,不能搜尋到預合成圖層,git issue沒有修復計劃:http://github.com/airbnb/lottie-ios/issues/1206

  • 同層能力擴充套件

鑑於SDK能力支援的缺陷,需要擴充套件開發增加新增自定義View的方法介面,圖層新增邏輯與SDK方法相同,主要更改目標圖層搜尋方法。

如下程式碼所示,先用LottieSDK方法搜尋一次目標圖層,如果沒有搜尋到,再遞迴遍歷Lottie根圖層持有的子圖層樹(包括預合成圖層)。

```

  • (void)addSubview:(nonnull UIView )view withLayerName:(nonnull NSString )layerName { CALayer layer = [self searchWithLayerName:layerName]; if (layer) { [self _layoutAndForceUpdate]; CGRect viewRect = view.frame; LOTView wrapperView = [[LOTView alloc] initWithFrame:viewRect]; view.frame = view.bounds; view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [wrapperView addSubview:view]; [self addSubview:wrapperView]; layer.contents = nil; [layer addSublayer:wrapperView.layer]; [self.mixLayers addObject:view.layer]; } }

  • (CALayer )searchWithLayerName:(NSString )layerName { CALayer *layer = [self normalSearchWithLayerName:layerName]; if (!layer) { layer = [self customSearchWithLayerName:layerName]; } return layer; }

  • (CALayer )normalSearchWithLayerName:(NSString )layerName { LOTCompositionContainer _compContainer = [self valueForKey:@"_compContainer"]; LOTKeypath keypath = [LOTKeypath keypathWithString:layerName]; CALayer *layer = [_compContainer _layerForKeypath:keypath]; return layer; }

  • (CALayer )customSearchWithLayerName:(NSString )layerName { LOTCompositionContainer _compContainer = [self valueForKey:@"_compContainer"]; LOTLayerContainer layerContainer = [self traversLayer:_compContainer withLayerName:layerName]; return layerContainer.wrapperLayer; }

  • (LOTLayerContainer )traversLayer:(LOTCompositionContainer )layer withLayerName:(NSString )layerName { if ([layer.layerName isEqualToString:layerName]) { return layer; } else if ([layer isKindOfClass:[LOTCompositionContainer class]]) { for (LOTCompositionContainer sublayer in layer.childLayers) { LOTLayerContainer *targetLayer = [self traversLayer:sublayer withLayerName:layerName]; if (targetLayer) { return targetLayer; } } } return nil; } ```

0x3 同層元素互動

iOS點選響應主要依賴Hit-Testing事件的傳遞鏈,因此只要在點選LottieView時,判斷點選區域在自定義View的Layer內(LottieView檢視層級均為Layer,不嵌入自定義View本身),將Hit-Testing事件直接傳給Layer的delegate自定義View,由自定義View繼續在其內部傳遞Hit-Testing事件即可。

如上所述,問題的關鍵在於“點選LottieView時,如何判斷點選區域是否在自定義View內”。

1.點選區域判斷

  • 方案1 使用iOS系統方法-(CALayer *)[CALayer  hitTest:(CGPoint)]

使用系統方法能確保準確找到觸控的圖層,問題:當自定義View的Layer之上有透明圖層或者帶透明區域的png圖片圖層時,自定義View不能命中查詢(能看到按鈕但不能點選響應,違反使用者直覺判斷)。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { CGPoint newPoint = [self convertPoint:point toView:self.superview]; CALayer *layer = [self.layer.presentationLayer hitTest:newPoint].modelLayer; UIView *view = (UIView *)layer.delegate; if ([layer.delegate isKindOfClass:[UIView class]]) { return [view hitTest:newPoint withEvent:event];; } return [super hitTest:point withEvent:event]; }

  • 方案2 只遍歷新增的自定義View,判斷CGPoint包含關係

如程式碼所示,逆序判斷新增的自定義view,即當觸控點在多個自定義view內時,後新增的自定義view接收該點選。問題:當自定義view被可見的lottie圖層遮擋,但觸控點在自定義view內時,誤判為自定義view需要響應該點選(看不到的按鈕響應了該點選,違反使用者直覺判斷)。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { NSArray *reversedItems = [[self.mixLayers reverseObjectEnumerator] allObjects]; for (CALayer *layer in reversedItems) { CGPoint newPoint = [self.layer.presentationLayer convertPoint:point toLayer:layer.presentationLayer]; if ([layer.presentationLayer containsPoint:newPoint] && [layer.delegate isKindOfClass:[UIView class]]) { return [(UIView *)layer.delegate hitTest:newPoint withEvent:event]; } } return [super hitTest:point withEvent:event]; }

  • 方案選擇

經過內部討論,我們採用了方案2。而“看不到的按鈕響應了該點選”的問題解決,一方面由動效設計師保證可互動控制元件只當其處於最上層時才移入舞臺,另一方面由研發同學在接收該點選的邏輯中兜底處理,判斷當前Lottie動畫所在的播放幀(NSNumber *)LOTAnimationView.currentFrame,決定是否要執行響應邏輯。

2.透明控制元件處理

在實際使用中,我們遇到了一個問題,動效設計師在按鈕點選完成後,僅將按鈕透明度設成了0,但位置和大小都沒有變化。因此如下程式碼中,我們增加了透明度的判斷。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { NSArray *reversedItems = [[self.mixLayers reverseObjectEnumerator] allObjects]; for (CALayer *layer in reversedItems) { CGPoint newPoint = [self.layer.presentationLayer convertPoint:point toLayer:layer.presentationLayer]; CALayer *origLayer = layer.superlayer.superlayer; BOOL ignore4Opacity = NO; while (origLayer != self.layer && origLayer.allowsGroupOpacity) { if (origLayer.opacity < 0.1) { ignore4Opacity = YES; break; } origLayer = origLayer.superlayer; } if (!ignore4Opacity && [layer.presentationLayer containsPoint:newPoint] && [layer.delegate isKindOfClass:[UIView class]]) { return [(UIView *)layer.delegate hitTest:newPoint withEvent:event]; } } return [super hitTest:point withEvent:event]; }

0x4 總結

1.實現示例

以上視訊中,左側為Lottie原始檔案,右側為同層實現。可以看出,即使逐幀比較,自定義新增的View跟其它動畫元素也有精密的軌跡同步,並且其中的按鈕控制元件能正常響應點選操作。

2.使用場景的限制

  • 當觸控點在多個自定義view內時,後新增的自定義view接收該點選。
  • 當自定義view被可見的lottie圖層遮擋,但觸控點在自定義view內時,可能誤判為自定義view需要響應該點選(看不到的按鈕響應了該點選,違反使用者直覺判斷)。如有此種情況,業務要在target-action中根據動畫的當前播放幀,判斷是否要執行響應邏輯。
  • 目前僅驗證了UIButton按鈕點選,在自定義view中註冊target-action即可,程式碼編寫與原生開發相同。

3.後續計劃

  • 探索是否有更好的點選區域處理的方案
  • 驗證更豐富的互動場景,例如長列表、播放器等

hi, 我是快手電商的格藍

快手電商無線技術團隊正在招賢納士🎉🎉🎉! 我們是公司的核心業務線, 這裡雲集了各路高手, 也充滿了機會與挑戰. 伴隨著業務的高速發展, 團隊也在快速擴張. 歡迎各位高手加入我們, 一起創造世界級的電商產品~

熱招崗位: Android/iOS 高階開發, Android/iOS 專家, Java 架構師, 產品經理(電商背景), 測試開發... 大量 HC 等你來呦~

內部推薦請發簡歷至 >>>我們的郵箱: [email protected] <<<, 備註我的花名成功率更高哦~ 😘