lottie-ios 4.3.3 → 4.4.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 (199) hide show
  1. package/.github/workflows/main.yml +51 -58
  2. package/Lottie.xcodeproj/project.pbxproj +40 -0
  3. package/Lottie.xcodeproj/xcuserdata/calstephens.xcuserdatad/xcschemes/xcschememanagement.plist +4 -17
  4. package/Lottie.xcworkspace/xcshareddata/swiftpm/Package.resolved +37 -40
  5. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  6. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +9 -666
  7. package/Package.resolved +20 -22
  8. package/Package.swift +4 -2
  9. package/README.md +21 -3
  10. package/Rakefile +52 -28
  11. package/Sources/PrivacyInfo.xcprivacy +23 -0
  12. package/Sources/Private/CoreAnimation/Animations/CAAnimation+TimingConfiguration.swift +1 -1
  13. package/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift +23 -13
  14. package/Sources/Private/CoreAnimation/Animations/CustomPathAnimation.swift +8 -2
  15. package/Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift +7 -1
  16. package/Sources/Private/CoreAnimation/Animations/GradientAnimations.swift +15 -2
  17. package/Sources/Private/CoreAnimation/Animations/LayerProperty.swift +8 -1
  18. package/Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift +8 -1
  19. package/Sources/Private/CoreAnimation/Animations/StarAnimation.swift +12 -1
  20. package/Sources/Private/CoreAnimation/Animations/StrokeAnimation.swift +0 -1
  21. package/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift +19 -11
  22. package/Sources/Private/CoreAnimation/Animations/VisibilityAnimation.swift +45 -19
  23. package/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +38 -3
  24. package/Sources/Private/CoreAnimation/Extensions/CALayer+fillBounds.swift +1 -1
  25. package/Sources/Private/CoreAnimation/Extensions/Keyframes+combined.swift +9 -2
  26. package/Sources/Private/CoreAnimation/Extensions/Keyframes+timeRemapping.swift +46 -0
  27. package/Sources/Private/CoreAnimation/Layers/AnimationLayer.swift +77 -13
  28. package/Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift +1 -1
  29. package/Sources/Private/CoreAnimation/Layers/CALayer+setupLayerHierarchy.swift +2 -2
  30. package/Sources/Private/CoreAnimation/Layers/ImageLayer.swift +1 -1
  31. package/Sources/Private/CoreAnimation/Layers/LayerModel+makeAnimationLayer.swift +1 -7
  32. package/Sources/Private/CoreAnimation/Layers/PreCompLayer.swift +19 -53
  33. package/Sources/Private/CoreAnimation/Layers/ShapeItemLayer.swift +2 -2
  34. package/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift +97 -31
  35. package/Sources/Private/CoreAnimation/Layers/SolidLayer.swift +5 -2
  36. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/EpoxyModelArrayBuilder.swift +1 -1
  37. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/EpoxyModelProperty.swift +10 -10
  38. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUIHostingController.swift +1 -1
  39. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUIHostingView.swift +1 -3
  40. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUIIntrinsicContentSizeInvalidator.swift +2 -0
  41. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUILayoutMargins.swift +2 -0
  42. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxyableView+SwiftUIView.swift +2 -0
  43. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift +2 -0
  44. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift +2 -0
  45. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/SwiftUIView.swift +2 -0
  46. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/UIView+SwiftUIView.swift +2 -0
  47. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/UIViewConfiguringSwiftUIView.swift +2 -0
  48. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/ViewType.swift +2 -1
  49. package/Sources/Private/EmbeddedLibraries/LRUCache/LRUCache.swift +256 -0
  50. package/Sources/Private/EmbeddedLibraries/LRUCache/README.md +24 -0
  51. package/Sources/Private/EmbeddedLibraries/README.md +16 -0
  52. package/Sources/Private/EmbeddedLibraries/ZipFoundation/Archive+Helpers.swift +2 -2
  53. package/Sources/Private/EmbeddedLibraries/ZipFoundation/Archive+MemoryFile.swift +7 -7
  54. package/Sources/Private/EmbeddedLibraries/ZipFoundation/Data+Compression.swift +2 -2
  55. package/Sources/Private/EmbeddedLibraries/ZipFoundation/FileManager+ZIP.swift +5 -5
  56. package/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.swift +1 -2
  57. package/Sources/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.swift +1 -3
  58. package/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.swift +1 -2
  59. package/Sources/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.swift +2 -3
  60. package/Sources/Private/MainThread/LayerContainers/CompLayers/SolidCompositionLayer.swift +2 -3
  61. package/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.swift +5 -12
  62. package/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift +0 -1
  63. package/Sources/Private/MainThread/LayerContainers/Utility/CachedImageProvider.swift +11 -7
  64. package/Sources/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.swift +3 -3
  65. package/Sources/Private/MainThread/LayerContainers/Utility/CoreTextRenderLayer.swift +12 -12
  66. package/Sources/Private/MainThread/LayerContainers/Utility/InvertedMatteLayer.swift +1 -3
  67. package/Sources/Private/MainThread/LayerContainers/Utility/LayerFontProvider.swift +0 -2
  68. package/Sources/Private/MainThread/LayerContainers/Utility/LayerImageProvider.swift +1 -3
  69. package/Sources/Private/MainThread/LayerContainers/Utility/LayerTextProvider.swift +0 -2
  70. package/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.swift +0 -2
  71. package/Sources/Private/MainThread/NodeRenderSystem/Extensions/ItemsExtension.swift +1 -3
  72. package/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/KeypathSearchable.swift +0 -1
  73. package/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.swift +0 -1
  74. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/GroupOutputNode.swift +1 -3
  75. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PassThroughOutputNode.swift +1 -2
  76. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/PathOutputNode.swift +0 -1
  77. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/FillRenderer.swift +0 -2
  78. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/GradientFillRenderer.swift +0 -1
  79. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/GradientStrokeRenderer.swift +0 -1
  80. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/LegacyGradientFillRenderer.swift +0 -1
  81. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/StrokeRenderer.swift +2 -3
  82. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderContainers/GroupNode.swift +1 -3
  83. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift +2 -2
  84. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.swift +0 -2
  85. package/Sources/Private/MainThread/NodeRenderSystem/Protocols/AnimatorNode.swift +0 -1
  86. package/Sources/Private/MainThread/NodeRenderSystem/Protocols/PathNode.swift +0 -2
  87. package/Sources/Private/MainThread/NodeRenderSystem/Protocols/RenderNode.swift +0 -2
  88. package/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/ShapeContainerLayer.swift +0 -1
  89. package/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/ShapeRenderLayer.swift +0 -1
  90. package/Sources/Private/Model/Assets/Asset.swift +2 -3
  91. package/Sources/Private/Model/Assets/AssetLibrary.swift +11 -11
  92. package/Sources/Private/Model/Assets/ImageAsset.swift +20 -0
  93. package/Sources/Private/Model/Assets/PrecompAsset.swift +0 -2
  94. package/Sources/Private/Model/DictionaryInitializable.swift +18 -10
  95. package/Sources/Private/Model/DotLottie/DotLottieImageProvider.swift +9 -6
  96. package/Sources/Private/Model/DotLottie/DotLottieUtils.swift +1 -1
  97. package/Sources/Private/Model/Extensions/KeyedDecodingContainerExtensions.swift +5 -4
  98. package/Sources/Private/Model/Keyframes/KeyframeData.swift +1 -4
  99. package/Sources/Private/Model/Keyframes/KeyframeGroup.swift +1 -3
  100. package/Sources/Private/Model/LayerEffects/DropShadowEffect.swift +0 -2
  101. package/Sources/Private/Model/LayerEffects/EffectValues/ColorEffectValue.swift +0 -2
  102. package/Sources/Private/Model/LayerEffects/EffectValues/EffectValue.swift +4 -5
  103. package/Sources/Private/Model/LayerEffects/EffectValues/Vector1DEffectValue.swift +0 -2
  104. package/Sources/Private/Model/LayerEffects/LayerEffect.swift +2 -3
  105. package/Sources/Private/Model/LayerStyles/DropShadowStyle.swift +0 -2
  106. package/Sources/Private/Model/LayerStyles/LayerStyle.swift +2 -3
  107. package/Sources/Private/Model/Layers/ImageLayerModel.swift +0 -2
  108. package/Sources/Private/Model/Layers/LayerModel.swift +16 -5
  109. package/Sources/Private/Model/Layers/PreCompLayerModel.swift +0 -2
  110. package/Sources/Private/Model/Layers/ShapeLayerModel.swift +0 -2
  111. package/Sources/Private/Model/Layers/SolidLayerModel.swift +0 -2
  112. package/Sources/Private/Model/Layers/TextLayerModel.swift +0 -2
  113. package/Sources/Private/Model/Objects/DashPattern.swift +1 -3
  114. package/Sources/Private/Model/Objects/Marker.swift +0 -2
  115. package/Sources/Private/Model/Objects/Mask.swift +0 -2
  116. package/Sources/Private/Model/Objects/Transform.swift +46 -9
  117. package/Sources/Private/Model/ShapeItems/Ellipse.swift +0 -2
  118. package/Sources/Private/Model/ShapeItems/Fill.swift +0 -2
  119. package/Sources/Private/Model/ShapeItems/GradientFill.swift +1 -3
  120. package/Sources/Private/Model/ShapeItems/GradientStroke.swift +1 -3
  121. package/Sources/Private/Model/ShapeItems/Group.swift +0 -2
  122. package/Sources/Private/Model/ShapeItems/Merge.swift +1 -3
  123. package/Sources/Private/Model/ShapeItems/Rectangle.swift +0 -2
  124. package/Sources/Private/Model/ShapeItems/Repeater.swift +0 -2
  125. package/Sources/Private/Model/ShapeItems/RoundedCorners.swift +0 -2
  126. package/Sources/Private/Model/ShapeItems/Shape.swift +0 -2
  127. package/Sources/Private/Model/ShapeItems/ShapeItem.swift +2 -3
  128. package/Sources/Private/Model/ShapeItems/ShapeTransform.swift +0 -2
  129. package/Sources/Private/Model/ShapeItems/Star.swift +1 -3
  130. package/Sources/Private/Model/ShapeItems/Stroke.swift +0 -2
  131. package/Sources/Private/Model/ShapeItems/Trim.swift +1 -3
  132. package/Sources/Private/Model/Text/Font.swift +0 -2
  133. package/Sources/Private/Model/Text/Glyph.swift +0 -2
  134. package/Sources/Private/Model/Text/TextAnimator.swift +0 -2
  135. package/Sources/Private/Model/Text/TextDocument.swift +2 -4
  136. package/Sources/Private/Utility/Debugging/AnimatorNodeDebugging.swift +0 -2
  137. package/Sources/Private/Utility/Debugging/LayerDebugging.swift +4 -5
  138. package/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift +2 -3
  139. package/Sources/Private/Utility/Extensions/DataExtension.swift +0 -1
  140. package/Sources/Private/Utility/Extensions/StringExtensions.swift +5 -0
  141. package/Sources/Private/Utility/Helpers/AnimationContext.swift +2 -3
  142. package/Sources/Private/Utility/Helpers/AnyEquatable.swift +0 -2
  143. package/Sources/Private/Utility/Helpers/Binding+Map.swift +2 -0
  144. package/Sources/Private/Utility/Helpers/View+ValueChanged.swift +2 -0
  145. package/Sources/Private/Utility/LottieAnimationSource.swift +11 -1
  146. package/Sources/Private/Utility/Primitives/BezierPath.swift +2 -3
  147. package/Sources/Private/Utility/Primitives/CGPointExtension.swift +1 -1
  148. package/Sources/Private/Utility/Primitives/ColorExtension.swift +1 -2
  149. package/Sources/Private/Utility/Primitives/VectorsExtensions.swift +6 -6
  150. package/Sources/Public/Animation/LottieAnimation.swift +2 -2
  151. package/Sources/Public/Animation/LottieAnimationHelpers.swift +7 -5
  152. package/Sources/Public/Animation/LottieAnimationLayer.swift +30 -43
  153. package/Sources/Public/Animation/LottieAnimationView.swift +18 -8
  154. package/Sources/Public/Animation/LottieAnimationViewInitializers.swift +3 -3
  155. package/Sources/Public/Animation/LottiePlaybackMode.swift +53 -0
  156. package/Sources/Public/Animation/LottieView.swift +145 -48
  157. package/Sources/Public/AnimationCache/AnimationCacheProvider.swift +0 -2
  158. package/Sources/Public/AnimationCache/DefaultAnimationCache.swift +21 -7
  159. package/Sources/Public/AnimationCache/LRUAnimationCache.swift +0 -2
  160. package/Sources/Public/Controls/AnimatedButton.swift +4 -6
  161. package/Sources/Public/Controls/AnimatedControl.swift +1 -3
  162. package/Sources/Public/Controls/AnimatedSwitch.swift +1 -3
  163. package/Sources/Public/Controls/LottieButton.swift +2 -1
  164. package/Sources/Public/Controls/LottieSwitch.swift +2 -0
  165. package/Sources/Public/DotLottie/Cache/DotLottieCache.swift +17 -4
  166. package/Sources/Public/DotLottie/Cache/DotLottieCacheProvider.swift +1 -3
  167. package/Sources/Public/DotLottie/DotLottieConfiguration.swift +53 -5
  168. package/Sources/Public/DotLottie/DotLottieFile.swift +4 -3
  169. package/Sources/Public/DotLottie/DotLottieFileHelpers.swift +18 -11
  170. package/Sources/Public/DynamicProperties/AnimationKeypath.swift +0 -2
  171. package/Sources/Public/DynamicProperties/AnyValueProvider.swift +0 -1
  172. package/Sources/Public/DynamicProperties/ValueProviders/ColorValueProvider.swift +2 -2
  173. package/Sources/Public/DynamicProperties/ValueProviders/FloatValueProvider.swift +1 -1
  174. package/Sources/Public/DynamicProperties/ValueProviders/GradientValueProvider.swift +1 -1
  175. package/Sources/Public/DynamicProperties/ValueProviders/PointValueProvider.swift +1 -1
  176. package/Sources/Public/DynamicProperties/ValueProviders/SizeValueProvider.swift +1 -1
  177. package/Sources/Public/FontProvider/AnimationFontProvider.swift +0 -2
  178. package/Sources/Public/ImageProvider/AnimationImageProvider.swift +0 -2
  179. package/Sources/Public/Keyframes/Interpolatable.swift +26 -0
  180. package/Sources/Public/Primitives/LottieColor.swift +0 -2
  181. package/Sources/Public/Primitives/Vectors.swift +0 -2
  182. package/Sources/Public/TextProvider/AnimationTextProvider.swift +0 -2
  183. package/Sources/Public/iOS/AnimationSubview.swift +0 -1
  184. package/Sources/Public/iOS/BundleImageProvider.swift +3 -8
  185. package/Sources/Public/iOS/Compatibility/CompatibleAnimationView.swift +4 -8
  186. package/Sources/Public/iOS/LottieAnimationViewBase.swift +5 -2
  187. package/Sources/Public/iOS/UIColorExtension.swift +1 -2
  188. package/Sources/Public/macOS/BundleImageProvider.macOS.swift +3 -6
  189. package/lottie-ios.podspec +6 -2
  190. package/package.json +1 -1
  191. package/Lottie.xcodeproj/project.xcworkspace/xcuserdata/cal.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  192. package/Lottie.xcodeproj/project.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  193. package/Lottie.xcodeproj/xcuserdata/cal.xcuserdatad/xcschemes/xcschememanagement.plist +0 -37
  194. package/Lottie.xcworkspace/xcuserdata/cal.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  195. package/Lottie.xcworkspace/xcuserdata/cal.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
  196. package/Lottie.xcworkspace/xcuserdata/cal.xcuserdatad/xcdebugger/Expressions.xcexplist +0 -153
  197. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/IDEFindNavigatorScopes.plist +0 -5
  198. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Expressions.xcexplist +0 -231
  199. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcschemes/xcschememanagement.plist +0 -37
@@ -10,28 +10,54 @@ extension CALayer {
10
10
  inFrame: AnimationFrameTime,
11
11
  outFrame: AnimationFrameTime,
12
12
  context: LayerAnimationContext)
13
+ throws
13
14
  {
14
- let animation = CAKeyframeAnimation(keyPath: #keyPath(isHidden))
15
- animation.calculationMode = .discrete
15
+ /// If this layer uses `complexTimeRemapping`, use the `addAnimation` codepath
16
+ /// which uses `Keyframes.manuallyInterpolatedWithTimeRemapping`.
17
+ if context.mustUseComplexTimeRemapping {
18
+ let isHiddenKeyframes = KeyframeGroup(keyframes: [
19
+ Keyframe(value: true, time: 0, isHold: true), // hidden, before `inFrame`
20
+ Keyframe(value: false, time: inFrame, isHold: true), // visible
21
+ Keyframe(value: true, time: outFrame, isHold: true), // hidden, after `outFrame`
22
+ ])
16
23
 
17
- animation.values = [
18
- true, // hidden, before `inFrame`
19
- false, // visible
20
- true, // hidden, after `outFrame`
21
- ]
24
+ try addAnimation(
25
+ for: .isHidden,
26
+ keyframes: isHiddenKeyframes.map { Hold(value: $0) },
27
+ value: { $0.value },
28
+ context: context)
29
+ }
22
30
 
23
- // From the documentation of `keyTimes`:
24
- // - If the calculationMode is set to discrete, the first value in the array
25
- // must be 0.0 and the last value must be 1.0. The array should have one more
26
- // entry than appears in the values array. For example, if there are two values,
27
- // there should be three key times.
28
- animation.keyTimes = [
29
- NSNumber(value: 0.0),
30
- NSNumber(value: max(Double(context.progressTime(for: inFrame)), 0)),
31
- NSNumber(value: min(Double(context.progressTime(for: outFrame)), 1)),
32
- NSNumber(value: 1.0),
33
- ]
31
+ /// Otherwise continue using the legacy codepath that doesn't support complex time remapping.
32
+ /// - TODO: We could remove this codepath in favor of always using the simpler codepath above,
33
+ /// but would have to solve https://github.com/airbnb/lottie-ios/pull/2254 for that codepath.
34
+ else {
35
+ let animation = CAKeyframeAnimation(keyPath: #keyPath(isHidden))
36
+ animation.calculationMode = .discrete
34
37
 
35
- add(animation, timedWith: context)
38
+ animation.values = [
39
+ true, // hidden, before `inFrame`
40
+ false, // visible
41
+ true, // hidden, after `outFrame`
42
+ ]
43
+
44
+ // From the documentation of `keyTimes`:
45
+ // - If the calculationMode is set to discrete, the first value in the array
46
+ // must be 0.0 and the last value must be 1.0. The array should have one more
47
+ // entry than appears in the values array. For example, if there are two values,
48
+ // there should be three key times.
49
+ animation.keyTimes = [
50
+ NSNumber(value: 0.0),
51
+ NSNumber(value: max(Double(try context.progressTime(for: inFrame)), 0)),
52
+ // Anything visible during the last frame should stay visible until the absolute end of the animation.
53
+ // - This matches the behavior of the main thread rendering engine.
54
+ context.simpleTimeRemapping(outFrame) == context.animation.endFrame
55
+ ? NSNumber(value: Double(1.0))
56
+ : NSNumber(value: min(Double(try context.progressTime(for: outFrame)), 1)),
57
+ NSNumber(value: 1.0),
58
+ ]
59
+
60
+ add(animation, timedWith: context)
61
+ }
36
62
  }
37
63
  }
@@ -1,7 +1,6 @@
1
1
  // Created by Cal Stephens on 12/13/21.
2
2
  // Copyright © 2021 Airbnb Inc. All rights reserved.
3
3
 
4
- import Foundation
5
4
  import QuartzCore
6
5
 
7
6
  // MARK: - CoreAnimationLayer
@@ -161,7 +160,7 @@ final class CoreAnimationLayer: BaseAnimationLayer {
161
160
  // allocate a very large amount of memory (400mb+).
162
161
  // - Alternatively this layer could subclass `CATransformLayer`,
163
162
  // but this causes Core Animation to emit unnecessary logs.
164
- if var pendingAnimationConfiguration = pendingAnimationConfiguration {
163
+ if var pendingAnimationConfiguration {
165
164
  pendingAnimationConfigurationModification?(&pendingAnimationConfiguration.animationConfiguration)
166
165
  pendingAnimationConfigurationModification = nil
167
166
  self.pendingAnimationConfiguration = nil
@@ -249,6 +248,8 @@ final class CoreAnimationLayer: BaseAnimationLayer {
249
248
  try setupLayerHierarchy(
250
249
  for: animation.layers,
251
250
  context: layerContext)
251
+
252
+ try validateReasonableNumberOfTimeRemappingLayers()
252
253
  }
253
254
 
254
255
  /// Immediately builds and begins playing `CAAnimation`s for each sublayer
@@ -387,7 +388,10 @@ extension CoreAnimationLayer: RootAnimationLayer {
387
388
  let requiredAnimationConfiguration = AnimationConfiguration(
388
389
  animationContext: AnimationContext(
389
390
  playFrom: animation.startFrame,
390
- playTo: animation.endFrame,
391
+ // Normal animation playback (like when looping) skips the last frame.
392
+ // However when the animation is paused, we need to be able to render the final frame.
393
+ // To allow this we have to extend the length of the animation by one frame.
394
+ playTo: animation.endFrame + 1,
391
395
  closure: nil),
392
396
  timingConfiguration: CAMediaTimingConfiguration(speed: 0))
393
397
 
@@ -529,6 +533,18 @@ extension CoreAnimationLayer: RootAnimationLayer {
529
533
  }
530
534
  }
531
535
 
536
+ /// Time remapping in the Core Animation rendering engine requires manually interpolating
537
+ /// every frame of every animation. For very large animations with a huge number of layers,
538
+ /// this can be prohibitively expensive.
539
+ func validateReasonableNumberOfTimeRemappingLayers() throws {
540
+ try layerContext.compatibilityAssert(
541
+ numberOfLayersWithTimeRemapping < 500,
542
+ """
543
+ This animation has a very large number of layers with time remapping (\(numberOfLayersWithTimeRemapping)),
544
+ so will perform poorly with the Core Animation rendering engine.
545
+ """)
546
+ }
547
+
532
548
  }
533
549
 
534
550
  // MARK: - CALayer + allSublayers
@@ -546,4 +562,23 @@ extension CALayer {
546
562
 
547
563
  return allSublayers
548
564
  }
565
+
566
+ /// The number of layers in this layer hierarchy that have a time remapping applied
567
+ @nonobjc
568
+ var numberOfLayersWithTimeRemapping: Int {
569
+ var numberOfSublayersWithTimeRemapping = 0
570
+
571
+ for sublayer in sublayers ?? [] {
572
+ if
573
+ let preCompLayer = sublayer as? PreCompLayer,
574
+ preCompLayer.preCompLayer.timeRemapping != nil
575
+ {
576
+ numberOfSublayersWithTimeRemapping += preCompLayer.allSublayers.count
577
+ } else {
578
+ numberOfSublayersWithTimeRemapping += sublayer.numberOfLayersWithTimeRemapping
579
+ }
580
+ }
581
+
582
+ return numberOfSublayersWithTimeRemapping
583
+ }
549
584
  }
@@ -10,7 +10,7 @@ extension CALayer {
10
10
  /// without setting `frame` (which is not permitted if the layer can rotate)
11
11
  @nonobjc
12
12
  func fillBoundsOfSuperlayer() {
13
- guard let superlayer = superlayer else { return }
13
+ guard let superlayer else { return }
14
14
 
15
15
  if let customLayerLayer = self as? CustomLayoutLayer {
16
16
  customLayerLayer.layout(superlayerBounds: superlayer.bounds)
@@ -256,7 +256,11 @@ enum Keyframes {
256
256
 
257
257
  let minimumTime = times.min() ?? 0
258
258
  let maximumTime = times.max() ?? 0
259
- let animationLocalTimeRange = Int(minimumTime)...Int(maximumTime)
259
+
260
+ // We disable Core Animation interpolation when using manually interpolated keyframes,
261
+ // so we don't animate between these values. To prevent the animation from being choppy
262
+ // even at low playback speed, we have to interpolate at a very high fidelity.
263
+ let animationLocalTimeRange = stride(from: minimumTime, to: maximumTime, by: 0.1)
260
264
 
261
265
  let interpolatedKeyframes = try animationLocalTimeRange.compactMap { localTime -> Keyframe<CombinedResult>? in
262
266
  let interpolatedValues = untypedInterpolators.map { interpolator in
@@ -272,7 +276,10 @@ enum Keyframes {
272
276
 
273
277
  return Keyframe(
274
278
  value: combinedResult,
275
- time: AnimationFrameTime(localTime))
279
+ time: AnimationFrameTime(localTime),
280
+ // Since we already manually interpolated the keyframes, have Core Animation display
281
+ // each value as a static keyframe rather than trying to interpolate between them.
282
+ isHold: true)
276
283
  }
277
284
 
278
285
  return KeyframeGroup(keyframes: ContiguousArray(interpolatedKeyframes))
@@ -0,0 +1,46 @@
1
+ // Created by Cal Stephens on 1/8/24.
2
+ // Copyright © 2024 Airbnb Inc. All rights reserved.
3
+
4
+ extension Keyframes {
5
+ /// Manually interpolates the given keyframes, and applies `context.complexTimeRemapping`.
6
+ /// - Since `complexTimeRemapping` is a mapping from "global time" to "local time",
7
+ /// we have to manually interpolate the keyframes at every frame in the animation.
8
+ static func manuallyInterpolatedWithTimeRemapping<T: AnyInterpolatable>(
9
+ _ keyframes: KeyframeGroup<T>,
10
+ context: LayerAnimationContext)
11
+ -> KeyframeGroup<T>
12
+ {
13
+ let minimumTime = context.animation.startFrame
14
+ let maximumTime = context.animation.endFrame
15
+ let animationLocalTimeRange = stride(from: minimumTime, to: maximumTime, by: 1.0)
16
+
17
+ let interpolator = keyframes.interpolator
18
+
19
+ // Since potentially many global times can refer to the same local time,
20
+ // we can cache and reused these local-time values.
21
+ var localTimeCache = [AnimationFrameTime: T]()
22
+
23
+ let interpolatedRemappedKeyframes = animationLocalTimeRange.compactMap { globalTime -> Keyframe<T>? in
24
+ let remappedLocalTime = context.complexTimeRemapping(globalTime)
25
+
26
+ let valueAtRemappedTime: T
27
+ if let cachedValue = localTimeCache[remappedLocalTime] {
28
+ valueAtRemappedTime = cachedValue
29
+ } else if let interpolatedValue = interpolator.value(frame: remappedLocalTime) as? T {
30
+ valueAtRemappedTime = interpolatedValue
31
+ localTimeCache[remappedLocalTime] = interpolatedValue
32
+ } else {
33
+ LottieLogger.shared.assertionFailure("""
34
+ Failed to cast untyped keyframe values to expected type. This is an internal error.
35
+ """)
36
+ return nil
37
+ }
38
+
39
+ return Keyframe(
40
+ value: valueAtRemappedTime,
41
+ time: AnimationFrameTime(globalTime))
42
+ }
43
+
44
+ return KeyframeGroup(keyframes: ContiguousArray(interpolatedRemappedKeyframes))
45
+ }
46
+ }
@@ -52,8 +52,37 @@ struct LayerAnimationContext {
52
52
  var recordHierarchyKeypath: ((String) -> Void)?
53
53
 
54
54
  /// A closure that remaps the given frame in the child layer's local time to a frame
55
- /// in the animation's overall global time
56
- private(set) var timeRemapping: ((AnimationFrameTime) -> AnimationFrameTime) = { $0 }
55
+ /// in the animation's overall global time.
56
+ /// - This time remapping is simple and only used `preCompLayer.timeStretch` and `preCompLayer.startTime`,
57
+ /// so is a trivial function and is invertible. This allows us to invert the time remapping from
58
+ /// "global time to local time" to instead be "local time to global time".
59
+ private(set) var simpleTimeRemapping: ((_ localTime: AnimationFrameTime) -> AnimationFrameTime) = { $0 }
60
+
61
+ /// A complex time remapping closure that remaps the given frame in the animation's overall global time
62
+ /// into the child layer's local time.
63
+ /// - This time remapping is arbitrarily complex because it includes the full `preCompLayer.timeRemapping`.
64
+ /// - Since it isn't possible to invert the time remapping function, this can only be applied by converting
65
+ /// from global time to local time. This requires using `Keyframes.manuallyInterpolatedWithTimeRemapping`.
66
+ private(set) var complexTimeRemapping: ((_ globalTime: AnimationFrameTime) -> AnimationFrameTime) = { $0 }
67
+
68
+ /// Whether or not this layer is required to use the `complexTimeRemapping` via
69
+ /// the more expensive `Keyframes.manuallyInterpolatedWithTimeRemapping` codepath.
70
+ var mustUseComplexTimeRemapping = false
71
+
72
+ /// The duration of the animation
73
+ var animationDuration: AnimationFrameTime {
74
+ // Normal animation playback (like when looping) skips the last frame.
75
+ // However when the animation is paused, we need to be able to render the final frame.
76
+ // To allow this we have to extend the length of the animation by one frame.
77
+ let animationEndFrame: AnimationFrameTime
78
+ if timingConfiguration.speed == 0 {
79
+ animationEndFrame = animation.endFrame + 1
80
+ } else {
81
+ animationEndFrame = animation.endFrame
82
+ }
83
+
84
+ return Double(animationEndFrame - animation.startFrame) / animation.framerate
85
+ }
57
86
 
58
87
  /// Adds the given component string to the `AnimationKeypath` stored
59
88
  /// that describes the current path being configured by this context value
@@ -64,28 +93,63 @@ struct LayerAnimationContext {
64
93
  }
65
94
 
66
95
  /// The `AnimationProgressTime` for the given `AnimationFrameTime` within this layer,
67
- /// accounting for the `timeRemapping` applied to this layer
68
- func progressTime(for frame: AnimationFrameTime) -> AnimationProgressTime {
69
- animation.progressTime(forFrame: timeRemapping(frame), clamped: false)
96
+ /// accounting for the `simpleTimeRemapping` applied to this layer.
97
+ func progressTime(for frame: AnimationFrameTime) throws -> AnimationProgressTime {
98
+ try compatibilityAssert(
99
+ !mustUseComplexTimeRemapping,
100
+ "LayerAnimationContext.time(forFrame:) does not support complex time remapping")
101
+
102
+ let animationFrameCount = animationDuration * animation.framerate
103
+ return (simpleTimeRemapping(frame) - animation.startFrame) / animationFrameCount
70
104
  }
71
105
 
72
106
  /// The real-time `TimeInterval` for the given `AnimationFrameTime` within this layer,
73
- /// accounting for the `timeRemapping` applied to this layer
74
- func time(for frame: AnimationFrameTime) -> TimeInterval {
75
- animation.time(forFrame: timeRemapping(frame))
107
+ /// accounting for the `simpleTimeRemapping` applied to this layer.
108
+ func time(forFrame frame: AnimationFrameTime) throws -> TimeInterval {
109
+ try compatibilityAssert(
110
+ !mustUseComplexTimeRemapping,
111
+ "LayerAnimationContext.time(forFrame:) does not support complex time remapping")
112
+
113
+ return animation.time(forFrame: simpleTimeRemapping(frame))
76
114
  }
77
115
 
78
- /// Chains an additional `timeRemapping` closure onto this layer context
79
- func withTimeRemapping(
80
- _ additionalTimeRemapping: @escaping (AnimationFrameTime) -> AnimationFrameTime)
116
+ /// Chains an additional time remapping closure onto the `simpleTimeRemapping` closure
117
+ func withSimpleTimeRemapping(
118
+ _ additionalSimpleTimeRemapping: @escaping (_ localTime: AnimationFrameTime) -> AnimationFrameTime)
81
119
  -> LayerAnimationContext
82
120
  {
83
121
  var copy = self
84
- copy.timeRemapping = { [existingTimeRemapping = timeRemapping] time in
85
- existingTimeRemapping(additionalTimeRemapping(time))
122
+ copy.simpleTimeRemapping = { [existingSimpleTimeRemapping = simpleTimeRemapping] time in
123
+ existingSimpleTimeRemapping(additionalSimpleTimeRemapping(time))
86
124
  }
87
125
  return copy
88
126
  }
127
+
128
+ /// Chains an additional time remapping closure onto the `complexTimeRemapping` closure.
129
+ /// - If `required` is `true`, all subsequent child layers will be required to use the expensive
130
+ /// `complexTimeRemapping` / `Keyframes.manuallyInterpolatedWithTimeRemapping` codepath.
131
+ /// - `required: true` is necessary when this time remapping is not available via `simpleTimeRemapping`.
132
+ func withComplexTimeRemapping(
133
+ required: Bool,
134
+ _ additionalComplexTimeRemapping: @escaping (_ globalTime: AnimationFrameTime) -> AnimationFrameTime)
135
+ -> LayerAnimationContext
136
+ {
137
+ var copy = self
138
+ copy.mustUseComplexTimeRemapping = copy.mustUseComplexTimeRemapping || required
139
+ copy.complexTimeRemapping = { [existingComplexTimeRemapping = complexTimeRemapping] time in
140
+ additionalComplexTimeRemapping(existingComplexTimeRemapping(time))
141
+ }
142
+ return copy
143
+ }
144
+
145
+ /// Returns a copy of this context with time remapping removed
146
+ func withoutTimeRemapping() -> LayerAnimationContext {
147
+ var copy = self
148
+ copy.simpleTimeRemapping = { $0 }
149
+ copy.complexTimeRemapping = { $0 }
150
+ copy.mustUseComplexTimeRemapping = false
151
+ return copy
152
+ }
89
153
  }
90
154
 
91
155
  // MARK: - LoggingState
@@ -63,7 +63,7 @@ class BaseCompositionLayer: BaseAnimationLayer {
63
63
  if renderLayerContents {
64
64
  try contentsLayer.addOpacityAnimation(for: baseLayerModel.transform, context: transformContext)
65
65
 
66
- contentsLayer.addVisibilityAnimation(
66
+ try contentsLayer.addVisibilityAnimation(
67
67
  inFrame: CGFloat(baseLayerModel.inFrame),
68
68
  outFrame: CGFloat(baseLayerModel.outFrame),
69
69
  context: context)
@@ -67,7 +67,7 @@ extension CALayer {
67
67
 
68
68
  // Create the `mask` layer for this layer, if it has a `MatteType`
69
69
  if
70
- let mask = mask,
70
+ let mask,
71
71
  let maskLayer = try maskLayer(for: mask.model, type: mask.matteType, context: context)
72
72
  {
73
73
  let maskParentTransformLayer = makeParentTransformLayer(
@@ -138,7 +138,7 @@ extension CALayer {
138
138
 
139
139
  }
140
140
 
141
- extension Collection where Element == LayerModel {
141
+ extension Collection<LayerModel> {
142
142
  /// Pairs each `LayerModel` within this array with
143
143
  /// a `LayerModel` to use as its mask, if applicable
144
144
  /// based on the layer's `MatteType` configuration.
@@ -65,7 +65,7 @@ extension ImageLayer: CustomLayoutLayer {
65
65
  func layout(superlayerBounds: CGRect) {
66
66
  anchorPoint = .zero
67
67
 
68
- guard let imageAsset = imageAsset else {
68
+ guard let imageAsset else {
69
69
  bounds = superlayerBounds
70
70
  return
71
71
  }
@@ -1,8 +1,6 @@
1
1
  // Created by Cal Stephens on 12/20/21.
2
2
  // Copyright © 2021 Airbnb Inc. All rights reserved.
3
3
 
4
- import QuartzCore
5
-
6
4
  // MARK: - LayerContext
7
5
 
8
6
  /// Context available when constructing an `AnimationLayer`
@@ -53,11 +51,7 @@ extension LayerModel {
53
51
  case (.null, _):
54
52
  return TransformLayer(layerModel: self)
55
53
 
56
- default:
57
- try context.logCompatibilityIssue("""
58
- Unexpected layer type combination ("\(type)" and "\(Swift.type(of: self))")
59
- """)
60
-
54
+ case (.unknown, _), (.precomp, _), (.solid, _), (.image, _), (.shape, _), (.text, _):
61
55
  return nil
62
56
  }
63
57
  }
@@ -27,12 +27,13 @@ final class PreCompLayer: BaseCompositionLayer {
27
27
  }
28
28
 
29
29
  preCompLayer = typedLayer.preCompLayer
30
- timeRemappingInterpolator = typedLayer.timeRemappingInterpolator
31
30
  super.init(layer: typedLayer)
32
31
  }
33
32
 
34
33
  // MARK: Internal
35
34
 
35
+ let preCompLayer: PreCompLayerModel
36
+
36
37
  /// Post-init setup for `PreCompLayer`s.
37
38
  /// Should always be called after `PreCompLayer.init(preCompLayer:)`.
38
39
  ///
@@ -51,12 +52,6 @@ final class PreCompLayer: BaseCompositionLayer {
51
52
  /// - `PreCompLayer.init(preCompLayer:context:)`
52
53
  ///
53
54
  func setup(context: LayerContext) throws {
54
- if let timeRemappingKeyframes = preCompLayer.timeRemapping {
55
- timeRemappingInterpolator = try .timeRemapping(keyframes: timeRemappingKeyframes, context: context)
56
- } else {
57
- timeRemappingInterpolator = nil
58
- }
59
-
60
55
  try setupLayerHierarchy(
61
56
  for: context.animation.assetLibrary?.precompAssets[preCompLayer.referenceID]?.layers ?? [],
62
57
  context: context)
@@ -67,24 +62,28 @@ final class PreCompLayer: BaseCompositionLayer {
67
62
  context = context.addingKeypathComponent(preCompLayer.name)
68
63
  try setupLayerAnimations(context: context)
69
64
 
70
- // Precomp layers can adjust the local time of their child layers (relative to the
71
- // animation's global time) via `timeRemapping` or a custom `startTime` / `timeStretch`
72
- let contextForChildren = context.withTimeRemapping { [preCompLayer, timeRemappingInterpolator] layerLocalFrame in
73
- if let timeRemappingInterpolator = timeRemappingInterpolator {
74
- return timeRemappingInterpolator.value(frame: layerLocalFrame) as? AnimationFrameTime ?? layerLocalFrame
75
- } else {
76
- return (layerLocalFrame * AnimationFrameTime(preCompLayer.timeStretch)) + AnimationFrameTime(preCompLayer.startTime)
65
+ let timeRemappingInterpolator = preCompLayer.timeRemapping.flatMap { KeyframeInterpolator(keyframes: $0.keyframes) }
66
+
67
+ let contextForChildren = context
68
+ // `timeStretch` and `startTime` are a simple linear function so can be inverted from a
69
+ // "global time to local time" function into the simpler "local time to global time".
70
+ .withSimpleTimeRemapping { [preCompLayer] layerLocalFrame in
71
+ (layerLocalFrame * AnimationFrameTime(preCompLayer.timeStretch)) + AnimationFrameTime(preCompLayer.startTime)
72
+ }
73
+ // `timeRemappingInterpolator` is arbitrarily complex and cannot be inverted,
74
+ // so can only be applied via `complexTimeRemapping` from global time to local time.
75
+ .withComplexTimeRemapping(required: preCompLayer.timeRemapping != nil) { [preCompLayer] globalTime in
76
+ if let timeRemappingInterpolator {
77
+ let remappedLocalTime = timeRemappingInterpolator.value(frame: globalTime) as! LottieVector1D
78
+ return remappedLocalTime.cgFloatValue * context.animation.framerate
79
+ } else {
80
+ return (globalTime - preCompLayer.startTime) / preCompLayer.timeStretch
81
+ }
77
82
  }
78
- }
79
83
 
80
84
  try setupChildAnimations(context: contextForChildren)
81
85
  }
82
86
 
83
- // MARK: Private
84
-
85
- private let preCompLayer: PreCompLayerModel
86
- private var timeRemappingInterpolator: KeyframeInterpolator<AnimationFrameTime>?
87
-
88
87
  }
89
88
 
90
89
  // MARK: CustomLayoutLayer
@@ -104,36 +103,3 @@ extension PreCompLayer: CustomLayoutLayer {
104
103
  contentsLayer.masksToBounds = true
105
104
  }
106
105
  }
107
-
108
- extension KeyframeInterpolator where ValueType == AnimationFrameTime {
109
- /// A `KeyframeInterpolator` for the given `timeRemapping` keyframes
110
- static func timeRemapping(
111
- keyframes timeRemappingKeyframes: KeyframeGroup<LottieVector1D>,
112
- context: LayerContext)
113
- throws -> KeyframeInterpolator<AnimationFrameTime>
114
- {
115
- try context.logCompatibilityIssue("""
116
- The Core Animation rendering engine partially supports time remapping keyframes,
117
- but this is somewhat experimental and has some known issues. Since it doesn't work
118
- in all cases, we have to fall back to using the main thread engine when using
119
- `RenderingEngineOption.automatic`.
120
- """)
121
-
122
- // `timeRemapping` is a mapping from the animation's global time to the layer's local time.
123
- // In the Core Animation engine, we need to perform the opposite calculation -- convert
124
- // the layer's local time into the animation's global time. We can get this by inverting
125
- // the time remapping, swapping the x axis (global time) and the y axis (local time).
126
- let localTimeToGlobalTimeMapping = timeRemappingKeyframes.keyframes.map { keyframe in
127
- Keyframe(
128
- value: keyframe.time,
129
- time: keyframe.value.cgFloatValue * CGFloat(context.animation.framerate),
130
- isHold: keyframe.isHold,
131
- inTangent: keyframe.inTangent,
132
- outTangent: keyframe.outTangent,
133
- spatialInTangent: keyframe.spatialInTangent,
134
- spatialOutTangent: keyframe.spatialOutTangent)
135
- }
136
-
137
- return KeyframeInterpolator(keyframes: .init(localTimeToGlobalTimeMapping))
138
- }
139
- }
@@ -67,7 +67,7 @@ final class ShapeItemLayer: BaseAnimationLayer {
67
67
  override func setupAnimations(context: LayerAnimationContext) throws {
68
68
  try super.setupAnimations(context: context)
69
69
 
70
- guard let sublayerConfiguration = sublayerConfiguration else { return }
70
+ guard let sublayerConfiguration else { return }
71
71
 
72
72
  switch sublayerConfiguration.fill {
73
73
  case .solidFill(let shapeLayer):
@@ -299,7 +299,7 @@ final class ShapeItemLayer: BaseAnimationLayer {
299
299
 
300
300
  // MARK: - [ShapeItem] helpers
301
301
 
302
- extension Array where Element == ShapeItemLayer.Item {
302
+ extension [ShapeItemLayer.Item] {
303
303
  /// The first `ShapeItem` in this array of the given type
304
304
  func first<ItemType: ShapeItem>(
305
305
  _: ItemType.Type,