Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Lottie.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +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+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 @@ -1186,6 +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+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 @@ -2145,6 +2150,7 @@
2E9C95AA2822F43100677516 /* CALayer+fillBounds.swift */,
2E9C95AB2822F43100677516 /* Keyframes+combined.swift */,
2E9C95AC2822F43100677516 /* KeyframeGroup+exactlyOneKeyframe.swift */,
089E5D9E2B4CCD3F00F4F836 /* Keyframes+timeRemap.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -2942,6 +2948,7 @@
080DEFB12A9570FE00BE2D96 /* WillDisplayProviding.swift in Sources */,
080DEFE82A95711E00BE2D96 /* ImageLayer.swift in Sources */,
080DF0672A95717600BE2D96 /* AnimatorNodeDebugging.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 @@ -3004,6 +3011,7 @@
0887346F28F0CBDE00458627 /* LottieAnimation.swift in Sources */,
08C002002A46150D00AB54BA /* Data+CompressionDeprecated.swift in Sources */,
0820D5B82A8BF159007D705C /* DropShadowStyle.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 @@ -3318,6 +3326,7 @@
2E9C966D2822F43100677516 /* LayerImageProvider.swift in Sources */,
2EAF5ABD27A0798700E00531 /* FilepathImageProvider.swift in Sources */,
2EAF5AEA27A0798700E00531 /* AnimationTextProvider.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 @@ -3606,6 +3615,7 @@
0887347128F0CBDE00458627 /* LottieAnimation.swift in Sources */,
2E9C97432822F43100677516 /* TestHelpers.swift in Sources */,
0820D5BA2A8BF159007D705C /* DropShadowStyle.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
36 changes: 23 additions & 13 deletions Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extension CALayer {
/// Constructs a `CAKeyframeAnimation` that reflects the given keyframes,
/// and adds it to this `CALayer`.
@nonobjc
func addAnimation<KeyframeValue, ValueRepresentation>(
func addAnimation<KeyframeValue: AnyInterpolatable, ValueRepresentation>(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keyframes.manuallyInterpolatedWithTimeRemapping requires the keyframe values to be Interpolatable, which adds this additional requirement here. This PR adds an Interpolatable conformance to any existing type that is used with this method but didn't already have a conformance.

for property: LayerProperty<ValueRepresentation>,
keyframes: KeyframeGroup<KeyframeValue>,
value keyframeValueMapping: (KeyframeValue) throws -> ValueRepresentation,
Expand Down Expand Up @@ -39,7 +39,7 @@ extension CALayer {
/// - If the value can be applied directly to the CALayer using KVC,
/// then no `CAAnimation` will be created and the value will be applied directly.
@nonobjc
private func defaultAnimation<KeyframeValue, ValueRepresentation>(
private func defaultAnimation<KeyframeValue: AnyInterpolatable, ValueRepresentation>(
for property: LayerProperty<ValueRepresentation>,
keyframes keyframeGroup: KeyframeGroup<KeyframeValue>,
value keyframeValueMapping: (KeyframeValue) throws -> ValueRepresentation,
Expand All @@ -61,16 +61,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 @@ -179,8 +189,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 @@ -190,13 +200,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 @@ -206,8 +216,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 @@ -241,8 +251,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 @@ -53,7 +53,7 @@ extension CGPath {
// MARK: - BezierPathKeyframe

/// Data that represents how to render a bezier path at a specific point in time
struct BezierPathKeyframe {
struct BezierPathKeyframe: Interpolatable {
let path: BezierPath
let cornerRadius: LottieVector1D?

Expand All @@ -77,4 +77,10 @@ struct BezierPathKeyframe {
path, cornerRadius,
makeCombinedResult: BezierPathKeyframe.init)
}

func interpolate(to: BezierPathKeyframe, amount: CGFloat) -> BezierPathKeyframe {
BezierPathKeyframe(
path: path.interpolate(to: to.path, amount: amount),
cornerRadius: cornerRadius.interpolate(to: to.cornerRadius, amount: amount))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ extension CAShapeLayer {

extension Ellipse {
/// Data that represents how to render an ellipse at a specific point in time
struct Keyframe {
struct Keyframe: Interpolatable {
let size: LottieVector3D
let position: LottieVector3D

func interpolate(to: Ellipse.Keyframe, amount: CGFloat) -> Ellipse.Keyframe {
Keyframe(
size: size.interpolate(to: to.size, amount: amount),
position: position.interpolate(to: to.position, amount: amount))
}
}

/// Creates a single array of animatable keyframes from the separate arrays of keyframes in this Ellipse
Expand Down
17 changes: 15 additions & 2 deletions Sources/Private/CoreAnimation/Animations/GradientAnimations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ extension GradientRenderLayer {

let combinedKeyframes = Keyframes.combined(
gradient.startPoint, gradient.endPoint,
makeCombinedResult: { absoluteStartPoint, absoluteEndPoint -> (startPoint: CGPoint, endPoint: CGPoint) in
makeCombinedResult: { absoluteStartPoint, absoluteEndPoint -> RadialGradientKeyframes in
// Convert the absolute start / end points to the relative structure used by Core Animation
let relativeStartPoint = percentBasedPointInBounds(from: absoluteStartPoint.pointValue)
let radius = absoluteStartPoint.pointValue.distanceTo(absoluteEndPoint.pointValue)
Expand All @@ -123,7 +123,7 @@ extension GradientRenderLayer {
x: absoluteStartPoint.x + radius,
y: absoluteStartPoint.y + radius))

return (startPoint: relativeStartPoint, endPoint: relativeEndPoint)
return RadialGradientKeyframes(startPoint: relativeStartPoint, endPoint: relativeEndPoint)
})

try addAnimation(
Expand All @@ -140,6 +140,19 @@ extension GradientRenderLayer {
}
}

// MARK: - RadialGradientKeyframes

private struct RadialGradientKeyframes: Interpolatable {
let startPoint: CGPoint
let endPoint: CGPoint

func interpolate(to: RadialGradientKeyframes, amount: CGFloat) -> RadialGradientKeyframes {
RadialGradientKeyframes(
startPoint: startPoint.interpolate(to: to.startPoint, amount: amount),
endPoint: endPoint.interpolate(to: to.endPoint, amount: amount))
}
}

// MARK: - GradientContentType

/// Each type of gradient that can be constructed from a `GradientShapeItem`
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,17 @@ extension CAShapeLayer {

extension Rectangle {
/// Data that represents how to render a rectangle at a specific point in time
struct Keyframe {
struct Keyframe: Interpolatable {
let size: LottieVector3D
let position: LottieVector3D
let cornerRadius: LottieVector1D

func interpolate(to: Rectangle.Keyframe, amount: CGFloat) -> Rectangle.Keyframe {
Rectangle.Keyframe(
size: size.interpolate(to: to.size, amount: amount),
position: position.interpolate(to: to.position, amount: amount),
cornerRadius: cornerRadius.interpolate(to: to.cornerRadius, amount: amount))
}
}

/// Creates a single array of animatable keyframes from the separate arrays of keyframes in this Rectangle
Expand Down
13 changes: 12 additions & 1 deletion Sources/Private/CoreAnimation/Animations/StarAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,25 @@ extension CAShapeLayer {

extension Star {
/// Data that represents how to render a star at a specific point in time
struct Keyframe {
struct Keyframe: Interpolatable {
let position: LottieVector3D
let outerRadius: LottieVector1D
let innerRadius: LottieVector1D
let outerRoundness: LottieVector1D
let innerRoundness: LottieVector1D
let points: LottieVector1D
let rotation: LottieVector1D

func interpolate(to: Star.Keyframe, amount: CGFloat) -> Star.Keyframe {
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))
}
}

/// Creates a single array of animatable keyframes from the separate arrays of keyframes in this star/polygon
Expand Down
22 changes: 15 additions & 7 deletions Sources/Private/CoreAnimation/Animations/TransformAnimations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,16 @@ extension CALayer {
context: LayerAnimationContext)
throws
{
// Core Animation doesn't animate skew changes properly. If the skew value
// changes over the course of the animation then we have to manually
// compute the `CATransform3D` for each frame individually.
let requiresManualInterpolation = transformModel.hasSkewAnimation
let requiresManualInterpolation =
// Core Animation doesn't animate skew changes properly. If the skew value
// changes over the course of the animation then we have to manually
// compute the `CATransform3D` for each frame individually.
transformModel.hasSkewAnimation
// `addAnimation` requires that we use an `Interpolatable` value, but we can't interpolate a `CATransform3D`.
// Since this is only necessary when using `complexTimeRemapping`, we can avoid this by manually interpolating
// when `context.mustUseComplexTimeRemapping` is true and just returning a `Hold` container.
// Since our keyframes are already manually interpolated, they won't need to be interpolated again.
|| context.mustUseComplexTimeRemapping

let combinedTransformKeyframes = Keyframes.combined(
transformModel.anchorPoint,
Expand All @@ -272,7 +278,7 @@ extension CALayer {
requiresManualInterpolation: requiresManualInterpolation,
makeCombinedResult: {
anchor, position, positionX, positionY, scale, rotationX, rotationY, rotationZ, skew, skewAxis
-> CATransform3D in
-> Hold<CATransform3D> in

let transformPosition: CGPoint
if transformModel._positionX != nil, transformModel._positionY != nil {
Expand All @@ -281,7 +287,7 @@ extension CALayer {
transformPosition = position.pointValue
}

return CATransform3D.makeTransform(
let transform = CATransform3D.makeTransform(
anchor: anchor.pointValue,
position: transformPosition,
scale: scale.sizeValue,
Expand All @@ -290,12 +296,14 @@ extension CALayer {
rotationZ: rotationZ.cgFloatValue,
skew: skew.cgFloatValue,
skewAxis: skewAxis.cgFloatValue)

return Hold(value: transform)
})

try addAnimation(
for: .transform,
keyframes: combinedTransformKeyframes,
value: { $0 },
value: { $0.value },
context: context)
}

Expand Down
Loading