支持點擊交互的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沒有修復計劃:https://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] <<<, 備註我的花名成功率更高哦~ 😘