lottie-ios 4.1.3 → 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 (84) 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 +1 -1
  10. package/Rakefile +3 -3
  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 +68 -6
  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 +11 -11
  23. package/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +55 -32
  24. package/Sources/Private/CoreAnimation/Layers/AnimationLayer.swift +3 -3
  25. package/Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift +7 -9
  26. package/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift +9 -1
  27. package/Sources/Private/CoreAnimation/ValueProviderStore.swift +22 -11
  28. package/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.swift +1 -1
  29. package/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift +13 -2
  30. package/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.swift +4 -4
  31. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/EllipseNode.swift +1 -1
  32. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/PolygonNode.swift +2 -2
  33. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/RectNode.swift +1 -1
  34. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/StarNode.swift +2 -2
  35. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderContainers/GroupNode.swift +4 -4
  36. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/FillNode.swift +1 -1
  37. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientFillNode.swift +1 -1
  38. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientStrokeNode.swift +1 -1
  39. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift +1 -1
  40. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.swift +4 -4
  41. package/Sources/Private/Model/Assets/ImageAsset.swift +4 -3
  42. package/Sources/Private/Model/DotLottie/DotLottieAnimation.swift +2 -8
  43. package/Sources/Private/Model/DotLottie/DotLottieManifest.swift +3 -14
  44. package/Sources/Private/Model/DotLottie/DotLottieUtils.swift +11 -1
  45. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+BackingConfiguration.swift +147 -0
  46. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Helpers.swift +351 -0
  47. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+MemoryFile.swift +183 -0
  48. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Progress.swift +66 -0
  49. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Reading.swift +144 -0
  50. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+ReadingDeprecated.swift +49 -0
  51. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Writing.swift +385 -0
  52. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+WritingDeprecated.swift +91 -0
  53. package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+ZIP64.swift +170 -0
  54. package/Sources/Private/Model/DotLottie/{Zip/ZipArchive.swift → ZipFoundation/Archive.swift} +150 -227
  55. package/Sources/Private/Model/DotLottie/ZipFoundation/Data+Compression.swift +403 -0
  56. package/Sources/Private/Model/DotLottie/ZipFoundation/Data+CompressionDeprecated.swift +44 -0
  57. package/Sources/Private/Model/DotLottie/{Zip → ZipFoundation}/Data+Serialization.swift +62 -0
  58. package/Sources/Private/Model/DotLottie/{Zip/ZipEntry+Serialization.swift → ZipFoundation/Entry+Serialization.swift} +7 -7
  59. package/Sources/Private/Model/DotLottie/{Zip/ZipEntry+ZIP64.swift → ZipFoundation/Entry+ZIP64.swift} +13 -19
  60. package/Sources/Private/Model/DotLottie/{Zip/ZipEntry.swift → ZipFoundation/Entry.swift} +141 -10
  61. package/Sources/Private/Model/DotLottie/ZipFoundation/FileManager+ZIP.swift +368 -0
  62. package/Sources/Private/Model/DotLottie/ZipFoundation/README.md +24 -0
  63. package/Sources/Private/Model/DotLottie/ZipFoundation/URL+ZIP.swift +32 -0
  64. package/Sources/Private/Model/Extensions/Bundle.swift +5 -14
  65. package/Sources/Private/Model/Keyframes/KeyframeGroup.swift +31 -8
  66. package/Sources/Private/RootAnimationLayer.swift +3 -1
  67. package/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift +12 -4
  68. package/Sources/Private/Utility/Extensions/DataExtension.swift +14 -4
  69. package/Sources/Private/Utility/Primitives/BezierPathRoundExtension.swift +11 -0
  70. package/Sources/Public/Animation/LottieAnimationHelpers.swift +12 -10
  71. package/Sources/Public/Animation/LottieAnimationView.swift +52 -27
  72. package/Sources/Public/DotLottie/DotLottieFile.swift +11 -34
  73. package/Sources/Public/DotLottie/DotLottieFileHelpers.swift +92 -71
  74. package/Sources/Public/iOS/Compatibility/CompatibleAnimationView.swift +58 -0
  75. package/lottie-ios.podspec +1 -1
  76. package/package.json +1 -1
  77. package/LottieAnimation/LottieAnimation.xcodeproj/project.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  78. package/LottieAnimation/LottieAnimation.xcodeproj/project.xcworkspace/xcuserdata/valentinperignon.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  79. package/LottieAnimation/LottieAnimation.xcodeproj/xcuserdata/calstephens.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  80. package/LottieAnimation/LottieAnimation.xcodeproj/xcuserdata/valentinperignon.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
  81. package/LottieAnimation/LottieAnimation.xcodeproj/xcuserdata/valentinperignon.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  82. package/Sources/Private/Model/DotLottie/Zip/Data+Compression.swift +0 -134
  83. package/Sources/Private/Model/DotLottie/Zip/FileManager+ZIP.swift +0 -130
  84. package/Sources/Private/Utility/Interpolatable/KeyframeGroup+Extensions.swift +0 -59
@@ -57,7 +57,7 @@ extension CAShapeLayer {
57
57
 
58
58
  try addAnimation(
59
59
  for: .fillColor,
60
- keyframes: fill.color.keyframes,
60
+ keyframes: fill.color,
61
61
  value: \.cgColorValue,
62
62
  context: context)
63
63
 
@@ -71,7 +71,7 @@ extension CAShapeLayer {
71
71
 
72
72
  try addAnimation(
73
73
  for: .strokeStart,
74
- keyframes: strokeStartKeyframes.keyframes,
74
+ keyframes: strokeStartKeyframes,
75
75
  value: { strokeStart in
76
76
  // Lottie animation files express stoke trims as a numerical percentage value
77
77
  // (e.g. 25%, 50%, 100%) so we divide by 100 to get the decimal values
@@ -81,7 +81,7 @@ extension CAShapeLayer {
81
81
 
82
82
  try addAnimation(
83
83
  for: .strokeEnd,
84
- keyframes: strokeEndKeyframes.keyframes,
84
+ keyframes: strokeEndKeyframes,
85
85
  value: { strokeEnd in
86
86
  // Lottie animation files express stoke trims as a numerical percentage value
87
87
  // (e.g. 25%, 50%, 100%) so we divide by 100 to get the decimal values
@@ -112,14 +112,24 @@ extension Trim {
112
112
 
113
113
  // CAShapeLayer requires strokeStart to be less than strokeEnd. This
114
114
  // isn't required by the Lottie schema, so some animations may have
115
- // strokeStart and strokeEnd flipped. If we detect this is the case,
116
- // then swap them.
117
- if startValueIsAlwaysGreaterThanEndValue() {
115
+ // strokeStart and strokeEnd flipped.
116
+ if startValueIsAlwaysLessOrEqualToThanEndValue() {
117
+ // If the start value is always _less than_ or equal to the end value
118
+ // then we can use the given values without any modifications
119
+ strokeStart = start
120
+ strokeEnd = end
121
+ } else if startValueIsAlwaysGreaterThanOrEqualToEndValue() {
122
+ // If the start value is always _greater than_ or equal to the end value,
123
+ // then we can just swap the start / end keyframes. This lets us avoid
124
+ // manually interpolating the keyframes values at each frame, which
125
+ // would be more expensive.
118
126
  strokeStart = end
119
127
  strokeEnd = start
120
128
  } else {
121
- strokeStart = start
122
- strokeEnd = end
129
+ // Otherwise if the start / end values ever swap places we have to
130
+ // fix the order on a per-keyframe basis, which may require manually
131
+ // interpolating the keyframe values at each frame.
132
+ (strokeStart, strokeEnd) = interpolatedAtEachFrame()
123
133
  }
124
134
 
125
135
  // If there are no offsets, then the stroke values can be used as-is
@@ -130,22 +140,20 @@ extension Trim {
130
140
  return (strokeStart, strokeEnd, 1)
131
141
  }
132
142
 
133
- // Otherwise, adjust the stroke values to account for the offsets
134
- // 1. Interpolate the keyframes so they are all on a linear timing function
135
- // 2. Merge by summing the stroke values with the offset values
136
- let interpolatedStrokeStart = strokeStart.manuallyInterpolateKeyframes()
137
- let interpolatedStrokeEnd = strokeEnd.manuallyInterpolateKeyframes()
138
- let interpolatedStrokeOffset = offset.manuallyInterpolateKeyframes()
139
-
140
- var adjustedStrokeStart = KeyframeGroup(
141
- keyframes: try adjustKeyframesForTrimOffsets(
142
- strokeKeyframes: interpolatedStrokeStart,
143
- offsetKeyframes: interpolatedStrokeOffset))
144
-
145
- var adjustedStrokeEnd = KeyframeGroup(
146
- keyframes: try adjustKeyframesForTrimOffsets(
147
- strokeKeyframes: interpolatedStrokeEnd,
148
- offsetKeyframes: interpolatedStrokeOffset))
143
+ // Apply the offset to the start / end values at each frame
144
+ let offsetStrokeKeyframes = Keyframes.combined(
145
+ strokeStart,
146
+ strokeEnd,
147
+ offset,
148
+ makeCombinedResult: { start, end, offset -> (start: LottieVector1D, end: LottieVector1D) in
149
+ // Compute the adjusted value by converting the offset value to a stroke value
150
+ let offsetStart = start.cgFloatValue + (offset.cgFloatValue / 360 * 100)
151
+ let offsetEnd = end.cgFloatValue + (offset.cgFloatValue / 360 * 100)
152
+ return (start: LottieVector1D(offsetStart), end: LottieVector1D(offsetEnd))
153
+ })
154
+
155
+ var adjustedStrokeStart = offsetStrokeKeyframes.map { $0.start }
156
+ var adjustedStrokeEnd = offsetStrokeKeyframes.map { $0.end }
149
157
 
150
158
  // If maximum stroke value is larger than 100%, then we have to create copies of the path
151
159
  // so the total path length includes the maximum stroke
@@ -169,9 +177,23 @@ extension Trim {
169
177
 
170
178
  // MARK: Private
171
179
 
172
- /// Checks whether or not the value for `trim.start` is greater
173
- /// than the value for every `trim.end` at every keyframe.
174
- private func startValueIsAlwaysGreaterThanEndValue() -> Bool {
180
+ /// Checks whether or not the value for `trim.start` is less than
181
+ /// or equal to the value for every `trim.end` at every frame.
182
+ private func startValueIsAlwaysLessOrEqualToThanEndValue() -> Bool {
183
+ startAndEndValuesAllSatisfy { startValue, endValue in
184
+ startValue <= endValue
185
+ }
186
+ }
187
+
188
+ /// Checks whether or not the value for `trim.start` is greater than
189
+ /// or equal to the value for every `trim.end` at every frame.
190
+ private func startValueIsAlwaysGreaterThanOrEqualToEndValue() -> Bool {
191
+ startAndEndValuesAllSatisfy { startValue, endValue in
192
+ startValue >= endValue
193
+ }
194
+ }
195
+
196
+ private func startAndEndValuesAllSatisfy(_ condition: (_ start: CGFloat, _ end: CGFloat) -> Bool) -> Bool {
175
197
  let keyframeTimes = Set(start.keyframes.map { $0.time } + end.keyframes.map { $0.time })
176
198
 
177
199
  let startInterpolator = KeyframeInterpolator(keyframes: start.keyframes)
@@ -183,7 +205,7 @@ extension Trim {
183
205
  let endAtTime = endInterpolator.value(frame: keyframeTime) as? LottieVector1D
184
206
  else { continue }
185
207
 
186
- if startAtTime.cgFloatValue < endAtTime.cgFloatValue {
208
+ if !condition(startAtTime.cgFloatValue, endAtTime.cgFloatValue) {
187
209
  return false
188
210
  }
189
211
  }
@@ -191,82 +213,24 @@ extension Trim {
191
213
  return true
192
214
  }
193
215
 
194
- /// Adjusted stroke keyframes to account for offset keyframes by merging them into a single keyframe collection
195
- ///
196
- /// Since stroke keyframes and offset keyframes can be defined on different animation curves, they must be
197
- /// manually interpolated prior to invoking this method. Manually interpolating the keyframes will redefine both
198
- /// keyframe groups such that they can be interpolated linearly.
199
- ///
200
- /// - Precondition: The keyframes must be interpolated using `KeyframeGroup.manuallyInterpolateKeyframes()`
201
- private func adjustKeyframesForTrimOffsets(
202
- strokeKeyframes: ContiguousArray<Keyframe<LottieVector1D>>,
203
- offsetKeyframes: ContiguousArray<Keyframe<LottieVector1D>>)
204
- throws -> ContiguousArray<Keyframe<LottieVector1D>>
216
+ /// Interpolates the start and end keyframes, at each frame if necessary,
217
+ /// so that the value of `strokeStart` is always less than `strokeEnd`.
218
+ private func interpolatedAtEachFrame()
219
+ -> (strokeStart: KeyframeGroup<LottieVector1D>, strokeEnd: KeyframeGroup<LottieVector1D>)
205
220
  {
206
- guard
207
- !strokeKeyframes.isEmpty,
208
- !offsetKeyframes.isEmpty
209
- else {
210
- return strokeKeyframes
211
- }
212
-
213
- // Map each time to its corresponding stroke/offset keyframe
214
- var timeMap = [AnimationFrameTime: [Keyframe<LottieVector1D>?]]()
215
- for stroke in strokeKeyframes {
216
- timeMap[stroke.time] = [stroke, nil]
217
- }
218
- for offset in offsetKeyframes {
219
- if var existing = timeMap[offset.time] {
220
- existing[1] = offset
221
- timeMap[offset.time] = existing
222
- } else {
223
- timeMap[offset.time] = [nil, offset]
224
- }
225
- }
226
-
227
- // Each time will be mapped to a new, adjusted keyframe
228
- var output = ContiguousArray<Keyframe<LottieVector1D>>()
229
- var lastKeyframe: Keyframe<LottieVector1D>?
230
- var lastOffset: Keyframe<LottieVector1D>?
231
-
232
- for (time, values) in timeMap.sorted(by: { $0.0 < $1.0 }) {
233
- // Extract keyframe/offset associated with this timestamp
234
- let keyframe = values[0]
235
- let offset = values[1]
236
- lastKeyframe = keyframe ?? lastKeyframe
237
- lastOffset = offset ?? lastOffset
238
-
239
- guard let currentKeyframe = lastKeyframe else {
240
- // No keyframes are output until the first keyframe occurs
241
- continue
242
- }
243
-
244
- guard let currentOffset = lastOffset else {
245
- // Scalar isHold keyframes are not output as they offset the offset keyframes
246
- if !(strokeKeyframes.count == 1 && currentKeyframe.isHold) {
247
- output.append(currentKeyframe)
221
+ let combinedKeyframes = Keyframes.combined(
222
+ start,
223
+ end,
224
+ makeCombinedResult: { startValue, endValue -> (start: LottieVector1D, end: LottieVector1D) in
225
+ if startValue.cgFloatValue < endValue.cgFloatValue {
226
+ return (start: startValue, end: endValue)
227
+ } else {
228
+ return (start: endValue, end: startValue)
248
229
  }
249
- continue
250
- }
230
+ })
251
231
 
252
- // Compute the adjusted value by converting the offset value to a stroke value
253
- let strokeValue = currentKeyframe.value.value
254
- let offsetValue = currentOffset.value.value
255
- let adjustedValue = strokeValue + (offsetValue / 360 * 100)
256
-
257
- // The tangent values are all `nil` as the keyframes should have been manually interpolated
258
- let adjustedKeyframe = Keyframe<LottieVector1D>(
259
- value: LottieVector1D(adjustedValue),
260
- time: time,
261
- isHold: currentKeyframe.isHold,
262
- inTangent: nil,
263
- outTangent: nil,
264
- spatialInTangent: nil,
265
- spatialOutTangent: nil)
266
-
267
- output.append(adjustedKeyframe)
268
- }
269
-
270
- return output
232
+ return (
233
+ strokeStart: combinedKeyframes.map { $0.start },
234
+ strokeEnd: combinedKeyframes.map { $0.end })
271
235
  }
272
236
  }
@@ -36,7 +36,7 @@ extension CAShapeLayer {
36
36
  {
37
37
  try addAnimation(
38
38
  for: .path,
39
- keyframes: try star.combinedKeyframes().keyframes,
39
+ keyframes: try star.combinedKeyframes(),
40
40
  value: { keyframe in
41
41
  BezierPath.star(
42
42
  position: keyframe.position.pointValue,
@@ -62,7 +62,7 @@ extension CAShapeLayer {
62
62
  {
63
63
  try addAnimation(
64
64
  for: .path,
65
- keyframes: try star.combinedKeyframes().keyframes,
65
+ keyframes: try star.combinedKeyframes(),
66
66
  value: { keyframe in
67
67
  BezierPath.polygon(
68
68
  position: keyframe.position.pointValue,
@@ -54,14 +54,14 @@ extension CAShapeLayer {
54
54
  if let strokeColor = stroke.strokeColor {
55
55
  try addAnimation(
56
56
  for: .strokeColor,
57
- keyframes: strokeColor.keyframes,
57
+ keyframes: strokeColor,
58
58
  value: \.cgColorValue,
59
59
  context: context)
60
60
  }
61
61
 
62
62
  try addAnimation(
63
63
  for: .lineWidth,
64
- keyframes: stroke.width.keyframes,
64
+ keyframes: stroke.width,
65
65
  value: \.cgFloatValue,
66
66
  context: context)
67
67
 
@@ -79,7 +79,7 @@ extension CAShapeLayer {
79
79
 
80
80
  try addAnimation(
81
81
  for: .lineDashPhase,
82
- keyframes: dashPhase,
82
+ keyframes: KeyframeGroup(keyframes: dashPhase),
83
83
  value: \.cgFloatValue,
84
84
  context: context)
85
85
  }
@@ -93,15 +93,15 @@ extension CALayer {
93
93
  context: LayerAnimationContext)
94
94
  throws
95
95
  {
96
- if let positionKeyframes = transformModel._position?.keyframes {
96
+ if let positionKeyframes = transformModel._position {
97
97
  try addAnimation(
98
98
  for: .position,
99
99
  keyframes: positionKeyframes,
100
100
  value: \.pointValue,
101
101
  context: context)
102
102
  } else if
103
- let xKeyframes = transformModel._positionX?.keyframes,
104
- let yKeyframes = transformModel._positionY?.keyframes
103
+ let xKeyframes = transformModel._positionX,
104
+ let yKeyframes = transformModel._positionY
105
105
  {
106
106
  try addAnimation(
107
107
  for: .positionX,
@@ -129,7 +129,7 @@ extension CALayer {
129
129
  {
130
130
  try addAnimation(
131
131
  for: .anchorPoint,
132
- keyframes: transformModel.anchorPoint.keyframes,
132
+ keyframes: transformModel.anchorPoint,
133
133
  value: { absoluteAnchorPoint in
134
134
  guard bounds.width > 0, bounds.height > 0 else {
135
135
  context.logger.assertionFailure("Size must be non-zero before an animation can be played")
@@ -154,7 +154,7 @@ extension CALayer {
154
154
  {
155
155
  try addAnimation(
156
156
  for: .scaleX,
157
- keyframes: transformModel.scale.keyframes,
157
+ keyframes: transformModel.scale,
158
158
  value: { scale in
159
159
  // Lottie animation files express scale as a numerical percentage value
160
160
  // (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values
@@ -207,7 +207,7 @@ extension CALayer {
207
207
 
208
208
  try addAnimation(
209
209
  for: .rotationY,
210
- keyframes: transformModel.scale.keyframes,
210
+ keyframes: transformModel.scale,
211
211
  value: { scale in
212
212
  if scale.x < 0 {
213
213
  return .pi
@@ -220,7 +220,7 @@ extension CALayer {
220
220
 
221
221
  try addAnimation(
222
222
  for: .scaleY,
223
- keyframes: transformModel.scale.keyframes,
223
+ keyframes: transformModel.scale,
224
224
  value: { scale in
225
225
  // Lottie animation files express scale as a numerical percentage value
226
226
  // (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values
@@ -263,7 +263,7 @@ extension CALayer {
263
263
 
264
264
  try addAnimation(
265
265
  for: .rotationX,
266
- keyframes: transformModel.rotationX.keyframes,
266
+ keyframes: transformModel.rotationX,
267
267
  value: { rotationDegrees in
268
268
  rotationDegrees.cgFloatValue * .pi / 180
269
269
  },
@@ -271,7 +271,7 @@ extension CALayer {
271
271
 
272
272
  try addAnimation(
273
273
  for: .rotationY,
274
- keyframes: transformModel.rotationY.keyframes,
274
+ keyframes: transformModel.rotationY,
275
275
  value: { rotationDegrees in
276
276
  rotationDegrees.cgFloatValue * .pi / 180
277
277
  },
@@ -279,7 +279,7 @@ extension CALayer {
279
279
 
280
280
  try addAnimation(
281
281
  for: .rotationZ,
282
- keyframes: transformModel.rotationZ.keyframes,
282
+ keyframes: transformModel.rotationZ,
283
283
  value: { rotationDegrees in
284
284
  // Lottie animation files express rotation in degrees
285
285
  // (e.g. 90º, 180º, 360º) so we covert to radians to get the
@@ -321,7 +321,7 @@ extension CALayer {
321
321
 
322
322
  try addAnimation(
323
323
  for: .transform,
324
- keyframes: combinedTransformKeyframes.keyframes,
324
+ keyframes: combinedTransformKeyframes,
325
325
  value: { $0 },
326
326
  context: context)
327
327
  }
@@ -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) {
@@ -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
@@ -48,22 +48,20 @@ class BaseCompositionLayer: BaseAnimationLayer {
48
48
  /// and all child `AnimationLayer`s.
49
49
  /// - Can be overridden by subclasses, which much call `super`.
50
50
  override func setupAnimations(context: LayerAnimationContext) throws {
51
- var context = context
52
- if renderLayerContents {
53
- context = context.addingKeypathComponent(baseLayerModel.name)
54
- }
51
+ let layerContext = context.addingKeypathComponent(baseLayerModel.name)
52
+ let childContext = renderLayerContents ? layerContext : context
55
53
 
56
- try setupLayerAnimations(context: context)
57
- try setupChildAnimations(context: context)
54
+ try setupLayerAnimations(context: layerContext)
55
+ try setupChildAnimations(context: childContext)
58
56
  }
59
57
 
60
58
  func setupLayerAnimations(context: LayerAnimationContext) throws {
61
- let context = context.addingKeypathComponent(baseLayerModel.name)
59
+ let transformContext = context.addingKeypathComponent("Transform")
62
60
 
63
- try contentsLayer.addTransformAnimations(for: baseLayerModel.transform, context: context)
61
+ try contentsLayer.addTransformAnimations(for: baseLayerModel.transform, context: transformContext)
64
62
 
65
63
  if renderLayerContents {
66
- try contentsLayer.addOpacityAnimation(for: baseLayerModel.transform, context: context)
64
+ try contentsLayer.addOpacityAnimation(for: baseLayerModel.transform, context: transformContext)
67
65
 
68
66
  contentsLayer.addVisibilityAnimation(
69
67
  inFrame: CGFloat(baseLayerModel.inFrame),
@@ -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
  }