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.
- package/.github/workflows/main.yml +27 -9
- package/Lottie.xcodeproj/project.pbxproj +158 -70
- package/Lottie.xcodeproj/xcuserdata/calstephens.xcuserdatad/xcschemes/xcschememanagement.plist +2 -2
- package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/IDEFindNavigatorScopes.plist +5 -0
- package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +258 -0
- package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Expressions.xcexplist +13 -2
- package/Package.swift +2 -1
- package/README.md +1 -1
- package/Rakefile +3 -3
- package/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift +16 -2
- package/Sources/Private/CoreAnimation/Animations/CombinedShapeAnimation.swift +1 -1
- package/Sources/Private/CoreAnimation/Animations/CustomPathAnimation.swift +1 -1
- package/Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift +1 -1
- package/Sources/Private/CoreAnimation/Animations/GradientAnimations.swift +6 -6
- package/Sources/Private/CoreAnimation/Animations/LayerProperty.swift +68 -6
- package/Sources/Private/CoreAnimation/Animations/OpacityAnimation.swift +1 -1
- package/Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift +1 -1
- package/Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift +66 -102
- package/Sources/Private/CoreAnimation/Animations/StarAnimation.swift +2 -2
- package/Sources/Private/CoreAnimation/Animations/StrokeAnimation.swift +3 -3
- package/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift +11 -11
- package/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +55 -32
- package/Sources/Private/CoreAnimation/Layers/AnimationLayer.swift +3 -3
- package/Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift +7 -9
- package/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift +9 -1
- package/Sources/Private/CoreAnimation/ValueProviderStore.swift +22 -11
- package/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.swift +1 -1
- package/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift +13 -2
- package/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.swift +4 -4
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/EllipseNode.swift +1 -1
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/PolygonNode.swift +2 -2
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/RectNode.swift +1 -1
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/StarNode.swift +2 -2
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderContainers/GroupNode.swift +4 -4
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/FillNode.swift +1 -1
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientFillNode.swift +1 -1
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientStrokeNode.swift +1 -1
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift +1 -1
- package/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.swift +4 -4
- package/Sources/Private/Model/Assets/ImageAsset.swift +4 -3
- package/Sources/Private/Model/DotLottie/DotLottieAnimation.swift +2 -8
- package/Sources/Private/Model/DotLottie/DotLottieManifest.swift +3 -14
- package/Sources/Private/Model/DotLottie/DotLottieUtils.swift +11 -1
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+BackingConfiguration.swift +147 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Helpers.swift +351 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+MemoryFile.swift +183 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Progress.swift +66 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Reading.swift +144 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+ReadingDeprecated.swift +49 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+Writing.swift +385 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+WritingDeprecated.swift +91 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Archive+ZIP64.swift +170 -0
- package/Sources/Private/Model/DotLottie/{Zip/ZipArchive.swift → ZipFoundation/Archive.swift} +150 -227
- package/Sources/Private/Model/DotLottie/ZipFoundation/Data+Compression.swift +403 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/Data+CompressionDeprecated.swift +44 -0
- package/Sources/Private/Model/DotLottie/{Zip → ZipFoundation}/Data+Serialization.swift +62 -0
- package/Sources/Private/Model/DotLottie/{Zip/ZipEntry+Serialization.swift → ZipFoundation/Entry+Serialization.swift} +7 -7
- package/Sources/Private/Model/DotLottie/{Zip/ZipEntry+ZIP64.swift → ZipFoundation/Entry+ZIP64.swift} +13 -19
- package/Sources/Private/Model/DotLottie/{Zip/ZipEntry.swift → ZipFoundation/Entry.swift} +141 -10
- package/Sources/Private/Model/DotLottie/ZipFoundation/FileManager+ZIP.swift +368 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/README.md +24 -0
- package/Sources/Private/Model/DotLottie/ZipFoundation/URL+ZIP.swift +32 -0
- package/Sources/Private/Model/Extensions/Bundle.swift +5 -14
- package/Sources/Private/Model/Keyframes/KeyframeGroup.swift +31 -8
- package/Sources/Private/RootAnimationLayer.swift +3 -1
- package/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift +12 -4
- package/Sources/Private/Utility/Extensions/DataExtension.swift +14 -4
- package/Sources/Private/Utility/Primitives/BezierPathRoundExtension.swift +11 -0
- package/Sources/Public/Animation/LottieAnimationHelpers.swift +12 -10
- package/Sources/Public/Animation/LottieAnimationView.swift +52 -27
- package/Sources/Public/DotLottie/DotLottieFile.swift +11 -34
- package/Sources/Public/DotLottie/DotLottieFileHelpers.swift +92 -71
- package/Sources/Public/iOS/Compatibility/CompatibleAnimationView.swift +58 -0
- package/lottie-ios.podspec +1 -1
- package/package.json +1 -1
- package/LottieAnimation/LottieAnimation.xcodeproj/project.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/LottieAnimation/LottieAnimation.xcodeproj/project.xcworkspace/xcuserdata/valentinperignon.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/LottieAnimation/LottieAnimation.xcodeproj/xcuserdata/calstephens.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/LottieAnimation/LottieAnimation.xcodeproj/xcuserdata/valentinperignon.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
- package/LottieAnimation/LottieAnimation.xcodeproj/xcuserdata/valentinperignon.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/Sources/Private/Model/DotLottie/Zip/Data+Compression.swift +0 -134
- package/Sources/Private/Model/DotLottie/Zip/FileManager+ZIP.swift +0 -130
- 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
|
|
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
|
|
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
|
|
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.
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
var
|
|
146
|
-
|
|
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
|
|
173
|
-
///
|
|
174
|
-
private func
|
|
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
|
|
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
|
-
///
|
|
195
|
-
///
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
250
|
-
}
|
|
230
|
+
})
|
|
251
231
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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()
|
|
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()
|
|
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
|
|
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
|
|
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
|
|
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
|
|
104
|
-
let yKeyframes = transformModel._positionY
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
///
|
|
48
|
-
/// - Used for `CoreAnimationLayer.logHierarchyKeypaths()`
|
|
49
|
-
var
|
|
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
|
-
|
|
52
|
-
|
|
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:
|
|
57
|
-
try setupChildAnimations(context:
|
|
54
|
+
try setupLayerAnimations(context: layerContext)
|
|
55
|
+
try setupChildAnimations(context: childContext)
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
func setupLayerAnimations(context: LayerAnimationContext) throws {
|
|
61
|
-
let
|
|
59
|
+
let transformContext = context.addingKeypathComponent("Transform")
|
|
62
60
|
|
|
63
|
-
try contentsLayer.addTransformAnimations(for: baseLayerModel.transform, context:
|
|
61
|
+
try contentsLayer.addTransformAnimations(for: baseLayerModel.transform, context: transformContext)
|
|
64
62
|
|
|
65
63
|
if renderLayerContents {
|
|
66
|
-
try contentsLayer.addOpacityAnimation(for: baseLayerModel.transform, 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
|
-
|
|
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
|
-
|
|
29
|
+
let supportedProperties = PropertyName.allCases.map { $0.rawValue }
|
|
30
|
+
let propertyBeingCustomized = keypath.keys.last ?? ""
|
|
31
|
+
|
|
30
32
|
logger.assert(
|
|
31
|
-
|
|
32
|
-
"
|
|
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
|
-
|
|
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
|
|
129
|
-
//
|
|
130
|
-
|
|
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
|
|
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:
|
|
145
|
+
regex = regex.replacingOccurrences(of: singleWildcardMarker, with: "[^.]+")
|
|
135
146
|
|
|
136
147
|
return fullPath.range(of: regex, options: .regularExpression) != nil
|
|
137
148
|
}
|