lottie-ios 4.2.0 → 4.3.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 (192) hide show
  1. package/.github/workflows/main.yml +117 -11
  2. package/Gemfile +1 -0
  3. package/Gemfile.lock +5 -0
  4. package/Lottie.xcodeproj/project.pbxproj +1542 -194
  5. package/Lottie.xcodeproj/project.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  6. package/Lottie.xcodeproj/xcshareddata/xcschemes/Lottie (iOS).xcscheme +1 -1
  7. package/Lottie.xcodeproj/xcshareddata/xcschemes/Lottie (macOS).xcscheme +1 -1
  8. package/Lottie.xcodeproj/xcshareddata/xcschemes/Lottie (tvOS).xcscheme +4 -5
  9. package/Lottie.xcodeproj/xcshareddata/xcschemes/Lottie (visionOS).xcscheme +66 -0
  10. package/Lottie.xcodeproj/xcuserdata/calstephens.xcuserdatad/xcschemes/xcschememanagement.plist +18 -0
  11. package/Lottie.xcworkspace/xcshareddata/swiftpm/Package.resolved +2 -11
  12. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  13. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +346 -102
  14. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Expressions.xcexplist +16 -5
  15. package/Package.resolved +2 -2
  16. package/Package.swift +16 -9
  17. package/README.md +2 -2
  18. package/Rakefile +122 -25
  19. package/Sources/Private/CoreAnimation/Animations/DropShadowAnimation.swift +160 -0
  20. package/Sources/Private/CoreAnimation/Animations/LayerProperty.swift +83 -10
  21. package/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift +85 -79
  22. package/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +7 -7
  23. package/Sources/Private/CoreAnimation/Extensions/Keyframes+combined.swift +67 -6
  24. package/Sources/Private/CoreAnimation/Layers/AnimationLayer.swift +2 -2
  25. package/Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift +9 -0
  26. package/Sources/Private/CoreAnimation/Layers/ImageLayer.swift +1 -0
  27. package/Sources/Private/CoreAnimation/Layers/LayerModel+makeAnimationLayer.swift +1 -1
  28. package/Sources/Private/CoreAnimation/Layers/RepeaterLayer.swift +2 -0
  29. package/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift +11 -13
  30. package/Sources/Private/CoreAnimation/Layers/SolidLayer.swift +20 -5
  31. package/Sources/Private/CoreAnimation/Layers/TextLayer.swift +13 -3
  32. package/Sources/Private/CoreAnimation/ValueProviderStore.swift +6 -4
  33. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Diffing/Collection+Diff.swift +263 -0
  34. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Diffing/Diffable.swift +18 -0
  35. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Diffing/DiffableSection.swift +16 -0
  36. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Diffing/IndexChangeset.swift +187 -0
  37. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Diffing/SectionedChangeset.swift +32 -0
  38. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Logging/EpoxyLogger.swift +99 -0
  39. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/CallbackContextEpoxyModeled.swift +8 -0
  40. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/EpoxyModelArrayBuilder.swift +48 -0
  41. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/EpoxyModelProperty.swift +158 -0
  42. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/EpoxyModelStorage.swift +88 -0
  43. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/EpoxyModeled.swift +54 -0
  44. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Internal/AnyEpoxyModelProperty.swift +29 -0
  45. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Internal/ClassReference.swift +39 -0
  46. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/AnimatedProviding.swift +10 -0
  47. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/DataIDProviding.swift +57 -0
  48. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/DidDisplayProviding.swift +41 -0
  49. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/DidEndDisplayingProviding.swift +41 -0
  50. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/DidSelectProviding.swift +36 -0
  51. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/ErasedContentProviding.swift +49 -0
  52. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/MakeViewProviding.swift +60 -0
  53. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/SetBehaviorsProviding.swift +38 -0
  54. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/SetContentProviding.swift +38 -0
  55. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/StyleIDProviding.swift +37 -0
  56. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/TraitCollectionProviding.swift +14 -0
  57. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/ViewDifferentiatorProviding.swift +34 -0
  58. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/ViewProviding.swift +13 -0
  59. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/WillDisplayProviding.swift +41 -0
  60. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/ViewEpoxyModeled.swift +10 -0
  61. package/Sources/Private/EmbeddedLibraries/EpoxyCore/README.md +31 -0
  62. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUIHostingController.swift +46 -0
  63. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUIHostingView.swift +391 -0
  64. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUIIntrinsicContentSizeInvalidator.swift +44 -0
  65. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUILayoutMargins.swift +51 -0
  66. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxyableView+SwiftUIView.swift +172 -0
  67. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift +128 -0
  68. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift +452 -0
  69. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/SwiftUIView.swift +148 -0
  70. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/UIView+SwiftUIView.swift +40 -0
  71. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/UIViewConfiguringSwiftUIView.swift +43 -0
  72. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/BehaviorsConfigurableView.swift +45 -0
  73. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/ContentConfigurableView.swift +36 -0
  74. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/EpoxyableView.swift +5 -0
  75. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/StyledView.swift +42 -0
  76. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/ViewType.swift +51 -0
  77. package/Sources/Private/EmbeddedLibraries/README.md +27 -0
  78. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+BackingConfiguration.swift +4 -4
  79. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+MemoryFile.swift +2 -2
  80. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+Writing.swift +4 -4
  81. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive.swift +8 -7
  82. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Data+Compression.swift +5 -5
  83. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/FileManager+ZIP.swift +4 -4
  84. package/Sources/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.swift +6 -0
  85. package/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.swift +3 -1
  86. package/Sources/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.swift +18 -5
  87. package/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.swift +31 -3
  88. package/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift +33 -8
  89. package/Sources/Private/MainThread/LayerContainers/Utility/CachedImageProvider.swift +8 -1
  90. package/Sources/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.swift +13 -4
  91. package/Sources/Private/MainThread/LayerContainers/Utility/LayerFontProvider.swift +2 -2
  92. package/Sources/Private/MainThread/LayerContainers/Utility/LayerImageProvider.swift +1 -0
  93. package/Sources/Private/MainThread/LayerContainers/Utility/LayerTextProvider.swift +4 -4
  94. package/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.swift +1 -1
  95. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/ModifierNodes/TrimPathNode.swift +3 -1
  96. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/GradientFillRenderer.swift +2 -1
  97. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/LegacyGradientFillRenderer.swift +2 -1
  98. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientFillNode.swift +1 -1
  99. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientStrokeNode.swift +2 -2
  100. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift +1 -1
  101. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.swift +1 -1
  102. package/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/ShapeContainerLayer.swift +6 -2
  103. package/Sources/Private/Model/Assets/Asset.swift +8 -0
  104. package/Sources/Private/Model/Assets/AssetLibrary.swift +2 -2
  105. package/Sources/Private/Model/Assets/ImageAsset.swift +3 -3
  106. package/Sources/Private/Model/DotLottie/DotLottieAnimation.swift +16 -2
  107. package/Sources/Private/Model/DotLottie/DotLottieImageProvider.swift +20 -7
  108. package/Sources/Private/Model/Extensions/KeyedDecodingContainerExtensions.swift +26 -0
  109. package/Sources/Private/Model/Keyframes/KeyframeGroup.swift +8 -5
  110. package/Sources/Private/Model/LayerEffects/DropShadowEffect.swift +45 -0
  111. package/Sources/Private/Model/LayerEffects/EffectValues/ColorEffectValue.swift +38 -0
  112. package/Sources/Private/Model/LayerEffects/EffectValues/EffectValue.swift +98 -0
  113. package/Sources/Private/Model/LayerEffects/EffectValues/Vector1DEffectValue.swift +38 -0
  114. package/Sources/Private/Model/LayerEffects/LayerEffect.swift +103 -0
  115. package/Sources/Private/Model/LayerStyles/DropShadowStyle.swift +72 -0
  116. package/Sources/Private/Model/LayerStyles/LayerStyle.swift +85 -0
  117. package/Sources/Private/Model/Layers/LayerModel.swift +27 -0
  118. package/Sources/Private/Model/Objects/Marker.swift +1 -1
  119. package/Sources/Private/Model/Objects/Transform.swift +1 -2
  120. package/Sources/Private/Model/ShapeItems/GradientFill.swift +1 -1
  121. package/Sources/Private/Model/ShapeItems/GradientStroke.swift +2 -2
  122. package/Sources/Private/Model/ShapeItems/Merge.swift +1 -1
  123. package/Sources/Private/Model/ShapeItems/ShapeItem.swift +31 -26
  124. package/Sources/Private/Model/ShapeItems/ShapeTransform.swift +0 -9
  125. package/Sources/Private/Model/ShapeItems/Star.swift +1 -1
  126. package/Sources/Private/Model/Text/Font.swift +2 -2
  127. package/Sources/Private/Model/Text/Glyph.swift +1 -1
  128. package/Sources/Private/RootAnimationLayer.swift +2 -2
  129. package/Sources/Private/Utility/Debugging/LayerDebugging.swift +3 -1
  130. package/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift +32 -2
  131. package/Sources/Private/Utility/Helpers/AnimationContext.swift +4 -2
  132. package/Sources/Private/Utility/Helpers/AnyEquatable.swift +24 -0
  133. package/Sources/Private/Utility/Helpers/Binding+Map.swift +18 -0
  134. package/Sources/Private/Utility/Helpers/View+ValueChanged.swift +20 -0
  135. package/Sources/Private/Utility/LottieAnimationSource.swift +41 -0
  136. package/Sources/Private/Utility/Primitives/BezierPath.swift +2 -2
  137. package/Sources/Private/Utility/Primitives/BezierPathRoundExtension.swift +2 -2
  138. package/Sources/Private/Utility/Primitives/VectorsExtensions.swift +1 -1
  139. package/Sources/Public/Animation/LottieAnimation.swift +21 -2
  140. package/Sources/Public/Animation/LottieAnimationLayer.swift +1464 -0
  141. package/Sources/Public/Animation/LottieAnimationView.swift +258 -735
  142. package/Sources/Public/Animation/LottiePlaybackMode.swift +117 -0
  143. package/Sources/Public/Animation/LottieView.swift +462 -0
  144. package/Sources/Public/AnimationCache/AnimationCacheProvider.swift +2 -1
  145. package/Sources/Public/AnimationCache/DefaultAnimationCache.swift +5 -6
  146. package/Sources/Public/Configuration/DecodingStrategy.swift +15 -0
  147. package/Sources/Public/Configuration/LottieConfiguration.swift +47 -0
  148. package/Sources/Public/Configuration/ReducedMotionOption.swift +114 -0
  149. package/Sources/Public/{LottieConfiguration.swift → Configuration/RenderingEngineOption.swift} +2 -57
  150. package/Sources/Public/{iOS → Controls}/AnimatedButton.swift +56 -13
  151. package/Sources/Public/{iOS → Controls}/AnimatedControl.swift +80 -8
  152. package/Sources/Public/{iOS → Controls}/AnimatedSwitch.swift +71 -31
  153. package/Sources/Public/Controls/LottieButton.swift +122 -0
  154. package/Sources/Public/Controls/LottieSwitch.swift +144 -0
  155. package/Sources/Public/Controls/LottieViewType.swift +79 -0
  156. package/Sources/Public/DotLottie/DotLottieConfiguration.swift +24 -0
  157. package/Sources/Public/DotLottie/DotLottieFile.swift +21 -6
  158. package/Sources/Public/DotLottie/DotLottieFileHelpers.swift +79 -0
  159. package/Sources/Public/DynamicProperties/AnimationKeypath.swift +11 -1
  160. package/Sources/Public/DynamicProperties/ValueProviders/ColorValueProvider.swift +14 -0
  161. package/Sources/Public/DynamicProperties/ValueProviders/FloatValueProvider.swift +13 -0
  162. package/Sources/Public/DynamicProperties/ValueProviders/GradientValueProvider.swift +22 -5
  163. package/Sources/Public/DynamicProperties/ValueProviders/PointValueProvider.swift +14 -0
  164. package/Sources/Public/DynamicProperties/ValueProviders/SizeValueProvider.swift +13 -0
  165. package/Sources/Public/FontProvider/AnimationFontProvider.swift +8 -0
  166. package/Sources/Public/ImageProvider/AnimationImageProvider.swift +24 -0
  167. package/Sources/Public/Keyframes/Keyframe.swift +6 -0
  168. package/Sources/Public/Primitives/Vectors.swift +2 -2
  169. package/Sources/Public/TextProvider/AnimationTextProvider.swift +79 -6
  170. package/Sources/Public/iOS/AnimationSubview.swift +1 -1
  171. package/Sources/Public/iOS/BundleImageProvider.swift +16 -2
  172. package/Sources/Public/iOS/Compatibility/CompatibleAnimationView.swift +2 -2
  173. package/Sources/Public/iOS/FilepathImageProvider.swift +22 -3
  174. package/Sources/Public/iOS/LottieAnimationViewBase.swift +7 -1
  175. package/Sources/Public/macOS/BundleImageProvider.macOS.swift +15 -1
  176. package/Sources/Public/macOS/FilepathImageProvider.macOS.swift +21 -2
  177. package/lottie-ios.podspec +2 -1
  178. package/package.json +1 -1
  179. package/Sources/Private/Model/DotLottie/DotLottieConfiguration.swift +0 -15
  180. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+Helpers.swift +0 -0
  181. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+Progress.swift +0 -0
  182. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+Reading.swift +0 -0
  183. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+ReadingDeprecated.swift +0 -0
  184. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+WritingDeprecated.swift +0 -0
  185. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+ZIP64.swift +0 -0
  186. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Data+CompressionDeprecated.swift +0 -0
  187. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Data+Serialization.swift +0 -0
  188. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Entry+Serialization.swift +0 -0
  189. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Entry+ZIP64.swift +0 -0
  190. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Entry.swift +0 -0
  191. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/README.md +0 -0
  192. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/URL+ZIP.swift +0 -0
@@ -6,7 +6,7 @@ import QuartzCore
6
6
  // MARK: - TransformModel
7
7
 
8
8
  /// This protocol mirrors the interface of `Transform`,
9
- /// but it also implemented by `ShapeTransform` to allow
9
+ /// but is also implemented by `ShapeTransform` to allow
10
10
  /// both transform types to share the same animation implementation.
11
11
  protocol TransformModel {
12
12
  /// The anchor point of the transform.
@@ -32,6 +32,12 @@ protocol TransformModel {
32
32
 
33
33
  /// The rotation of the transform on Z axis.
34
34
  var rotationZ: KeyframeGroup<LottieVector1D> { get }
35
+
36
+ /// The skew of the transform (only present on `ShapeTransform`s)
37
+ var _skew: KeyframeGroup<LottieVector1D>? { get }
38
+
39
+ /// The skew axis of the transform (only present on `ShapeTransform`s)
40
+ var _skewAxis: KeyframeGroup<LottieVector1D>? { get }
35
41
  }
36
42
 
37
43
  // MARK: - Transform + TransformModel
@@ -40,6 +46,8 @@ extension Transform: TransformModel {
40
46
  var _position: KeyframeGroup<LottieVector3D>? { position }
41
47
  var _positionX: KeyframeGroup<LottieVector1D>? { positionX }
42
48
  var _positionY: KeyframeGroup<LottieVector1D>? { positionY }
49
+ var _skew: KeyframeGroup<LottieVector1D>? { nil }
50
+ var _skewAxis: KeyframeGroup<LottieVector1D>? { nil }
43
51
  }
44
52
 
45
53
  // MARK: - ShapeTransform + TransformModel
@@ -49,6 +57,8 @@ extension ShapeTransform: TransformModel {
49
57
  var _position: KeyframeGroup<LottieVector3D>? { position }
50
58
  var _positionX: KeyframeGroup<LottieVector1D>? { nil }
51
59
  var _positionY: KeyframeGroup<LottieVector1D>? { nil }
60
+ var _skew: KeyframeGroup<LottieVector1D>? { skew }
61
+ var _skewAxis: KeyframeGroup<LottieVector1D>? { skewAxis }
52
62
  }
53
63
 
54
64
  // MARK: - CALayer + TransformModel
@@ -66,15 +76,18 @@ extension CALayer {
66
76
  context: LayerAnimationContext)
67
77
  throws
68
78
  {
69
- // CALayers don't support animating skew with its own set of keyframes.
70
- // If the transform includes a skew, we have to combine all of the transform
71
- // components into a single set of keyframes.
72
- // Only `ShapeTransform` supports skews.
73
79
  if
74
- let shapeTransform = transformModel as? ShapeTransform,
75
- shapeTransform.hasSkew
80
+ // CALayers don't support animating skew with its own set of keyframes.
81
+ // If the transform includes a skew, we have to combine all of the transform
82
+ // components into a single set of keyframes.
83
+ transformModel.hasSkew
84
+ // Negative `scale.x` values aren't applied correctly by Core Animation when animating
85
+ // `transform.scale.x` and `transform.scale.y` using separate `CAKeyframeAnimation`s
86
+ // (https://openradar.appspot.com/FB9862872). If the transform includes negative `scale.x`
87
+ // values, we have to combine all of the transform components into a single set of keyframes.
88
+ || transformModel.hasNegativeXScaleValues
76
89
  {
77
- try addCombinedTransformAnimation(for: shapeTransform, context: context)
90
+ try addCombinedTransformAnimation(for: transformModel, context: context)
78
91
  }
79
92
 
80
93
  else {
@@ -159,65 +172,10 @@ extension CALayer {
159
172
  // Lottie animation files express scale as a numerical percentage value
160
173
  // (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values
161
174
  // expected by Core Animation (e.g. 0.5, 1.0, 2.0).
162
- // - Negative `scale.x` values aren't applied correctly by Core Animation.
163
- // This appears to be because we animate `transform.scale.x` and `transform.scale.y`
164
- // as separate `CAKeyframeAnimation`s instead of using a single animation of `transform` itself.
165
- // https://openradar.appspot.com/FB9862872
166
- // - To work around this, we set up a `rotationY` animation below
167
- // to flip the view horizontally, which gives us the desired effect.
168
- abs(CGFloat(scale.x) / 100)
175
+ CGFloat(scale.x) / 100
169
176
  },
170
177
  context: context)
171
178
 
172
- /// iOS 14 and earlier doesn't properly support rendering transforms with
173
- /// negative `scale.x` values: https://github.com/airbnb/lottie-ios/issues/1882
174
- let osSupportsNegativeScaleValues: Bool = {
175
- #if os(iOS) || os(tvOS)
176
- if #available(iOS 15.0, tvOS 15.0, *) {
177
- return true
178
- } else {
179
- return false
180
- }
181
- #else
182
- // We'll assume this works correctly on macOS until told otherwise
183
- return true
184
- #endif
185
- }()
186
-
187
- lazy var hasNegativeXScaleValues = transformModel.scale.keyframes.contains(where: { $0.value.x < 0 })
188
-
189
- // When `scale.x` is negative, we have to rotate the view
190
- // half way around the y axis to flip it horizontally.
191
- // - We don't do this in snapshot tests because it breaks the tests
192
- // in surprising ways that don't happen at runtime. Definitely not ideal.
193
- // - This isn't supported on iOS 14 and earlier either, so we have to
194
- // log a compatibility error on devices running older OSs.
195
- if TestHelpers.snapshotTestsAreRunning {
196
- if hasNegativeXScaleValues {
197
- context.logger.warn("""
198
- Negative `scale.x` values are not displayed correctly in snapshot tests
199
- """)
200
- }
201
- } else {
202
- if !osSupportsNegativeScaleValues, hasNegativeXScaleValues {
203
- try context.logCompatibilityIssue("""
204
- iOS 14 and earlier does not support rendering negative `scale.x` values
205
- """)
206
- }
207
-
208
- try addAnimation(
209
- for: .rotationY,
210
- keyframes: transformModel.scale,
211
- value: { scale in
212
- if scale.x < 0 {
213
- return .pi
214
- } else {
215
- return 0
216
- }
217
- },
218
- context: context)
219
- }
220
-
221
179
  try addAnimation(
222
180
  for: .scaleY,
223
181
  keyframes: transformModel.scale,
@@ -225,9 +183,6 @@ extension CALayer {
225
183
  // Lottie animation files express scale as a numerical percentage value
226
184
  // (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values
227
185
  // expected by Core Animation (e.g. 0.5, 1.0, 2.0).
228
- // - Negative `scaleY` values are correctly applied (they flip the view
229
- // vertically), so we don't have to apply an additional rotation animation
230
- // like we do for `scaleX`.
231
186
  CGFloat(scale.y) / 100
232
187
  },
233
188
  context: context)
@@ -258,7 +213,7 @@ extension CALayer {
258
213
  }
259
214
 
260
215
  // Lottie animation files express rotation in degrees
261
- // (e.g. 90º, 180º, 360º) so we covert to radians to get the
216
+ // (e.g. 90º, 180º, 360º) so we convert to radians to get the
262
217
  // values expected by Core Animation (e.g. π/2, π, 2π)
263
218
 
264
219
  try addAnimation(
@@ -282,7 +237,7 @@ extension CALayer {
282
237
  keyframes: transformModel.rotationZ,
283
238
  value: { rotationDegrees in
284
239
  // Lottie animation files express rotation in degrees
285
- // (e.g. 90º, 180º, 360º) so we covert to radians to get the
240
+ // (e.g. 90º, 180º, 360º) so we convert to radians to get the
286
241
  // values expected by Core Animation (e.g. π/2, π, 2π)
287
242
  rotationDegrees.cgFloatValue * .pi / 180
288
243
  },
@@ -291,26 +246,44 @@ extension CALayer {
291
246
 
292
247
  /// Adds an animation for the entire `transform` key by combining all of the
293
248
  /// position / size / rotation / skew animations into a single set of keyframes.
294
- /// This is necessary when there's a skew animation, since skew can only
295
- /// be applied via a transform.
249
+ /// This is more expensive that animating each component separately, since
250
+ /// it may require manually interpolating the keyframes at each frame.
296
251
  private func addCombinedTransformAnimation(
297
- for transformModel: ShapeTransform,
252
+ for transformModel: TransformModel,
298
253
  context: LayerAnimationContext)
299
254
  throws
300
255
  {
256
+ // Core Animation doesn't animate skew changes properly. If the skew value
257
+ // changes over the course of the animation then we have to manually
258
+ // compute the `CATransform3D` for each frame individually.
259
+ let requiresManualInterpolation = transformModel.hasSkewAnimation
260
+
301
261
  let combinedTransformKeyframes = Keyframes.combined(
302
- transformModel.anchor,
303
- transformModel.position,
262
+ transformModel.anchorPoint,
263
+ transformModel._position ?? KeyframeGroup(LottieVector3D(x: 0.0, y: 0.0, z: 0.0)),
264
+ transformModel._positionX ?? KeyframeGroup(LottieVector1D(0)),
265
+ transformModel._positionY ?? KeyframeGroup(LottieVector1D(0)),
304
266
  transformModel.scale,
305
267
  transformModel.rotationX,
306
268
  transformModel.rotationY,
307
269
  transformModel.rotationZ,
308
- transformModel.skew,
309
- transformModel.skewAxis,
310
- makeCombinedResult: { anchor, position, scale, rotationX, rotationY, rotationZ, skew, skewAxis in
311
- CATransform3D.makeTransform(
270
+ transformModel._skew ?? KeyframeGroup(LottieVector1D(0)),
271
+ transformModel._skewAxis ?? KeyframeGroup(LottieVector1D(0)),
272
+ requiresManualInterpolation: requiresManualInterpolation,
273
+ makeCombinedResult: {
274
+ anchor, position, positionX, positionY, scale, rotationX, rotationY, rotationZ, skew, skewAxis
275
+ -> CATransform3D in
276
+
277
+ let transformPosition: CGPoint
278
+ if transformModel._positionX != nil, transformModel._positionY != nil {
279
+ transformPosition = CGPoint(x: positionX.cgFloatValue, y: positionY.cgFloatValue)
280
+ } else {
281
+ transformPosition = position.pointValue
282
+ }
283
+
284
+ return CATransform3D.makeTransform(
312
285
  anchor: anchor.pointValue,
313
- position: position.pointValue,
286
+ position: transformPosition,
314
287
  scale: scale.sizeValue,
315
288
  rotationX: rotationX.cgFloatValue,
316
289
  rotationY: rotationY.cgFloatValue,
@@ -327,3 +300,36 @@ extension CALayer {
327
300
  }
328
301
 
329
302
  }
303
+
304
+ extension TransformModel {
305
+ /// Whether or not this transform has a non-zero skew value
306
+ var hasSkew: Bool {
307
+ guard
308
+ let _skew = _skew,
309
+ let _skewAxis = _skewAxis,
310
+ !_skew.keyframes.isEmpty,
311
+ !_skewAxis.keyframes.isEmpty
312
+ else {
313
+ return false
314
+ }
315
+
316
+ return _skew.keyframes.contains(where: { $0.value.cgFloatValue != 0 })
317
+ }
318
+
319
+ /// Whether or not this transform has a non-zero skew value which animates
320
+ var hasSkewAnimation: Bool {
321
+ guard
322
+ hasSkew,
323
+ let _skew = _skew,
324
+ let _skewAxis = _skewAxis
325
+ else { return false }
326
+
327
+ return _skew.keyframes.count > 1
328
+ || _skewAxis.keyframes.count > 1
329
+ }
330
+
331
+ /// Whether or not this `TransformModel` has any negative X scale values
332
+ var hasNegativeXScaleValues: Bool {
333
+ scale.keyframes.contains(where: { $0.value.x < 0 })
334
+ }
335
+ }
@@ -17,7 +17,7 @@ final class CoreAnimationLayer: BaseAnimationLayer {
17
17
  init(
18
18
  animation: LottieAnimation,
19
19
  imageProvider: AnimationImageProvider,
20
- textProvider: AnimationTextProvider,
20
+ textProvider: AnimationKeypathTextProvider,
21
21
  fontProvider: AnimationFontProvider,
22
22
  maskAnimationToBounds: Bool,
23
23
  compatibilityTrackerMode: CompatibilityTracker.Mode,
@@ -94,8 +94,8 @@ final class CoreAnimationLayer: BaseAnimationLayer {
94
94
  }
95
95
  }
96
96
 
97
- /// The parent `LottieAnimationView` that manages this layer
98
- weak var animationView: LottieAnimationView?
97
+ /// The parent `LottieAnimationLayer` that manages this layer
98
+ weak var lottieAnimationLayer: LottieAnimationLayer?
99
99
 
100
100
  /// A closure that is called after this layer sets up its animation.
101
101
  /// If the animation setup was unsuccessful and encountered compatibility issues,
@@ -108,9 +108,9 @@ final class CoreAnimationLayer: BaseAnimationLayer {
108
108
  didSet { reloadImages() }
109
109
  }
110
110
 
111
- /// The `AnimationTextProvider` that `TextLayer`'s use to retrieve texts,
111
+ /// The `AnimationKeypathTextProvider` that `TextLayer`'s use to retrieve texts,
112
112
  /// that they should use to render their text context
113
- var textProvider: AnimationTextProvider {
113
+ var textProvider: AnimationKeypathTextProvider {
114
114
  didSet {
115
115
  // We need to rebuild the current animation after updating the text provider,
116
116
  // since this is used in `TextLayer.setupAnimations(context:)`
@@ -316,7 +316,7 @@ final class CoreAnimationLayer: BaseAnimationLayer {
316
316
  else { return }
317
317
 
318
318
  if isAnimationPlaying == true {
319
- animationView?.updateInFlightAnimation()
319
+ lottieAnimationLayer?.updateInFlightAnimation()
320
320
  } else {
321
321
  let currentFrame = currentFrame
322
322
  removeAnimations()
@@ -449,7 +449,7 @@ extension CoreAnimationLayer: RootAnimationLayer {
449
449
  }
450
450
 
451
451
  func forceDisplayUpdate() {
452
- // Unimplemented / unused
452
+ display()
453
453
  }
454
454
 
455
455
  func logHierarchyKeypaths() {
@@ -10,12 +10,18 @@ enum Keyframes {
10
10
  /// Combines the given keyframe groups of `Keyframe<T>`s into a single keyframe group of of `Keyframe<[T]>`s
11
11
  /// - If all of the `KeyframeGroup`s have the exact same animation timing, the keyframes are merged
12
12
  /// - Otherwise, the keyframes are manually interpolated at each frame in the animation
13
- static func combined<T>(_ allGroups: [KeyframeGroup<T>]) -> KeyframeGroup<[T]>
13
+ static func combined<T>(
14
+ _ allGroups: [KeyframeGroup<T>],
15
+ requiresManualInterpolation: Bool = false)
16
+ -> KeyframeGroup<[T]>
14
17
  where T: AnyInterpolatable
15
18
  {
16
- Keyframes.combined(allGroups, makeCombinedResult: { untypedValues in
17
- untypedValues.compactMap { $0 as? T }
18
- })
19
+ Keyframes.combined(
20
+ allGroups,
21
+ requiresManualInterpolation: requiresManualInterpolation,
22
+ makeCombinedResult: { untypedValues in
23
+ untypedValues.compactMap { $0 as? T }
24
+ })
19
25
  }
20
26
 
21
27
  /// Combines the given keyframe groups of `Keyframe<T>`s into a single keyframe group of of `Keyframe<[T]>`s
@@ -24,6 +30,7 @@ enum Keyframes {
24
30
  static func combined<T1, T2, CombinedResult>(
25
31
  _ k1: KeyframeGroup<T1>,
26
32
  _ k2: KeyframeGroup<T2>,
33
+ requiresManualInterpolation: Bool = false,
27
34
  makeCombinedResult: (T1, T2) throws -> CombinedResult)
28
35
  rethrows
29
36
  -> KeyframeGroup<CombinedResult>
@@ -31,6 +38,7 @@ enum Keyframes {
31
38
  {
32
39
  try Keyframes.combined(
33
40
  [k1, k2],
41
+ requiresManualInterpolation: requiresManualInterpolation,
34
42
  makeCombinedResult: { untypedValues in
35
43
  guard
36
44
  let t1 = untypedValues[0] as? T1,
@@ -48,12 +56,14 @@ enum Keyframes {
48
56
  _ k1: KeyframeGroup<T1>,
49
57
  _ k2: KeyframeGroup<T2>,
50
58
  _ k3: KeyframeGroup<T3>,
59
+ requiresManualInterpolation: Bool = false,
51
60
  makeCombinedResult: (T1, T2, T3) -> CombinedResult)
52
61
  -> KeyframeGroup<CombinedResult>
53
62
  where T1: AnyInterpolatable, T2: AnyInterpolatable, T3: AnyInterpolatable
54
63
  {
55
64
  Keyframes.combined(
56
65
  [k1, k2, k3],
66
+ requiresManualInterpolation: requiresManualInterpolation,
57
67
  makeCombinedResult: { untypedValues in
58
68
  guard
59
69
  let t1 = untypedValues[0] as? T1,
@@ -76,6 +86,7 @@ enum Keyframes {
76
86
  _ k5: KeyframeGroup<T5>,
77
87
  _ k6: KeyframeGroup<T6>,
78
88
  _ k7: KeyframeGroup<T7>,
89
+ requiresManualInterpolation: Bool = false,
79
90
  makeCombinedResult: (T1, T2, T3, T4, T5, T6, T7) -> CombinedResult)
80
91
  -> KeyframeGroup<CombinedResult>
81
92
  where T1: AnyInterpolatable, T2: AnyInterpolatable, T3: AnyInterpolatable, T4: AnyInterpolatable,
@@ -83,6 +94,7 @@ enum Keyframes {
83
94
  {
84
95
  Keyframes.combined(
85
96
  [k1, k2, k3, k4, k5, k6, k7],
97
+ requiresManualInterpolation: requiresManualInterpolation,
86
98
  makeCombinedResult: { untypedValues in
87
99
  guard
88
100
  let t1 = untypedValues[0] as? T1,
@@ -110,6 +122,7 @@ enum Keyframes {
110
122
  _ k6: KeyframeGroup<T6>,
111
123
  _ k7: KeyframeGroup<T7>,
112
124
  _ k8: KeyframeGroup<T8>,
125
+ requiresManualInterpolation: Bool = false,
113
126
  makeCombinedResult: (T1, T2, T3, T4, T5, T6, T7, T8) -> CombinedResult)
114
127
  -> KeyframeGroup<CombinedResult>
115
128
  where T1: AnyInterpolatable, T2: AnyInterpolatable, T3: AnyInterpolatable, T4: AnyInterpolatable,
@@ -117,6 +130,7 @@ enum Keyframes {
117
130
  {
118
131
  Keyframes.combined(
119
132
  [k1, k2, k3, k4, k5, k6, k7, k8],
133
+ requiresManualInterpolation: requiresManualInterpolation,
120
134
  makeCombinedResult: { untypedValues in
121
135
  guard
122
136
  let t1 = untypedValues[0] as? T1,
@@ -133,6 +147,48 @@ enum Keyframes {
133
147
  })
134
148
  }
135
149
 
150
+ /// Combines the given keyframe groups of `Keyframe<T>`s into a single keyframe group of of `Keyframe<[T]>`s
151
+ /// - If all of the `KeyframeGroup`s have the exact same animation timing, the keyframes are merged
152
+ /// - Otherwise, the keyframes are manually interpolated at each frame in the animation
153
+ static func combined<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, CombinedResult>(
154
+ _ k1: KeyframeGroup<T1>,
155
+ _ k2: KeyframeGroup<T2>,
156
+ _ k3: KeyframeGroup<T3>,
157
+ _ k4: KeyframeGroup<T4>,
158
+ _ k5: KeyframeGroup<T5>,
159
+ _ k6: KeyframeGroup<T6>,
160
+ _ k7: KeyframeGroup<T7>,
161
+ _ k8: KeyframeGroup<T8>,
162
+ _ k9: KeyframeGroup<T9>,
163
+ _ k10: KeyframeGroup<T10>,
164
+ requiresManualInterpolation: Bool = false,
165
+ makeCombinedResult: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) -> CombinedResult)
166
+ -> KeyframeGroup<CombinedResult>
167
+ where T1: AnyInterpolatable, T2: AnyInterpolatable, T3: AnyInterpolatable, T4: AnyInterpolatable,
168
+ T5: AnyInterpolatable, T6: AnyInterpolatable, T7: AnyInterpolatable, T8: AnyInterpolatable,
169
+ T9: AnyInterpolatable, T10: AnyInterpolatable
170
+ {
171
+ Keyframes.combined(
172
+ [k1, k2, k3, k4, k5, k6, k7, k8, k9, k10],
173
+ requiresManualInterpolation: requiresManualInterpolation,
174
+ makeCombinedResult: { untypedValues in
175
+ guard
176
+ let t1 = untypedValues[0] as? T1,
177
+ let t2 = untypedValues[1] as? T2,
178
+ let t3 = untypedValues[2] as? T3,
179
+ let t4 = untypedValues[3] as? T4,
180
+ let t5 = untypedValues[4] as? T5,
181
+ let t6 = untypedValues[5] as? T6,
182
+ let t7 = untypedValues[6] as? T7,
183
+ let t8 = untypedValues[7] as? T8,
184
+ let t9 = untypedValues[8] as? T9,
185
+ let t10 = untypedValues[9] as? T10
186
+ else { return nil }
187
+
188
+ return makeCombinedResult(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10)
189
+ })
190
+ }
191
+
136
192
  // MARK: Private
137
193
 
138
194
  /// Combines the given `[KeyframeGroup]` of `Keyframe<T>`s into a single `KeyframeGroup` of `Keyframe<CombinedResult>`s
@@ -141,8 +197,12 @@ enum Keyframes {
141
197
  ///
142
198
  /// `makeCombinedResult` is a closure that takes an array of keyframe values (with the exact same length as `AnyKeyframeGroup`),
143
199
  /// casts them to the expected type, and combined them into the final resulting keyframe.
200
+ ///
201
+ /// `requiresManualInterpolation` determines whether the keyframes must be computed using `Keyframes.manuallyInterpolated`,
202
+ /// which interpolates the value at each frame, or if the keyframes can simply be combined.
144
203
  private static func combined<CombinedResult>(
145
204
  _ allGroups: [AnyKeyframeGroup],
205
+ requiresManualInterpolation: Bool,
146
206
  makeCombinedResult: ([Any]) throws -> CombinedResult?)
147
207
  rethrows
148
208
  -> KeyframeGroup<CombinedResult>
@@ -155,6 +215,7 @@ enum Keyframes {
155
215
  let animatingKeyframes = untypedGroups.filter { $0.keyframes.count > 1 }
156
216
 
157
217
  guard
218
+ !requiresManualInterpolation,
158
219
  !allGroups.isEmpty,
159
220
  animatingKeyframes.allSatisfy({ $0.hasSameTimingParameters(as: animatingKeyframes[0]) })
160
221
  else {
@@ -222,7 +283,7 @@ enum Keyframes {
222
283
  extension KeyframeGroup {
223
284
  /// Whether or not all of the keyframes in this `KeyframeGroup` have the same
224
285
  /// timing parameters as the corresponding keyframe in the other given `KeyframeGroup`
225
- func hasSameTimingParameters<T>(as other: KeyframeGroup<T>) -> Bool {
286
+ func hasSameTimingParameters<U>(as other: KeyframeGroup<U>) -> Bool {
226
287
  guard keyframes.count == other.keyframes.count else {
227
288
  return false
228
289
  }
@@ -236,7 +297,7 @@ extension KeyframeGroup {
236
297
  extension Keyframe {
237
298
  /// Whether or not this keyframe has the same timing parameters as the given keyframe,
238
299
  /// excluding `spatialInTangent` and `spatialOutTangent`.
239
- fileprivate func hasSameTimingParameters<T>(as other: Keyframe<T>) -> Bool {
300
+ fileprivate func hasSameTimingParameters<U>(as other: Keyframe<U>) -> Bool {
240
301
  time == other.time
241
302
  && isHold == other.isHold
242
303
  && inTangent == other.inTangent
@@ -41,8 +41,8 @@ struct LayerAnimationContext {
41
41
  /// The AnimationKeypath represented by the current layer
42
42
  var currentKeypath: AnimationKeypath
43
43
 
44
- /// The `AnimationTextProvider`
45
- var textProvider: AnimationTextProvider
44
+ /// The `AnimationKeypathTextProvider`
45
+ var textProvider: AnimationKeypathTextProvider
46
46
 
47
47
  /// Records the given animation keypath so it can be logged or collected into a list
48
48
  /// - Used for `CoreAnimationLayer.logHierarchyKeypaths()` and `allHierarchyKeypaths()`
@@ -67,6 +67,15 @@ class BaseCompositionLayer: BaseAnimationLayer {
67
67
  inFrame: CGFloat(baseLayerModel.inFrame),
68
68
  outFrame: CGFloat(baseLayerModel.outFrame),
69
69
  context: context)
70
+
71
+ // There are two different drop shadow schemas, either using `DropShadowEffect` or `DropShadowStyle`.
72
+ // If both happen to be present, prefer the `DropShadowEffect` (which is the drop shadow schema
73
+ // supported on other platforms).
74
+ let dropShadowEffect = baseLayerModel.effects.first(where: { $0 is DropShadowEffect }) as? DropShadowModel
75
+ let dropShadowStyle = baseLayerModel.styles.first(where: { $0 is DropShadowStyle }) as? DropShadowModel
76
+ if let dropShadowModel = dropShadowEffect ?? dropShadowStyle {
77
+ try contentsLayer.addDropShadowAnimations(for: dropShadowModel, context: context)
78
+ }
70
79
  }
71
80
  }
72
81
 
@@ -48,6 +48,7 @@ final class ImageLayer: BaseCompositionLayer {
48
48
 
49
49
  self.imageAsset = imageAsset
50
50
  contentsLayer.contents = image
51
+ contentsLayer.contentsGravity = context.imageProvider.contentsGravity(for: imageAsset)
51
52
  setNeedsLayout()
52
53
  }
53
54
 
@@ -9,7 +9,7 @@ import QuartzCore
9
9
  struct LayerContext {
10
10
  let animation: LottieAnimation
11
11
  let imageProvider: AnimationImageProvider
12
- let textProvider: AnimationTextProvider
12
+ let textProvider: AnimationKeypathTextProvider
13
13
  let fontProvider: AnimationFontProvider
14
14
  let compatibilityTracker: CompatibilityTracker
15
15
  var layerName: String
@@ -93,4 +93,6 @@ extension RepeaterTransform: TransformModel {
93
93
  var _position: KeyframeGroup<LottieVector3D>? { position }
94
94
  var _positionX: KeyframeGroup<LottieVector1D>? { nil }
95
95
  var _positionY: KeyframeGroup<LottieVector1D>? { nil }
96
+ var _skew: KeyframeGroup<LottieVector1D>? { nil }
97
+ var _skewAxis: KeyframeGroup<LottieVector1D>? { nil }
96
98
  }
@@ -41,7 +41,8 @@ final class ShapeLayer: BaseCompositionLayer {
41
41
  if let repeater = shapeLayer.items.first(where: { $0 is Repeater }) as? Repeater {
42
42
  try setUpRepeater(repeater, context: context)
43
43
  } else {
44
- try setupGroups(from: shapeLayer.items, parentGroup: nil, parentGroupPath: [], context: context)
44
+ let shapeItems = shapeLayer.items.map { ShapeItemLayer.Item(item: $0, groupPath: []) }
45
+ try setupGroups(from: shapeItems, parentGroup: nil, parentGroupPath: [], context: context)
45
46
  }
46
47
  }
47
48
 
@@ -50,7 +51,8 @@ final class ShapeLayer: BaseCompositionLayer {
50
51
  let copyCount = Int(try repeater.copies.exactlyOneKeyframe(context: context, description: "repeater copies").value)
51
52
 
52
53
  for index in 0..<copyCount {
53
- for groupLayer in try makeGroupLayers(from: items, parentGroup: nil, parentGroupPath: [], context: context) {
54
+ let shapeItems = items.map { ShapeItemLayer.Item(item: $0, groupPath: []) }
55
+ for groupLayer in try makeGroupLayers(from: shapeItems, parentGroup: nil, parentGroupPath: [], context: context) {
54
56
  let repeatedLayer = RepeaterLayer(repeater: repeater, childLayer: groupLayer, index: index)
55
57
  addSublayer(repeatedLayer)
56
58
  }
@@ -127,7 +129,7 @@ final class GroupLayer: BaseAnimationLayer {
127
129
  private func setupLayerHierarchy(context: LayerContext) throws {
128
130
  // Groups can contain other groups, so we may have to continue
129
131
  // recursively creating more `GroupLayer`s
130
- try setupGroups(from: group.items, parentGroup: group, parentGroupPath: groupPath, context: context)
132
+ try setupGroups(from: items, parentGroup: group, parentGroupPath: groupPath, context: context)
131
133
 
132
134
  // Create `ShapeItemLayer`s for each subgroup of shapes that should be rendered as a single unit
133
135
  // - These groups are listed from front-to-back, so we have to add the sublayers in reverse order
@@ -183,7 +185,7 @@ extension CALayer {
183
185
  /// - Each `Group` item becomes its own `GroupLayer` sublayer.
184
186
  /// - Other `ShapeItem` are applied to all sublayers
185
187
  fileprivate func setupGroups(
186
- from items: [ShapeItem],
188
+ from items: [ShapeItemLayer.Item],
187
189
  parentGroup: Group?,
188
190
  parentGroupPath: [String],
189
191
  context: LayerContext)
@@ -204,29 +206,27 @@ extension CALayer {
204
206
  /// - Each `Group` item becomes its own `GroupLayer` sublayer.
205
207
  /// - Other `ShapeItem` are applied to all sublayers
206
208
  fileprivate func makeGroupLayers(
207
- from items: [ShapeItem],
209
+ from items: [ShapeItemLayer.Item],
208
210
  parentGroup: Group?,
209
211
  parentGroupPath: [String],
210
212
  context: LayerContext)
211
213
  throws -> [GroupLayer]
212
214
  {
213
- var (groupItems, otherItems) = items
214
- .filter { !$0.hidden }
215
- .grouped(by: { $0 is Group })
215
+ var groupItems = items.compactMap { $0.item as? Group }.filter { !$0.hidden }
216
+ var otherItems = items.filter { !($0.item is Group) && !$0.item.hidden }
216
217
 
217
218
  // Handle the top-level `shapeLayer.items` array. This is typically just a single `Group`,
218
219
  // but in practice can be any combination of items. The implementation expects all path-drawing
219
220
  // shape items to be managed by a `GroupLayer`, so if there's a top-level path item we
220
221
  // have to create a placeholder group.
221
- if parentGroup == nil, otherItems.contains(where: { $0.drawsCGPath }) {
222
- groupItems = [Group(items: items, name: "")]
222
+ if parentGroup == nil, otherItems.contains(where: { $0.item.drawsCGPath }) {
223
+ groupItems = [Group(items: items.map { $0.item }, name: "")]
223
224
  otherItems = []
224
225
  }
225
226
 
226
227
  // Any child items that wouldn't be included in a valid shape render group
227
228
  // need to be applied to child groups (otherwise they'd be silently ignored).
228
229
  let inheritedItemsForChildGroups = otherItems
229
- .map { ShapeItemLayer.Item(item: $0, groupPath: parentGroupPath) }
230
230
  .shapeRenderGroups(groupHasChildGroupsToInheritUnusedItems: !groupItems.isEmpty)
231
231
  .unusedItems
232
232
 
@@ -235,8 +235,6 @@ extension CALayer {
235
235
  let groupsInZAxisOrder = groupItems.reversed()
236
236
 
237
237
  return try groupsInZAxisOrder.compactMap { group in
238
- guard let group = group as? Group else { return nil }
239
-
240
238
  var pathForChildren = parentGroupPath
241
239
  if !group.name.isEmpty {
242
240
  pathForChildren.append(group.name)
@@ -30,16 +30,31 @@ final class SolidLayer: BaseCompositionLayer {
30
30
  super.init(layer: typedLayer)
31
31
  }
32
32
 
33
+ // MARK: Internal
34
+
35
+ override func setupAnimations(context: LayerAnimationContext) throws {
36
+ try super.setupAnimations(context: context)
37
+
38
+ // Even though the Lottie json schema provides a fixed `solidLayer.colorHex` value,
39
+ // we still need to create a set of keyframes and go through the standard `CAAnimation`
40
+ // codepath so that this value can be customized using the custom `ValueProvider`s API.
41
+ try shapeLayer.addAnimation(
42
+ for: .fillColor,
43
+ keyframes: KeyframeGroup(solidLayer.colorHex.cgColor),
44
+ value: { $0 },
45
+ context: context)
46
+ }
47
+
33
48
  // MARK: Private
34
49
 
35
50
  private let solidLayer: SolidLayerModel
36
51
 
52
+ /// Render the fill color in a child `CAShapeLayer`
53
+ /// - Using a `CAShapeLayer` specifically, instead of a `CALayer` with a `backgroundColor`,
54
+ /// allows the size of the fill shape to be different from `contentsLayer.size`.
55
+ private let shapeLayer = CAShapeLayer()
56
+
37
57
  private func setupContentLayer() {
38
- // Render the fill color in a child `CAShapeLayer`
39
- // - Using a `CAShapeLayer` specifically, instead of a `CALayer` with a `backgroundColor`,
40
- // allows the size of the fill shape to be different from `contentsLayer.size`.
41
- let shapeLayer = CAShapeLayer()
42
- shapeLayer.fillColor = solidLayer.colorHex.cgColor
43
58
  shapeLayer.path = CGPath(rect: .init(x: 0, y: 0, width: solidLayer.width, height: solidLayer.height), transform: nil)
44
59
  addSublayer(shapeLayer)
45
60
  }
@@ -44,9 +44,19 @@ final class TextLayer: BaseCompositionLayer {
44
44
  context: textAnimationContext,
45
45
  description: "text layer text")
46
46
 
47
- renderLayer.text = context.textProvider.textFor(
48
- keypathName: textAnimationContext.currentKeypath.fullPath,
49
- sourceText: sourceText.text)
47
+ // Prior to Lottie 4.3.0 the Core Animation rendering engine always just used `LegacyAnimationTextProvider`
48
+ // but incorrectly called it with the full keypath string, unlike the Main Thread rendering engine
49
+ // which only used the last component of the keypath. Starting in Lottie 4.3.0 we use `AnimationKeypathTextProvider`
50
+ // instead if implemented.
51
+ if let keypathTextValue = context.textProvider.text(for: textAnimationContext.currentKeypath, sourceText: sourceText.text) {
52
+ renderLayer.text = keypathTextValue
53
+ } else if let legacyTextProvider = context.textProvider as? LegacyAnimationTextProvider {
54
+ renderLayer.text = legacyTextProvider.textFor(
55
+ keypathName: textAnimationContext.currentKeypath.fullPath,
56
+ sourceText: sourceText.text)
57
+ } else {
58
+ renderLayer.text = sourceText.text
59
+ }
50
60
 
51
61
  renderLayer.sizeToFit()
52
62
  }