lottie-ios 4.1.2 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/.github/workflows/main.yml +27 -9
  2. package/Lottie.xcodeproj/project.pbxproj +158 -70
  3. package/Lottie.xcodeproj/xcuserdata/calstephens.xcuserdatad/xcschemes/xcschememanagement.plist +2 -2
  4. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/IDEFindNavigatorScopes.plist +5 -0
  5. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  6. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +258 -0
  7. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Expressions.xcexplist +13 -2
  8. package/Package.swift +2 -1
  9. package/README.md +3 -3
  10. package/Rakefile +8 -4
  11. package/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift +16 -2
  12. package/Sources/Private/CoreAnimation/Animations/CombinedShapeAnimation.swift +1 -1
  13. package/Sources/Private/CoreAnimation/Animations/CustomPathAnimation.swift +1 -1
  14. package/Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift +1 -1
  15. package/Sources/Private/CoreAnimation/Animations/GradientAnimations.swift +6 -6
  16. package/Sources/Private/CoreAnimation/Animations/LayerProperty.swift +76 -7
  17. package/Sources/Private/CoreAnimation/Animations/OpacityAnimation.swift +1 -1
  18. package/Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift +1 -1
  19. package/Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift +66 -102
  20. package/Sources/Private/CoreAnimation/Animations/StarAnimation.swift +2 -2
  21. package/Sources/Private/CoreAnimation/Animations/StrokeAnimation.swift +3 -3
  22. package/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift +66 -17
  23. package/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +55 -32
  24. package/Sources/Private/CoreAnimation/Extensions/Keyframes+combined.swift +16 -12
  25. package/Sources/Private/CoreAnimation/Layers/AnimationLayer.swift +3 -3
  26. package/Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift +24 -11
  27. package/Sources/Private/CoreAnimation/Layers/ImageLayer.swift +2 -2
  28. package/Sources/Private/CoreAnimation/Layers/PreCompLayer.swift +1 -1
  29. package/Sources/Private/CoreAnimation/Layers/RepeaterLayer.swift +13 -2
  30. package/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift +9 -1
  31. package/Sources/Private/CoreAnimation/ValueProviderStore.swift +22 -11
  32. package/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.swift +1 -1
  33. package/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift +13 -2
  34. package/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.swift +16 -7
  35. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/EllipseNode.swift +1 -1
  36. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/PolygonNode.swift +2 -2
  37. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/RectNode.swift +1 -1
  38. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/StarNode.swift +2 -2
  39. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderContainers/GroupNode.swift +20 -8
  40. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/FillNode.swift +1 -1
  41. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientFillNode.swift +1 -1
  42. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientStrokeNode.swift +1 -1
  43. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift +1 -1
  44. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.swift +28 -9
  45. package/Sources/Private/Model/Assets/ImageAsset.swift +4 -3
  46. package/Sources/Private/Model/DotLottie/DotLottieAnimation.swift +2 -8
  47. package/Sources/Private/Model/DotLottie/DotLottieManifest.swift +3 -14
  48. package/Sources/Private/Model/DotLottie/DotLottieUtils.swift +11 -1
  49. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+BackingConfiguration.swift +147 -0
  50. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Helpers.swift +351 -0
  51. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+MemoryFile.swift +183 -0
  52. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Progress.swift +66 -0
  53. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Reading.swift +144 -0
  54. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+ReadingDeprecated.swift +49 -0
  55. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Writing.swift +385 -0
  56. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+WritingDeprecated.swift +91 -0
  57. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+ZIP64.swift +170 -0
  58. package/Sources/Private/Model/DotLottie/{Zip/ZipArchive.swift → ZipFoundation/Archive.swift} +150 -227
  59. package/Sources/Private/Model/DotLottie/ZipFoundation/Data+Compression.swift +403 -0
  60. package/Sources/Private/Model/DotLottie/ZipFoundation/Data+CompressionDeprecated.swift +44 -0
  61. package/Sources/Private/Model/DotLottie/{Zip → ZipFoundation}/Data+Serialization.swift +62 -0
  62. package/Sources/Private/Model/DotLottie/{Zip/ZipEntry+Serialization.swift → ZipFoundation/Entry+Serialization.swift} +7 -7
  63. package/Sources/Private/Model/DotLottie/{Zip/ZipEntry+ZIP64.swift → ZipFoundation/Entry+ZIP64.swift} +13 -19
  64. package/Sources/Private/Model/DotLottie/{Zip/ZipEntry.swift → ZipFoundation/Entry.swift} +141 -10
  65. package/Sources/Private/Model/DotLottie/ZipFoundation/FileManager+ZIP.swift +368 -0
  66. package/Sources/Private/Model/DotLottie/ZipFoundation/README.md +24 -0
  67. package/Sources/Private/Model/DotLottie/ZipFoundation/URL+ZIP.swift +32 -0
  68. package/Sources/Private/Model/Extensions/Bundle.swift +5 -14
  69. package/Sources/Private/Model/Keyframes/KeyframeGroup.swift +31 -8
  70. package/Sources/Private/Model/Objects/Transform.swift +58 -17
  71. package/Sources/Private/Model/ShapeItems/Repeater.swift +41 -7
  72. package/Sources/Private/Model/ShapeItems/ShapeTransform.swift +61 -7
  73. package/Sources/Private/Model/Text/TextAnimator.swift +37 -5
  74. package/Sources/Private/RootAnimationLayer.swift +3 -1
  75. package/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift +12 -4
  76. package/Sources/Private/Utility/Extensions/DataExtension.swift +14 -4
  77. package/Sources/Private/Utility/Primitives/BezierPathRoundExtension.swift +11 -0
  78. package/Sources/Private/Utility/Primitives/ColorExtension.swift +10 -13
  79. package/Sources/Private/Utility/Primitives/VectorsExtensions.swift +28 -6
  80. package/Sources/Public/Animation/LottieAnimationHelpers.swift +12 -10
  81. package/Sources/Public/Animation/LottieAnimationView.swift +213 -186
  82. package/Sources/Public/DotLottie/DotLottieFile.swift +11 -34
  83. package/Sources/Public/DotLottie/DotLottieFileHelpers.swift +101 -74
  84. package/Sources/Public/iOS/Compatibility/CompatibleAnimationView.swift +90 -0
  85. package/Sources/Public/iOS/LottieAnimationViewBase.swift +1 -1
  86. package/Sources/Public/macOS/LottieAnimationViewBase.macOS.swift +1 -1
  87. package/lottie-ios.podspec +1 -1
  88. package/package.json +1 -1
  89. package/LottieAnimation/LottieAnimation.xcodeproj/project.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  90. package/LottieAnimation/LottieAnimation.xcodeproj/project.xcworkspace/xcuserdata/valentinperignon.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  91. package/LottieAnimation/LottieAnimation.xcodeproj/xcuserdata/calstephens.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  92. package/LottieAnimation/LottieAnimation.xcodeproj/xcuserdata/valentinperignon.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
  93. package/LottieAnimation/LottieAnimation.xcodeproj/xcuserdata/valentinperignon.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  94. package/Sources/Private/Model/DotLottie/Zip/Data+Compression.swift +0 -134
  95. package/Sources/Private/Model/DotLottie/Zip/FileManager+ZIP.swift +0 -130
  96. package/Sources/Private/Utility/Interpolatable/KeyframeGroup+Extensions.swift +0 -59
@@ -19,6 +19,7 @@ final class CoreAnimationLayer: BaseAnimationLayer {
19
19
  imageProvider: AnimationImageProvider,
20
20
  textProvider: AnimationTextProvider,
21
21
  fontProvider: AnimationFontProvider,
22
+ maskAnimationToBounds: Bool,
22
23
  compatibilityTrackerMode: CompatibilityTracker.Mode,
23
24
  logger: LottieLogger)
24
25
  throws
@@ -31,7 +32,7 @@ final class CoreAnimationLayer: BaseAnimationLayer {
31
32
  compatibilityTracker = CompatibilityTracker(mode: compatibilityTrackerMode, logger: logger)
32
33
  valueProviderStore = ValueProviderStore(logger: logger)
33
34
  super.init()
34
- masksToBounds = true
35
+ masksToBounds = maskAnimationToBounds
35
36
  setup()
36
37
  try setupChildLayers()
37
38
  }
@@ -71,7 +72,10 @@ final class CoreAnimationLayer: BaseAnimationLayer {
71
72
  }
72
73
 
73
74
  enum PlaybackState: Equatable {
74
- /// The animation is playing in real-time
75
+ /// The animation is has started playing, and may still be playing.
76
+ /// - When animating with a finite duration (e.g. `playOnce`), playback
77
+ /// state will still be `playing` when the animation completes.
78
+ /// To check if the animation is currently playing, prefer `isAnimationPlaying`.
75
79
  case playing
76
80
  /// The animation is statically displaying a specific frame
77
81
  case paused(frame: AnimationFrameTime)
@@ -81,9 +85,18 @@ final class CoreAnimationLayer: BaseAnimationLayer {
81
85
  struct AnimationConfiguration: Equatable {
82
86
  var animationContext: AnimationContext
83
87
  var timingConfiguration: CAMediaTimingConfiguration
84
- var logHierarchyKeypaths = false
88
+ var recordHierarchyKeypath: ((String) -> Void)?
89
+
90
+ static func ==(_ lhs: AnimationConfiguration, _ rhs: AnimationConfiguration) -> Bool {
91
+ lhs.animationContext == rhs.animationContext
92
+ && lhs.timingConfiguration == rhs.timingConfiguration
93
+ && ((lhs.recordHierarchyKeypath == nil) == (rhs.recordHierarchyKeypath == nil))
94
+ }
85
95
  }
86
96
 
97
+ /// The parent `LottieAnimationView` that manages this layer
98
+ weak var animationView: LottieAnimationView?
99
+
87
100
  /// A closure that is called after this layer sets up its animation.
88
101
  /// If the animation setup was unsuccessful and encountered compatibility issues,
89
102
  /// those issues are included in this call.
@@ -148,7 +161,9 @@ final class CoreAnimationLayer: BaseAnimationLayer {
148
161
  // allocate a very large amount of memory (400mb+).
149
162
  // - Alternatively this layer could subclass `CATransformLayer`,
150
163
  // but this causes Core Animation to emit unnecessary logs.
151
- if let pendingAnimationConfiguration = pendingAnimationConfiguration {
164
+ if var pendingAnimationConfiguration = pendingAnimationConfiguration {
165
+ pendingAnimationConfigurationModification?(&pendingAnimationConfiguration.animationConfiguration)
166
+ pendingAnimationConfigurationModification = nil
152
167
  self.pendingAnimationConfiguration = nil
153
168
 
154
169
  do {
@@ -180,6 +195,9 @@ final class CoreAnimationLayer: BaseAnimationLayer {
180
195
  animationConfiguration: AnimationConfiguration,
181
196
  playbackState: PlaybackState)?
182
197
 
198
+ /// A modification that should be applied to the next animation configuration
199
+ private var pendingAnimationConfigurationModification: ((inout AnimationConfiguration) -> Void)?
200
+
183
201
  /// Configuration for the animation that is currently setup in this layer
184
202
  private var currentAnimationConfiguration: AnimationConfiguration?
185
203
 
@@ -249,7 +267,7 @@ final class CoreAnimationLayer: BaseAnimationLayer {
249
267
  logger: logger,
250
268
  currentKeypath: AnimationKeypath(keys: []),
251
269
  textProvider: textProvider,
252
- logHierarchyKeypaths: configuration.logHierarchyKeypaths)
270
+ recordHierarchyKeypath: configuration.recordHierarchyKeypath)
253
271
 
254
272
  // Perform a layout pass if necessary so all of the sublayers
255
273
  // have the most up-to-date sizing information
@@ -289,32 +307,20 @@ final class CoreAnimationLayer: BaseAnimationLayer {
289
307
 
290
308
  // Removes the current `CAAnimation`s, and rebuilds new animations
291
309
  // using the same configuration as the previous animations.
292
- private func rebuildCurrentAnimation(with newConfiguration: AnimationConfiguration? = nil) {
310
+ private func rebuildCurrentAnimation() {
293
311
  guard
294
- let currentConfiguration = currentAnimationConfiguration,
295
- let playbackState = playbackState,
296
312
  // Don't replace any pending animations that are queued to begin
297
313
  // on the next run loop cycle, since an existing pending animation
298
314
  // will cause the animation to be rebuilt anyway.
299
315
  pendingAnimationConfiguration == nil
300
- else {
301
- // If we already have a pending animation setup pass, but a new configuration was provided,
302
- // replace the pending configuration with the new configuration
303
- if let newConfiguration = newConfiguration {
304
- pendingAnimationConfiguration?.animationConfiguration = newConfiguration
305
- }
306
-
307
- return
308
- }
309
-
310
- removeAnimations()
311
-
312
- switch playbackState {
313
- case .paused(let frame):
314
- currentFrame = frame
315
-
316
- case .playing:
317
- playAnimation(configuration: newConfiguration ?? currentConfiguration)
316
+ else { return }
317
+
318
+ if isAnimationPlaying == true {
319
+ animationView?.updateInFlightAnimation()
320
+ } else {
321
+ let currentFrame = currentFrame
322
+ removeAnimations()
323
+ self.currentFrame = currentFrame
318
324
  }
319
325
  }
320
326
 
@@ -328,6 +334,9 @@ extension CoreAnimationLayer: RootAnimationLayer {
328
334
  .specific(#keyPath(animationProgress))
329
335
  }
330
336
 
337
+ /// Whether or not the animation is currently playing.
338
+ /// - Handles case where CAAnimations with a finite duration animation (e.g. `playOnce`)
339
+ /// have finished playing but still present on this layer.
331
340
  var isAnimationPlaying: Bool? {
332
341
  switch pendingAnimationConfiguration?.playbackState {
333
342
  case .playing:
@@ -344,6 +353,8 @@ extension CoreAnimationLayer: RootAnimationLayer {
344
353
  }
345
354
  }
346
355
 
356
+ /// The current frame of the animation being displayed,
357
+ /// accounting for the realtime progress of any active CAAnimations.
347
358
  var currentFrame: AnimationFrameTime {
348
359
  get {
349
360
  switch playbackState {
@@ -442,18 +453,30 @@ extension CoreAnimationLayer: RootAnimationLayer {
442
453
  }
443
454
 
444
455
  func logHierarchyKeypaths() {
445
- guard var configuration = pendingAnimationConfiguration?.animationConfiguration ?? currentAnimationConfiguration else {
456
+ for keypath in allHierarchyKeypaths() {
457
+ logger.info(keypath)
458
+ }
459
+ }
460
+
461
+ func allHierarchyKeypaths() -> [String] {
462
+ guard pendingAnimationConfiguration?.animationConfiguration ?? currentAnimationConfiguration != nil else {
446
463
  logger.info("Cannot log hierarchy keypaths until animation has been set up at least once")
447
- return
464
+ return []
448
465
  }
449
466
 
450
467
  logger.info("Lottie: Rebuilding animation with hierarchy keypath logging enabled")
451
468
 
452
- // Rebuild the animation with `logHierarchyKeypaths = true` so the `ValueProviderStore` will log any keypath lookups that occur.
453
- // This allows the consumer to know what keypaths can be customized in their animation.
454
- configuration.logHierarchyKeypaths = true
455
- rebuildCurrentAnimation(with: configuration)
469
+ var allAnimationKeypaths = [String]()
470
+ pendingAnimationConfigurationModification = { configuration in
471
+ configuration.recordHierarchyKeypath = { keypath in
472
+ allAnimationKeypaths.append(keypath)
473
+ }
474
+ }
475
+
476
+ rebuildCurrentAnimation()
456
477
  displayIfNeeded()
478
+
479
+ return allAnimationKeypaths
457
480
  }
458
481
 
459
482
  func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) {
@@ -68,20 +68,21 @@ enum Keyframes {
68
68
  /// Combines the given keyframe groups of `Keyframe<T>`s into a single keyframe group of of `Keyframe<[T]>`s
69
69
  /// - If all of the `KeyframeGroup`s have the exact same animation timing, the keyframes are merged
70
70
  /// - Otherwise, the keyframes are manually interpolated at each frame in the animation
71
- static func combined<T1, T2, T3, T4, T5, T6, CombinedResult>(
71
+ static func combined<T1, T2, T3, T4, T5, T6, T7, CombinedResult>(
72
72
  _ k1: KeyframeGroup<T1>,
73
73
  _ k2: KeyframeGroup<T2>,
74
74
  _ k3: KeyframeGroup<T3>,
75
75
  _ k4: KeyframeGroup<T4>,
76
76
  _ k5: KeyframeGroup<T5>,
77
77
  _ k6: KeyframeGroup<T6>,
78
- makeCombinedResult: (T1, T2, T3, T4, T5, T6) -> CombinedResult)
78
+ _ k7: KeyframeGroup<T7>,
79
+ makeCombinedResult: (T1, T2, T3, T4, T5, T6, T7) -> CombinedResult)
79
80
  -> KeyframeGroup<CombinedResult>
80
81
  where T1: AnyInterpolatable, T2: AnyInterpolatable, T3: AnyInterpolatable, T4: AnyInterpolatable,
81
- T5: AnyInterpolatable, T6: AnyInterpolatable
82
+ T5: AnyInterpolatable, T6: AnyInterpolatable, T7: AnyInterpolatable
82
83
  {
83
84
  Keyframes.combined(
84
- [k1, k2, k3, k4, k5, k6],
85
+ [k1, k2, k3, k4, k5, k6, k7],
85
86
  makeCombinedResult: { untypedValues in
86
87
  guard
87
88
  let t1 = untypedValues[0] as? T1,
@@ -89,17 +90,18 @@ enum Keyframes {
89
90
  let t3 = untypedValues[2] as? T3,
90
91
  let t4 = untypedValues[3] as? T4,
91
92
  let t5 = untypedValues[4] as? T5,
92
- let t6 = untypedValues[5] as? T6
93
+ let t6 = untypedValues[5] as? T6,
94
+ let t7 = untypedValues[6] as? T7
93
95
  else { return nil }
94
96
 
95
- return makeCombinedResult(t1, t2, t3, t4, t5, t6)
97
+ return makeCombinedResult(t1, t2, t3, t4, t5, t6, t7)
96
98
  })
97
99
  }
98
100
 
99
101
  /// Combines the given keyframe groups of `Keyframe<T>`s into a single keyframe group of of `Keyframe<[T]>`s
100
102
  /// - If all of the `KeyframeGroup`s have the exact same animation timing, the keyframes are merged
101
103
  /// - Otherwise, the keyframes are manually interpolated at each frame in the animation
102
- static func combined<T1, T2, T3, T4, T5, T6, T7, CombinedResult>(
104
+ static func combined<T1, T2, T3, T4, T5, T6, T7, T8, CombinedResult>(
103
105
  _ k1: KeyframeGroup<T1>,
104
106
  _ k2: KeyframeGroup<T2>,
105
107
  _ k3: KeyframeGroup<T3>,
@@ -107,13 +109,14 @@ enum Keyframes {
107
109
  _ k5: KeyframeGroup<T5>,
108
110
  _ k6: KeyframeGroup<T6>,
109
111
  _ k7: KeyframeGroup<T7>,
110
- makeCombinedResult: (T1, T2, T3, T4, T5, T6, T7) -> CombinedResult)
112
+ _ k8: KeyframeGroup<T8>,
113
+ makeCombinedResult: (T1, T2, T3, T4, T5, T6, T7, T8) -> CombinedResult)
111
114
  -> KeyframeGroup<CombinedResult>
112
115
  where T1: AnyInterpolatable, T2: AnyInterpolatable, T3: AnyInterpolatable, T4: AnyInterpolatable,
113
- T5: AnyInterpolatable, T6: AnyInterpolatable, T7: AnyInterpolatable
116
+ T5: AnyInterpolatable, T6: AnyInterpolatable, T7: AnyInterpolatable, T8: AnyInterpolatable
114
117
  {
115
118
  Keyframes.combined(
116
- [k1, k2, k3, k4, k5, k6, k7],
119
+ [k1, k2, k3, k4, k5, k6, k7, k8],
117
120
  makeCombinedResult: { untypedValues in
118
121
  guard
119
122
  let t1 = untypedValues[0] as? T1,
@@ -122,10 +125,11 @@ enum Keyframes {
122
125
  let t4 = untypedValues[3] as? T4,
123
126
  let t5 = untypedValues[4] as? T5,
124
127
  let t6 = untypedValues[5] as? T6,
125
- let t7 = untypedValues[6] as? T7
128
+ let t7 = untypedValues[6] as? T7,
129
+ let t8 = untypedValues[7] as? T8
126
130
  else { return nil }
127
131
 
128
- return makeCombinedResult(t1, t2, t3, t4, t5, t6, t7)
132
+ return makeCombinedResult(t1, t2, t3, t4, t5, t6, t7, t8)
129
133
  })
130
134
  }
131
135
 
@@ -44,9 +44,9 @@ struct LayerAnimationContext {
44
44
  /// The `AnimationTextProvider`
45
45
  var textProvider: AnimationTextProvider
46
46
 
47
- /// Whether or not to log `AnimationKeypath`s for all of the animation's layers
48
- /// - Used for `CoreAnimationLayer.logHierarchyKeypaths()`
49
- var logHierarchyKeypaths: Bool
47
+ /// Records the given animation keypath so it can be logged or collected into a list
48
+ /// - Used for `CoreAnimationLayer.logHierarchyKeypaths()` and `allHierarchyKeypaths()`
49
+ var recordHierarchyKeypath: ((String) -> Void)?
50
50
 
51
51
  /// A closure that remaps the given frame in the child layer's local time to a frame
52
52
  /// in the animation's overall global time
@@ -17,6 +17,7 @@ class BaseCompositionLayer: BaseAnimationLayer {
17
17
  setupSublayers()
18
18
  compositingFilter = layerModel.blendMode.filterName
19
19
  name = layerModel.name
20
+ contentsLayer.name = "\(layerModel.name) (Content)"
20
21
  }
21
22
 
22
23
  required init?(coder _: NSCoder) {
@@ -36,6 +37,10 @@ class BaseCompositionLayer: BaseAnimationLayer {
36
37
 
37
38
  // MARK: Internal
38
39
 
40
+ /// The layer that content / sublayers should be rendered in.
41
+ /// This is the layer that transform animations are applied to.
42
+ let contentsLayer = BaseAnimationLayer()
43
+
39
44
  /// Whether or not this layer render should render any visible content
40
45
  var renderLayerContents: Bool { true }
41
46
 
@@ -43,24 +48,22 @@ class BaseCompositionLayer: BaseAnimationLayer {
43
48
  /// and all child `AnimationLayer`s.
44
49
  /// - Can be overridden by subclasses, which much call `super`.
45
50
  override func setupAnimations(context: LayerAnimationContext) throws {
46
- var context = context
47
- if renderLayerContents {
48
- context = context.addingKeypathComponent(baseLayerModel.name)
49
- }
51
+ let layerContext = context.addingKeypathComponent(baseLayerModel.name)
52
+ let childContext = renderLayerContents ? layerContext : context
50
53
 
51
- try setupLayerAnimations(context: context)
52
- try setupChildAnimations(context: context)
54
+ try setupLayerAnimations(context: layerContext)
55
+ try setupChildAnimations(context: childContext)
53
56
  }
54
57
 
55
58
  func setupLayerAnimations(context: LayerAnimationContext) throws {
56
- let context = context.addingKeypathComponent(baseLayerModel.name)
59
+ let transformContext = context.addingKeypathComponent("Transform")
57
60
 
58
- try addTransformAnimations(for: baseLayerModel.transform, context: context)
61
+ try contentsLayer.addTransformAnimations(for: baseLayerModel.transform, context: transformContext)
59
62
 
60
63
  if renderLayerContents {
61
- try addOpacityAnimation(for: baseLayerModel.transform, context: context)
64
+ try contentsLayer.addOpacityAnimation(for: baseLayerModel.transform, context: transformContext)
62
65
 
63
- addVisibilityAnimation(
66
+ contentsLayer.addVisibilityAnimation(
64
67
  inFrame: CGFloat(baseLayerModel.inFrame),
65
68
  outFrame: CGFloat(baseLayerModel.outFrame),
66
69
  context: context)
@@ -71,17 +74,27 @@ class BaseCompositionLayer: BaseAnimationLayer {
71
74
  try super.setupAnimations(context: context)
72
75
  }
73
76
 
77
+ override func addSublayer(_ layer: CALayer) {
78
+ if layer === contentsLayer {
79
+ super.addSublayer(contentsLayer)
80
+ } else {
81
+ contentsLayer.addSublayer(layer)
82
+ }
83
+ }
84
+
74
85
  // MARK: Private
75
86
 
76
87
  private let baseLayerModel: LayerModel
77
88
 
78
89
  private func setupSublayers() {
90
+ addSublayer(contentsLayer)
91
+
79
92
  if
80
93
  renderLayerContents,
81
94
  let masks = baseLayerModel.masks?.filter({ $0.mode != .none }),
82
95
  !masks.isEmpty
83
96
  {
84
- mask = MaskCompositionLayer(masks: masks)
97
+ contentsLayer.mask = MaskCompositionLayer(masks: masks)
85
98
  }
86
99
  }
87
100
 
@@ -42,12 +42,12 @@ final class ImageLayer: BaseCompositionLayer {
42
42
  let image = context.imageProvider.imageForAsset(asset: imageAsset)
43
43
  else {
44
44
  self.imageAsset = nil
45
- contents = nil
45
+ contentsLayer.contents = nil
46
46
  return
47
47
  }
48
48
 
49
49
  self.imageAsset = imageAsset
50
- contents = image
50
+ contentsLayer.contents = image
51
51
  setNeedsLayout()
52
52
  }
53
53
 
@@ -101,7 +101,7 @@ extension PreCompLayer: CustomLayoutLayer {
101
101
  width: CGFloat(preCompLayer.width),
102
102
  height: CGFloat(preCompLayer.height))
103
103
 
104
- masksToBounds = true
104
+ contentsLayer.masksToBounds = true
105
105
  }
106
106
  }
107
107
 
@@ -55,7 +55,15 @@ private struct RepeaterTransform {
55
55
  anchorPoint = repeater.anchorPoint
56
56
  scale = repeater.scale
57
57
 
58
- rotation = repeater.rotation.map { rotation in
58
+ rotationX = repeater.rotationX.map { rotation in
59
+ LottieVector1D(rotation.value * Double(index))
60
+ }
61
+
62
+ rotationY = repeater.rotationY.map { rotation in
63
+ LottieVector1D(rotation.value * Double(index))
64
+ }
65
+
66
+ rotationZ = repeater.rotationZ.map { rotation in
59
67
  LottieVector1D(rotation.value * Double(index))
60
68
  }
61
69
 
@@ -71,7 +79,10 @@ private struct RepeaterTransform {
71
79
 
72
80
  let anchorPoint: KeyframeGroup<LottieVector3D>
73
81
  let position: KeyframeGroup<LottieVector3D>
74
- let rotation: KeyframeGroup<LottieVector1D>
82
+ let rotationX: KeyframeGroup<LottieVector1D>
83
+ let rotationY: KeyframeGroup<LottieVector1D>
84
+ let rotationZ: KeyframeGroup<LottieVector1D>
85
+
75
86
  let scale: KeyframeGroup<LottieVector3D>
76
87
 
77
88
  }
@@ -373,9 +373,17 @@ extension Array where Element == ShapeItemLayer.Item {
373
373
 
374
374
  for item in self {
375
375
  // `renderGroups` is non-empty, so is guaranteed to have a valid end index
376
- let lastIndex = renderGroups.indices.last!
376
+ var lastIndex: Int {
377
+ renderGroups.indices.last!
378
+ }
377
379
 
378
380
  if item.item.drawsCGPath {
381
+ // Trims should only affect paths that precede them in the group,
382
+ // so if the existing group already has a trim we create a new group for this path item.
383
+ if renderGroups[lastIndex].otherItems.contains(where: { $0.item is Trim }) {
384
+ renderGroups.append(ShapeRenderGroup())
385
+ }
386
+
379
387
  renderGroups[lastIndex].pathItems.append(item)
380
388
  }
381
389
 
@@ -26,10 +26,15 @@ final class ValueProviderStore {
26
26
  because that would require calling the closure on the main thread once per frame.
27
27
  """)
28
28
 
29
- // TODO: Support more value types
29
+ let supportedProperties = PropertyName.allCases.map { $0.rawValue }
30
+ let propertyBeingCustomized = keypath.keys.last ?? ""
31
+
30
32
  logger.assert(
31
- keypath.keys.last == PropertyName.color.rawValue,
32
- "The Core Animation rendering engine currently only supports customizing color values")
33
+ supportedProperties.contains(propertyBeingCustomized),
34
+ """
35
+ The Core Animation rendering engine currently doesn't support customizing "\(propertyBeingCustomized)" \
36
+ properties. Supported properties are: \(supportedProperties.joined(separator: ", ")).
37
+ """)
33
38
 
34
39
  valueProviders.append((keypath: keypath, valueProvider: valueProvider))
35
40
  }
@@ -42,9 +47,7 @@ final class ValueProviderStore {
42
47
  context: LayerAnimationContext)
43
48
  throws -> KeyframeGroup<Value>?
44
49
  {
45
- if context.logHierarchyKeypaths {
46
- context.logger.info(keypath.fullPath)
47
- }
50
+ context.recordHierarchyKeypath?(keypath.fullPath)
48
51
 
49
52
  guard let anyValueProvider = valueProvider(for: keypath) else {
50
53
  return nil
@@ -125,13 +128,21 @@ extension AnimationKeypath {
125
128
  + keypath.keys.joined(separator: "\\.") // match this keypath, escaping "." characters
126
129
  + "$" // match the end of the string
127
130
 
128
- // ** wildcards match anything
129
- // - "**.Color" matches both "Layer 1.Color" and "Layer 1.Layer 2.Color"
130
- regex = regex.replacingOccurrences(of: "**", with: ".+")
131
+ // Replace the ** and * wildcards with markers that are guaranteed to be unique
132
+ // and won't conflict with regex syntax (e.g. `.*`).
133
+ let doubleWildcardMarker = UUID().uuidString
134
+ let singleWildcardMarker = UUID().uuidString
135
+ regex = regex.replacingOccurrences(of: "**", with: doubleWildcardMarker)
136
+ regex = regex.replacingOccurrences(of: "*", with: singleWildcardMarker)
137
+
138
+ // "**" wildcards match zero or more path segments separated by "\\."
139
+ // - "**.Color" matches any of "Color", "Layer 1.Color", and "Layer 1.Layer 2.Color"
140
+ regex = regex.replacingOccurrences(of: "\(doubleWildcardMarker)\\.", with: ".*")
141
+ regex = regex.replacingOccurrences(of: doubleWildcardMarker, with: ".*")
131
142
 
132
- // * wildcards match any individual path component
143
+ // "*" wildcards match exactly one path component
133
144
  // - "*.Color" matches "Layer 1.Color" but not "Layer 1.Layer 2.Color"
134
- regex = regex.replacingOccurrences(of: "*", with: "[^.]+")
145
+ regex = regex.replacingOccurrences(of: singleWildcardMarker, with: "[^.]+")
135
146
 
136
147
  return fullPath.range(of: regex, options: .regularExpression) != nil
137
148
  }
@@ -167,7 +167,7 @@ private class MaskNodeProperties: NodePropertyMap {
167
167
  shape = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.shape.keyframes))
168
168
  expansion = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.expansion.keyframes))
169
169
  propertyMap = [
170
- "Opacity" : opacity,
170
+ PropertyName.opacity.rawValue : opacity,
171
171
  "Shape" : shape,
172
172
  "Expansion" : expansion,
173
173
  ]
@@ -23,6 +23,7 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
23
23
  imageProvider: AnimationImageProvider,
24
24
  textProvider: AnimationTextProvider,
25
25
  fontProvider: AnimationFontProvider,
26
+ maskAnimationToBounds: Bool,
26
27
  logger: LottieLogger)
27
28
  {
28
29
  layerImageProvider = LayerImageProvider(imageProvider: imageProvider, assets: animation.assetLibrary?.imageAssets)
@@ -31,7 +32,7 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
31
32
  animationLayers = []
32
33
  self.logger = logger
33
34
  super.init()
34
- masksToBounds = true
35
+ masksToBounds = maskAnimationToBounds
35
36
  bounds = animation.bounds
36
37
  let layers = animation.layers.initializeCompositionLayers(
37
38
  assetLibrary: animation.assetLibrary,
@@ -146,6 +147,9 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
146
147
  /// The animatable Current Frame Property
147
148
  @NSManaged var currentFrame: CGFloat
148
149
 
150
+ /// The parent `LottieAnimationView` that manages this layer
151
+ weak var animationView: LottieAnimationView?
152
+
149
153
  var animationLayers: ContiguousArray<CompositionLayer>
150
154
 
151
155
  var primaryAnimationKey: AnimationKey {
@@ -200,7 +204,14 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
200
204
 
201
205
  func logHierarchyKeypaths() {
202
206
  logger.info("Lottie: Logging Animation Keypaths")
203
- animationLayers.forEach { $0.logKeypaths(for: nil, logger: self.logger) }
207
+
208
+ for keypath in allHierarchyKeypaths() {
209
+ logger.info(keypath)
210
+ }
211
+ }
212
+
213
+ func allHierarchyKeypaths() -> [String] {
214
+ animationLayers.flatMap { $0.allKeypaths() }
204
215
  }
205
216
 
206
217
  func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) {
@@ -18,14 +18,19 @@ final class LayerTransformProperties: NodePropertyMap, KeypathSearchable {
18
18
  init(transform: Transform) {
19
19
  anchor = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.anchorPoint.keyframes))
20
20
  scale = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.scale.keyframes))
21
- rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.rotation.keyframes))
21
+ rotationX = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.rotationX.keyframes))
22
+ rotationY = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.rotationY.keyframes))
23
+ rotationZ = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.rotationZ.keyframes))
22
24
  opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.opacity.keyframes))
23
25
 
24
26
  var propertyMap: [String: AnyNodeProperty] = [
25
27
  "Anchor Point" : anchor,
26
- "Scale" : scale,
27
- "Rotation" : rotation,
28
- "Opacity" : opacity,
28
+ PropertyName.scale.rawValue : scale,
29
+ PropertyName.rotation.rawValue: rotationZ,
30
+ "Rotation X" : rotationX,
31
+ "Rotation Y" : rotationY,
32
+ "Rotation Z" : rotationZ,
33
+ PropertyName.opacity.rawValue : opacity,
29
34
  ]
30
35
 
31
36
  if
@@ -41,7 +46,7 @@ final class LayerTransformProperties: NodePropertyMap, KeypathSearchable {
41
46
  position = nil
42
47
  } else if let positionKeyframes = transform.position?.keyframes {
43
48
  let position: NodeProperty<LottieVector3D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframes))
44
- propertyMap["Position"] = position
49
+ propertyMap[PropertyName.position.rawValue] = position
45
50
  self.position = position
46
51
  positionX = nil
47
52
  positionY = nil
@@ -64,7 +69,9 @@ final class LayerTransformProperties: NodePropertyMap, KeypathSearchable {
64
69
 
65
70
  let anchor: NodeProperty<LottieVector3D>
66
71
  let scale: NodeProperty<LottieVector3D>
67
- let rotation: NodeProperty<LottieVector1D>
72
+ let rotationX: NodeProperty<LottieVector1D>
73
+ let rotationY: NodeProperty<LottieVector1D>
74
+ let rotationZ: NodeProperty<LottieVector1D>
68
75
  let position: NodeProperty<LottieVector3D>?
69
76
  let positionX: NodeProperty<LottieVector1D>?
70
77
  let positionY: NodeProperty<LottieVector1D>?
@@ -130,7 +137,9 @@ class LayerTransformNode: AnimatorNode {
130
137
  anchor: transformProperties.anchor.value.pointValue,
131
138
  position: position,
132
139
  scale: transformProperties.scale.value.sizeValue,
133
- rotation: transformProperties.rotation.value.cgFloatValue,
140
+ rotationX: transformProperties.rotationX.value.cgFloatValue,
141
+ rotationY: transformProperties.rotationY.value.cgFloatValue,
142
+ rotationZ: transformProperties.rotationZ.value.cgFloatValue,
134
143
  skew: nil,
135
144
  skewAxis: nil)
136
145
 
@@ -20,7 +20,7 @@ final class EllipseNodeProperties: NodePropertyMap, KeypathSearchable {
20
20
  position = NodeProperty(provider: KeyframeInterpolator(keyframes: ellipse.position.keyframes))
21
21
  size = NodeProperty(provider: KeyframeInterpolator(keyframes: ellipse.size.keyframes))
22
22
  keypathProperties = [
23
- "Position" : position,
23
+ PropertyName.position.rawValue : position,
24
24
  "Size" : size,
25
25
  ]
26
26
  properties = Array(keypathProperties.values)
@@ -23,10 +23,10 @@ final class PolygonNodeProperties: NodePropertyMap, KeypathSearchable {
23
23
  rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: star.rotation.keyframes))
24
24
  points = NodeProperty(provider: KeyframeInterpolator(keyframes: star.points.keyframes))
25
25
  keypathProperties = [
26
- "Position" : position,
26
+ PropertyName.position.rawValue : position,
27
27
  "Outer Radius" : outerRadius,
28
28
  "Outer Roundedness" : outerRoundedness,
29
- "Rotation" : rotation,
29
+ PropertyName.rotation.rawValue : rotation,
30
30
  "Points" : points,
31
31
  ]
32
32
  properties = Array(keypathProperties.values)
@@ -22,7 +22,7 @@ final class RectNodeProperties: NodePropertyMap, KeypathSearchable {
22
22
  cornerRadius = NodeProperty(provider: KeyframeInterpolator(keyframes: rectangle.cornerRadius.keyframes))
23
23
 
24
24
  keypathProperties = [
25
- "Position" : position,
25
+ PropertyName.position.rawValue : position,
26
26
  "Size" : size,
27
27
  "Roundness" : cornerRadius,
28
28
  ]
@@ -33,12 +33,12 @@ final class StarNodeProperties: NodePropertyMap, KeypathSearchable {
33
33
  rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: star.rotation.keyframes))
34
34
  points = NodeProperty(provider: KeyframeInterpolator(keyframes: star.points.keyframes))
35
35
  keypathProperties = [
36
- "Position" : position,
36
+ PropertyName.position.rawValue : position,
37
37
  "Outer Radius" : outerRadius,
38
38
  "Outer Roundedness" : outerRoundedness,
39
39
  "Inner Radius" : innerRadius,
40
40
  "Inner Roundedness" : innerRoundedness,
41
- "Rotation" : rotation,
41
+ PropertyName.rotation.rawValue : rotation,
42
42
  "Points" : points,
43
43
  ]
44
44
  properties = Array(keypathProperties.values)