Skip to content

Commit

Permalink
Fix support for existing samples using time remapping
Browse files Browse the repository at this point in the history
  • Loading branch information
calda committed Jan 9, 2024
1 parent e143dbe commit aa6822b
Show file tree
Hide file tree
Showing 39 changed files with 281 additions and 141 deletions.
20 changes: 10 additions & 10 deletions Lottie.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -331,10 +331,10 @@
0887347C28F0CCDD00458627 /* LottieAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0887347428F0CCDD00458627 /* LottieAnimationView.swift */; };
0887347D28F0CCDD00458627 /* LottieAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0887347428F0CCDD00458627 /* LottieAnimationView.swift */; };
089C50C22ABA0C6D007903D3 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089C50C12ABA0C6D007903D3 /* LoggingTests.swift */; };
089E5D9F2B4CCD3F00F4F836 /* Keyframes+timeRemapped.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089E5D9E2B4CCD3F00F4F836 /* Keyframes+timeRemapped.swift */; };
089E5DA02B4CCD3F00F4F836 /* Keyframes+timeRemapped.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089E5D9E2B4CCD3F00F4F836 /* Keyframes+timeRemapped.swift */; };
089E5DA12B4CCD3F00F4F836 /* Keyframes+timeRemapped.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089E5D9E2B4CCD3F00F4F836 /* Keyframes+timeRemapped.swift */; };
089E5DA22B4CCD3F00F4F836 /* Keyframes+timeRemapped.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089E5D9E2B4CCD3F00F4F836 /* Keyframes+timeRemapped.swift */; };
089E5D9F2B4CCD3F00F4F836 /* Keyframes+timeRemap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089E5D9E2B4CCD3F00F4F836 /* Keyframes+timeRemap.swift */; };
089E5DA02B4CCD3F00F4F836 /* Keyframes+timeRemap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089E5D9E2B4CCD3F00F4F836 /* Keyframes+timeRemap.swift */; };
089E5DA12B4CCD3F00F4F836 /* Keyframes+timeRemap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089E5D9E2B4CCD3F00F4F836 /* Keyframes+timeRemap.swift */; };
089E5DA22B4CCD3F00F4F836 /* Keyframes+timeRemap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089E5D9E2B4CCD3F00F4F836 /* Keyframes+timeRemap.swift */; };
08AB05552A61C20400DE86FD /* ReducedMotionOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB05542A61C20400DE86FD /* ReducedMotionOption.swift */; };
08AB05562A61C20400DE86FD /* ReducedMotionOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB05542A61C20400DE86FD /* ReducedMotionOption.swift */; };
08AB05572A61C20400DE86FD /* ReducedMotionOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB05542A61C20400DE86FD /* ReducedMotionOption.swift */; };
Expand Down Expand Up @@ -1190,7 +1190,7 @@
0887347328F0CCDD00458627 /* LottieAnimationViewInitializers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LottieAnimationViewInitializers.swift; sourceTree = "<group>"; };
0887347428F0CCDD00458627 /* LottieAnimationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LottieAnimationView.swift; sourceTree = "<group>"; };
089C50C12ABA0C6D007903D3 /* LoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingTests.swift; sourceTree = "<group>"; };
089E5D9E2B4CCD3F00F4F836 /* Keyframes+timeRemapped.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Keyframes+timeRemapped.swift"; sourceTree = "<group>"; };
089E5D9E2B4CCD3F00F4F836 /* Keyframes+timeRemap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Keyframes+timeRemap.swift"; sourceTree = "<group>"; };
08AB05542A61C20400DE86FD /* ReducedMotionOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReducedMotionOption.swift; sourceTree = "<group>"; };
08AB05582A61C5B700DE86FD /* DecodingStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingStrategy.swift; sourceTree = "<group>"; };
08AB055C2A61C5CC00DE86FD /* RenderingEngineOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingEngineOption.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2150,7 +2150,7 @@
2E9C95AA2822F43100677516 /* CALayer+fillBounds.swift */,
2E9C95AB2822F43100677516 /* Keyframes+combined.swift */,
2E9C95AC2822F43100677516 /* KeyframeGroup+exactlyOneKeyframe.swift */,
089E5D9E2B4CCD3F00F4F836 /* Keyframes+timeRemapped.swift */,
089E5D9E2B4CCD3F00F4F836 /* Keyframes+timeRemap.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -2948,7 +2948,7 @@
080DEFB12A9570FE00BE2D96 /* WillDisplayProviding.swift in Sources */,
080DEFE82A95711E00BE2D96 /* ImageLayer.swift in Sources */,
080DF0672A95717600BE2D96 /* AnimatorNodeDebugging.swift in Sources */,
089E5DA22B4CCD3F00F4F836 /* Keyframes+timeRemapped.swift in Sources */,
089E5DA22B4CCD3F00F4F836 /* Keyframes+timeRemap.swift in Sources */,
080DEFAC2A9570FE00BE2D96 /* StyleIDProviding.swift in Sources */,
080DF0002A95712400BE2D96 /* CombinedShapeAnimation.swift in Sources */,
080DEFD42A95711400BE2D96 /* Archive+Helpers.swift in Sources */,
Expand Down Expand Up @@ -3011,7 +3011,7 @@
0887346F28F0CBDE00458627 /* LottieAnimation.swift in Sources */,
08C002002A46150D00AB54BA /* Data+CompressionDeprecated.swift in Sources */,
0820D5B82A8BF159007D705C /* DropShadowStyle.swift in Sources */,
089E5D9F2B4CCD3F00F4F836 /* Keyframes+timeRemapped.swift in Sources */,
089E5D9F2B4CCD3F00F4F836 /* Keyframes+timeRemap.swift in Sources */,
2E9C97412822F43100677516 /* TestHelpers.swift in Sources */,
08EF21DC289C643B0097EA47 /* KeyframeInterpolator.swift in Sources */,
2E9C96152822F43100677516 /* Transform.swift in Sources */,
Expand Down Expand Up @@ -3326,7 +3326,7 @@
2E9C966D2822F43100677516 /* LayerImageProvider.swift in Sources */,
2EAF5ABD27A0798700E00531 /* FilepathImageProvider.swift in Sources */,
2EAF5AEA27A0798700E00531 /* AnimationTextProvider.swift in Sources */,
089E5DA02B4CCD3F00F4F836 /* Keyframes+timeRemapped.swift in Sources */,
089E5DA02B4CCD3F00F4F836 /* Keyframes+timeRemap.swift in Sources */,
08E206FE2A56014E002DCE17 /* Diffable.swift in Sources */,
2E9C96672822F43100677516 /* LayerTransformNode.swift in Sources */,
0887347028F0CBDE00458627 /* LottieAnimation.swift in Sources */,
Expand Down Expand Up @@ -3615,7 +3615,7 @@
0887347128F0CBDE00458627 /* LottieAnimation.swift in Sources */,
2E9C97432822F43100677516 /* TestHelpers.swift in Sources */,
0820D5BA2A8BF159007D705C /* DropShadowStyle.swift in Sources */,
089E5DA12B4CCD3F00F4F836 /* Keyframes+timeRemapped.swift in Sources */,
089E5DA12B4CCD3F00F4F836 /* Keyframes+timeRemap.swift in Sources */,
08EF21DE289C643B0097EA47 /* KeyframeInterpolator.swift in Sources */,
2E9C96172822F43100677516 /* Transform.swift in Sources */,
2E9C97492822F43100677516 /* CGFloatExtensions.swift in Sources */,
Expand Down
42 changes: 22 additions & 20 deletions Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,10 @@ extension CALayer {
keyframes keyframeGroup: KeyframeGroup<KeyframeValue>,
value keyframeValueMapping: (KeyframeValue) throws -> ValueRepresentation,
context: LayerAnimationContext)
throws
throws
-> CAAnimation?
where KeyframeValue: AnyInterpolatable
{
if context.mustUseComplexTimeRemapping {
let manuallyInterpolatedKeyframes = Keyframes.timeRemapped(keyframeGroup, context: context)

var context = context
context.mustUseComplexTimeRemapping = false
return try defaultAnimation(for: property, keyframes: manuallyInterpolatedKeyframes, value: keyframeValueMapping, context: context)
}

let keyframes = keyframeGroup.keyframes
guard !keyframes.isEmpty else { return nil }

Expand All @@ -72,16 +64,26 @@ extension CALayer {
""")
}

// If there is exactly one keyframe value, we can improve performance
// by applying that value directly to the layer instead of creating
// a relatively expensive `CAKeyframeAnimation`.
// If there is exactly one keyframe value that doesn't animate,
// we can improve performance by applying that value directly to the layer
// instead of creating a relatively expensive `CAKeyframeAnimation`.
if keyframes.count == 1 {
return singleKeyframeAnimation(
for: property,
keyframeValue: try keyframeValueMapping(keyframes[0].value),
writeDirectlyToPropertyIfPossible: true)
}

/// If we're required to use the `complexTimeRemapping` from some parent `PreCompLayer`,
/// we have to manually interpolate the keyframes with the time remapping applied.
if context.mustUseComplexTimeRemapping {
return try defaultAnimation(
for: property,
keyframes: Keyframes.manuallyInterpolatedWithTimeRemapping(keyframeGroup, context: context),
value: keyframeValueMapping,
context: context.withoutTimeRemapping())
}

// Split the keyframes into segments with the same `CAAnimationCalculationMode` value
// - Each of these segments will become their own `CAKeyframeAnimation`
let animationSegments = keyframes.segmentsSplitByCalculationMode()
Expand Down Expand Up @@ -190,8 +192,8 @@ extension CALayer {
// all of which have a non-zero number of keyframes.
let segmentAnimations: [CAKeyframeAnimation] = try animationSegments.indices.map { index in
let animationSegment = animationSegments[index]
var segmentStartTime = context.time(for: animationSegment.first!.time)
var segmentEndTime = context.time(for: animationSegment.last!.time)
var segmentStartTime = try context.time(forFrame: animationSegment.first!.time)
var segmentEndTime = try context.time(forFrame: animationSegment.last!.time)

// Every portion of the animation timeline has to be covered by a `CAKeyframeAnimation`,
// so if this is the first or last segment then the start/end time should be exactly
Expand All @@ -201,13 +203,13 @@ extension CALayer {

if isFirstSegment {
segmentStartTime = min(
context.time(for: context.animation.startFrame),
try context.time(forFrame: context.animation.startFrame),
segmentStartTime)
}

if isLastSegment {
segmentEndTime = max(
context.time(for: context.animation.endFrame),
try context.time(forFrame: context.animation.endFrame),
segmentEndTime)
}

Expand All @@ -217,8 +219,8 @@ extension CALayer {
// relative to 0 (`segmentStartTime`) and 1 (`segmentEndTime`). This is different
// from the default behavior of the `keyframeAnimation` method, where times
// are expressed relative to the entire animation duration.
let customKeyTimes = animationSegment.map { keyframeModel -> NSNumber in
let keyframeTime = context.time(for: keyframeModel.time)
let customKeyTimes = try animationSegment.map { keyframeModel -> NSNumber in
let keyframeTime = try context.time(forFrame: keyframeModel.time)
let segmentProgressTime = ((keyframeTime - segmentStartTime) / segmentDuration)
return segmentProgressTime as NSNumber
}
Expand Down Expand Up @@ -252,8 +254,8 @@ extension CALayer {
{
// Convert the list of `Keyframe<T>` into
// the representation used by `CAKeyframeAnimation`
var keyTimes = customKeyTimes ?? keyframes.map { keyframeModel -> NSNumber in
NSNumber(value: Float(context.progressTime(for: keyframeModel.time)))
var keyTimes = try customKeyTimes ?? keyframes.map { keyframeModel -> NSNumber in
NSNumber(value: Float(try context.progressTime(for: keyframeModel.time)))
}

var timingFunctions = timingFunctions(for: keyframes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ extension GradientRenderLayer {
}
}

// MARK: - RadialGradientKeyframes

private struct RadialGradientKeyframes: Interpolatable {
let startPoint: CGPoint
let endPoint: CGPoint
Expand Down
7 changes: 7 additions & 0 deletions Sources/Private/CoreAnimation/Animations/LayerProperty.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ extension LayerProperty {
customizableProperty: .opacity)
}

static var isHidden: LayerProperty<Bool> {
.init(
caLayerKeypath: #keyPath(CALayer.isHidden),
defaultValue: false,
customizableProperty: nil /* unsupported */ )
}

static var transform: LayerProperty<CATransform3D> {
.init(
caLayerKeypath: #keyPath(CALayer.transform),
Expand Down
9 changes: 8 additions & 1 deletion Sources/Private/CoreAnimation/Animations/StarAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,14 @@ extension Star {
let rotation: LottieVector1D

func interpolate(to: Star.Keyframe, amount: CGFloat) -> Star.Keyframe {
fatalError("TODO")
Star.Keyframe(
position: position.interpolate(to: to.position, amount: amount),
outerRadius: outerRadius.interpolate(to: to.outerRadius, amount: amount),
innerRadius: innerRadius.interpolate(to: to.innerRadius, amount: amount),
outerRoundness: outerRoundness.interpolate(to: to.outerRoundness, amount: amount),
innerRoundness: innerRoundness.interpolate(to: to.innerRoundness, amount: amount),
points: points.interpolate(to: to.points, amount: amount),
rotation: rotation.interpolate(to: to.rotation, amount: amount))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,9 +334,17 @@ extension TransformModel {
}
}

// MARK: - CATransform3D + AnyInterpolatable

extension CATransform3D: AnyInterpolatable {
@available(*, deprecated)
public func _interpolate(to: CATransform3D, amount: CGFloat, spatialOutTangent: CGPoint?, spatialInTangent: CGPoint?) -> CATransform3D {
public func _interpolate(
to _: CATransform3D,
amount _: CGFloat,
spatialOutTangent _: CGPoint?,
spatialInTangent _: CGPoint?)
-> CATransform3D
{
fatalError("Unused, only present to satisfy the compiler. Do not call.")
}
}
93 changes: 65 additions & 28 deletions Sources/Private/CoreAnimation/Animations/VisibilityAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,71 @@ extension CALayer {
inFrame: AnimationFrameTime,
outFrame: AnimationFrameTime,
context: LayerAnimationContext)
throws
{
let animation = CAKeyframeAnimation(keyPath: #keyPath(isHidden))
animation.calculationMode = .discrete

animation.values = [
true, // hidden, before `inFrame`
false, // visible
true, // hidden, after `outFrame`
]

// TODO: Would need to support complex time remapping

// From the documentation of `keyTimes`:
// - If the calculationMode is set to discrete, the first value in the array
// must be 0.0 and the last value must be 1.0. The array should have one more
// entry than appears in the values array. For example, if there are two values,
// there should be three key times.
animation.keyTimes = [
NSNumber(value: 0.0),
NSNumber(value: max(Double(context.progressTime(for: inFrame)), 0)),
// Anything visible during the last frame should stay visible until the absolute end of the animation.
// - This matches the behavior of the main thread rendering engine.
context.simpleTimeRemapping(outFrame) == context.animation.endFrame
? NSNumber(value: Double(1.0))
: NSNumber(value: min(Double(context.progressTime(for: outFrame)), 1)),
NSNumber(value: 1.0),
]

add(animation, timedWith: context)
/// If this layer uses `complexTimeRemapping`, use the `addAnimation` codepath
/// which uses `Keyframes.manuallyInterpolatedWithTimeRemapping`.
if context.mustUseComplexTimeRemapping {
let isHiddenKeyframes = KeyframeGroup(keyframes: [
Keyframe(value: true, time: 0, isHold: true), // hidden, before `inFrame`
Keyframe(value: false, time: inFrame, isHold: true), // visible
Keyframe(value: true, time: outFrame, isHold: true), // hidden, after `outFrame`
])

try addAnimation(
for: .isHidden,
keyframes: isHiddenKeyframes.map { Hold(value: $0) },
value: { $0.value },
context: context)
}

/// Otherwise continue using the previous codepath that doesn't support complex time remapping.
/// - TODO: We could remove this codepath in favor of always using the simpler codepath above,
/// but would have to solve https://github.com/airbnb/lottie-ios/pull/2254 for that codepath.
else {
let animation = CAKeyframeAnimation(keyPath: #keyPath(isHidden))
animation.calculationMode = .discrete

animation.values = [
true, // hidden, before `inFrame`
false, // visible
true, // hidden, after `outFrame`
]

// From the documentation of `keyTimes`:
// - If the calculationMode is set to discrete, the first value in the array
// must be 0.0 and the last value must be 1.0. The array should have one more
// entry than appears in the values array. For example, if there are two values,
// there should be three key times.
animation.keyTimes = [
NSNumber(value: 0.0),
NSNumber(value: max(Double(try context.progressTime(for: inFrame)), 0)),
// Anything visible during the last frame should stay visible until the absolute end of the animation.
// - This matches the behavior of the main thread rendering engine.
context.simpleTimeRemapping(outFrame) == context.animation.endFrame
? NSNumber(value: Double(1.0))
: NSNumber(value: min(Double(try context.progressTime(for: outFrame)), 1)),
NSNumber(value: 1.0),
]

add(animation, timedWith: context)
}
}
}

// MARK: - Hold

/// An `Interpolatable` container that animates using "hold" keyframes.
/// The keyframes do not animate, and instead always display the value from the most recent keyframe.
/// This is necessary when passing non-interpolatable values to a method that requires an `Interpolatable` conformance.
struct Hold<T>: Interpolatable {
let value: T

func interpolate(to: Hold<T>, amount: CGFloat) -> Hold<T> {
if amount < 1 {
return self
} else {
return to
}
}
}
33 changes: 33 additions & 0 deletions Sources/Private/CoreAnimation/CoreAnimationLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ final class CoreAnimationLayer: BaseAnimationLayer {
try setupLayerHierarchy(
for: animation.layers,
context: layerContext)

try validateReasonableNumberOfTimeRemappingLayers()
}

/// Immediately builds and begins playing `CAAnimation`s for each sublayer
Expand Down Expand Up @@ -531,6 +533,18 @@ extension CoreAnimationLayer: RootAnimationLayer {
}
}

/// Time remapping in the Core Animation rendering engine requires manually interpolating
/// every frame of every animation. For very large animations with a huge number of layers,
/// this can be prohibitively expensive.
func validateReasonableNumberOfTimeRemappingLayers() throws {
try layerContext.compatibilityAssert(
numberOfLayersWithTimeRemapping < 500,
"""
This animation has a very large number of layers with time remapping (\(numberOfLayersWithTimeRemapping)),
so will perform poorly with the Core Animation rendering engine.
""")
}

}

// MARK: - CALayer + allSublayers
Expand All @@ -548,4 +562,23 @@ extension CALayer {

return allSublayers
}

/// The number of layers in this layer hierarchy that have a time remapping applied
@nonobjc
var numberOfLayersWithTimeRemapping: Int {
var numberOfSublayersWithTimeRemapping = 0

for sublayer in sublayers ?? [] {
if
let preCompLayer = sublayer as? PreCompLayer,
preCompLayer.preCompLayer.timeRemapping != nil
{
numberOfSublayersWithTimeRemapping += preCompLayer.allSublayers.count
} else {
numberOfSublayersWithTimeRemapping += sublayer.numberOfLayersWithTimeRemapping
}
}

return numberOfSublayersWithTimeRemapping
}
}
Loading

0 comments on commit aa6822b

Please sign in to comment.