From 9c2d64e4e3232fb6a3bca6bae1f285e986ded07e Mon Sep 17 00:00:00 2001 From: Cal Stephens Date: Mon, 27 Mar 2023 08:30:57 -0700 Subject: [PATCH] Log a warning when playing animation that uses unsupported After Effects expressions (#2006) --- .../Animations/CALayer+addAnimation.swift | 18 ++++++++- .../Animations/CombinedShapeAnimation.swift | 2 +- .../Animations/CustomPathAnimation.swift | 2 +- .../Animations/EllipseAnimation.swift | 2 +- .../Animations/GradientAnimations.swift | 12 +++--- .../Animations/OpacityAnimation.swift | 2 +- .../Animations/RectangleAnimation.swift | 2 +- .../Animations/ShapeAnimation.swift | 6 +-- .../Animations/StarAnimation.swift | 4 +- .../Animations/StrokeAnimation.swift | 6 +-- .../Animations/TransformAnimations.swift | 22 +++++------ .../Model/Keyframes/KeyframeGroup.swift | 39 +++++++++++++++---- 12 files changed, 77 insertions(+), 40 deletions(-) diff --git a/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift b/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift index 4916e3ad45..0c4dd771f8 100644 --- a/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift +++ b/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift @@ -12,7 +12,7 @@ extension CALayer { @nonobjc func addAnimation( for property: LayerProperty, - keyframes: ContiguousArray>, + keyframes: KeyframeGroup, value keyframeValueMapping: (KeyframeValue) throws -> ValueRepresentation, context: LayerAnimationContext) throws @@ -41,13 +41,27 @@ extension CALayer { @nonobjc private func defaultAnimation( for property: LayerProperty, - keyframes: ContiguousArray>, + keyframes keyframeGroup: KeyframeGroup, value keyframeValueMapping: (KeyframeValue) throws -> ValueRepresentation, context: LayerAnimationContext) throws -> CAAnimation? { + let keyframes = keyframeGroup.keyframes guard !keyframes.isEmpty else { return nil } + // Check if this set of keyframes uses After Effects expressions, which aren't supported. + if let unsupportedAfterEffectsExpression = keyframeGroup.unsupportedAfterEffectsExpression { + context.logger.info(""" + `\(property.caLayerKeypath)` animation for "\(context.currentKeypath.fullPath)" \ + includes an After Effects expression (https://helpx.adobe.com/after-effects/using/expression-language.html), \ + which is not supported by lottie-ios (expressions are only supported by lottie-web). \ + This animation may not play correctly. + + \(unsupportedAfterEffectsExpression.replacingOccurrences(of: "\n", with: "\n ")) + + """) + } + // 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`. diff --git a/Sources/Private/CoreAnimation/Animations/CombinedShapeAnimation.swift b/Sources/Private/CoreAnimation/Animations/CombinedShapeAnimation.swift index 12a2de2106..e5b888f45e 100644 --- a/Sources/Private/CoreAnimation/Animations/CombinedShapeAnimation.swift +++ b/Sources/Private/CoreAnimation/Animations/CombinedShapeAnimation.swift @@ -14,7 +14,7 @@ extension CAShapeLayer { { try addAnimation( for: .path, - keyframes: combinedShapes.shapes.keyframes, + keyframes: combinedShapes.shapes, value: { paths in let combinedPath = CGMutablePath() for path in paths { diff --git a/Sources/Private/CoreAnimation/Animations/CustomPathAnimation.swift b/Sources/Private/CoreAnimation/Animations/CustomPathAnimation.swift index 313e98d206..db56da13d5 100644 --- a/Sources/Private/CoreAnimation/Animations/CustomPathAnimation.swift +++ b/Sources/Private/CoreAnimation/Animations/CustomPathAnimation.swift @@ -20,7 +20,7 @@ extension CAShapeLayer { try addAnimation( for: .path, - keyframes: combinedKeyframes.keyframes, + keyframes: combinedKeyframes, value: { pathKeyframe in var path = pathKeyframe.path if let cornerRadius = pathKeyframe.cornerRadius { diff --git a/Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift b/Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift index 7420ac36fe..a514cbb6f0 100644 --- a/Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift +++ b/Sources/Private/CoreAnimation/Animations/EllipseAnimation.swift @@ -14,7 +14,7 @@ extension CAShapeLayer { { try addAnimation( for: .path, - keyframes: ellipse.combinedKeyframes().keyframes, + keyframes: ellipse.combinedKeyframes(), value: { keyframe in BezierPath.ellipse( size: keyframe.size.sizeValue, diff --git a/Sources/Private/CoreAnimation/Animations/GradientAnimations.swift b/Sources/Private/CoreAnimation/Animations/GradientAnimations.swift index 3cf29b8a78..75b43a4099 100644 --- a/Sources/Private/CoreAnimation/Animations/GradientAnimations.swift +++ b/Sources/Private/CoreAnimation/Animations/GradientAnimations.swift @@ -57,7 +57,7 @@ extension GradientRenderLayer { try addAnimation( for: .colors, - keyframes: gradient.colors.keyframes, + keyframes: gradient.colors, value: { colorComponents in gradient.colorConfiguration(from: colorComponents, type: type).map { $0.color } }, @@ -65,7 +65,7 @@ extension GradientRenderLayer { try addAnimation( for: .locations, - keyframes: gradient.colors.keyframes, + keyframes: gradient.colors, value: { colorComponents in gradient.colorConfiguration(from: colorComponents, type: type).map { $0.location } }, @@ -94,7 +94,7 @@ extension GradientRenderLayer { try addAnimation( for: .startPoint, - keyframes: gradient.startPoint.keyframes, + keyframes: gradient.startPoint, value: { absoluteStartPoint in percentBasedPointInBounds(from: absoluteStartPoint.pointValue) }, @@ -102,7 +102,7 @@ extension GradientRenderLayer { try addAnimation( for: .endPoint, - keyframes: gradient.endPoint.keyframes, + keyframes: gradient.endPoint, value: { absoluteEndPoint in percentBasedPointInBounds(from: absoluteEndPoint.pointValue) }, @@ -128,13 +128,13 @@ extension GradientRenderLayer { try addAnimation( for: .startPoint, - keyframes: combinedKeyframes.keyframes, + keyframes: combinedKeyframes, value: \.startPoint, context: context) try addAnimation( for: .endPoint, - keyframes: combinedKeyframes.keyframes, + keyframes: combinedKeyframes, value: \.endPoint, context: context) } diff --git a/Sources/Private/CoreAnimation/Animations/OpacityAnimation.swift b/Sources/Private/CoreAnimation/Animations/OpacityAnimation.swift index 023e509169..b7bf2200eb 100644 --- a/Sources/Private/CoreAnimation/Animations/OpacityAnimation.swift +++ b/Sources/Private/CoreAnimation/Animations/OpacityAnimation.swift @@ -40,7 +40,7 @@ extension CALayer { func addOpacityAnimation(for opacity: OpacityAnimationModel, context: LayerAnimationContext) throws { try addAnimation( for: .opacity, - keyframes: opacity.opacity.keyframes, + keyframes: opacity.opacity, value: { // Lottie animation files express opacity as a numerical percentage value // (e.g. 0%, 50%, 100%) so we divide by 100 to get the decimal values diff --git a/Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift b/Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift index df954fd258..41147b414a 100644 --- a/Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift +++ b/Sources/Private/CoreAnimation/Animations/RectangleAnimation.swift @@ -15,7 +15,7 @@ extension CAShapeLayer { { try addAnimation( for: .path, - keyframes: try rectangle.combinedKeyframes(roundedCorners: roundedCorners).keyframes, + keyframes: try rectangle.combinedKeyframes(roundedCorners: roundedCorners), value: { keyframe in BezierPath.rectangle( position: keyframe.position.pointValue, diff --git a/Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift b/Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift index 7c82741639..a83adf385a 100644 --- a/Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift +++ b/Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift @@ -57,7 +57,7 @@ extension CAShapeLayer { try addAnimation( for: .fillColor, - keyframes: fill.color.keyframes, + keyframes: fill.color, value: \.cgColorValue, context: context) @@ -71,7 +71,7 @@ extension CAShapeLayer { try addAnimation( for: .strokeStart, - keyframes: strokeStartKeyframes.keyframes, + keyframes: strokeStartKeyframes, value: { strokeStart in // Lottie animation files express stoke trims as a numerical percentage value // (e.g. 25%, 50%, 100%) so we divide by 100 to get the decimal values @@ -81,7 +81,7 @@ extension CAShapeLayer { try addAnimation( for: .strokeEnd, - keyframes: strokeEndKeyframes.keyframes, + keyframes: strokeEndKeyframes, value: { strokeEnd in // Lottie animation files express stoke trims as a numerical percentage value // (e.g. 25%, 50%, 100%) so we divide by 100 to get the decimal values diff --git a/Sources/Private/CoreAnimation/Animations/StarAnimation.swift b/Sources/Private/CoreAnimation/Animations/StarAnimation.swift index 00fdfcadc1..44a47cdde6 100644 --- a/Sources/Private/CoreAnimation/Animations/StarAnimation.swift +++ b/Sources/Private/CoreAnimation/Animations/StarAnimation.swift @@ -36,7 +36,7 @@ extension CAShapeLayer { { try addAnimation( for: .path, - keyframes: try star.combinedKeyframes().keyframes, + keyframes: try star.combinedKeyframes(), value: { keyframe in BezierPath.star( position: keyframe.position.pointValue, @@ -62,7 +62,7 @@ extension CAShapeLayer { { try addAnimation( for: .path, - keyframes: try star.combinedKeyframes().keyframes, + keyframes: try star.combinedKeyframes(), value: { keyframe in BezierPath.polygon( position: keyframe.position.pointValue, diff --git a/Sources/Private/CoreAnimation/Animations/StrokeAnimation.swift b/Sources/Private/CoreAnimation/Animations/StrokeAnimation.swift index d9afaf14ed..f6adfdd7cb 100644 --- a/Sources/Private/CoreAnimation/Animations/StrokeAnimation.swift +++ b/Sources/Private/CoreAnimation/Animations/StrokeAnimation.swift @@ -54,14 +54,14 @@ extension CAShapeLayer { if let strokeColor = stroke.strokeColor { try addAnimation( for: .strokeColor, - keyframes: strokeColor.keyframes, + keyframes: strokeColor, value: \.cgColorValue, context: context) } try addAnimation( for: .lineWidth, - keyframes: stroke.width.keyframes, + keyframes: stroke.width, value: \.cgFloatValue, context: context) @@ -79,7 +79,7 @@ extension CAShapeLayer { try addAnimation( for: .lineDashPhase, - keyframes: dashPhase, + keyframes: KeyframeGroup(keyframes: dashPhase), value: \.cgFloatValue, context: context) } diff --git a/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift b/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift index 42ca753b7e..26c52e02a7 100644 --- a/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift +++ b/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift @@ -93,15 +93,15 @@ extension CALayer { context: LayerAnimationContext) throws { - if let positionKeyframes = transformModel._position?.keyframes { + if let positionKeyframes = transformModel._position { try addAnimation( for: .position, keyframes: positionKeyframes, value: \.pointValue, context: context) } else if - let xKeyframes = transformModel._positionX?.keyframes, - let yKeyframes = transformModel._positionY?.keyframes + let xKeyframes = transformModel._positionX, + let yKeyframes = transformModel._positionY { try addAnimation( for: .positionX, @@ -129,7 +129,7 @@ extension CALayer { { try addAnimation( for: .anchorPoint, - keyframes: transformModel.anchorPoint.keyframes, + keyframes: transformModel.anchorPoint, value: { absoluteAnchorPoint in guard bounds.width > 0, bounds.height > 0 else { context.logger.assertionFailure("Size must be non-zero before an animation can be played") @@ -154,7 +154,7 @@ extension CALayer { { try addAnimation( for: .scaleX, - keyframes: transformModel.scale.keyframes, + keyframes: transformModel.scale, value: { scale in // Lottie animation files express scale as a numerical percentage value // (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values @@ -207,7 +207,7 @@ extension CALayer { try addAnimation( for: .rotationY, - keyframes: transformModel.scale.keyframes, + keyframes: transformModel.scale, value: { scale in if scale.x < 0 { return .pi @@ -220,7 +220,7 @@ extension CALayer { try addAnimation( for: .scaleY, - keyframes: transformModel.scale.keyframes, + keyframes: transformModel.scale, value: { scale in // Lottie animation files express scale as a numerical percentage value // (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values @@ -263,7 +263,7 @@ extension CALayer { try addAnimation( for: .rotationX, - keyframes: transformModel.rotationX.keyframes, + keyframes: transformModel.rotationX, value: { rotationDegrees in rotationDegrees.cgFloatValue * .pi / 180 }, @@ -271,7 +271,7 @@ extension CALayer { try addAnimation( for: .rotationY, - keyframes: transformModel.rotationY.keyframes, + keyframes: transformModel.rotationY, value: { rotationDegrees in rotationDegrees.cgFloatValue * .pi / 180 }, @@ -279,7 +279,7 @@ extension CALayer { try addAnimation( for: .rotationZ, - keyframes: transformModel.rotationZ.keyframes, + keyframes: transformModel.rotationZ, value: { rotationDegrees in // Lottie animation files express rotation in degrees // (e.g. 90º, 180º, 360º) so we covert to radians to get the @@ -321,7 +321,7 @@ extension CALayer { try addAnimation( for: .transform, - keyframes: combinedTransformKeyframes.keyframes, + keyframes: combinedTransformKeyframes, value: { $0 }, context: context) } diff --git a/Sources/Private/Model/Keyframes/KeyframeGroup.swift b/Sources/Private/Model/Keyframes/KeyframeGroup.swift index d61c6c195b..8fca5380f6 100644 --- a/Sources/Private/Model/Keyframes/KeyframeGroup.swift +++ b/Sources/Private/Model/Keyframes/KeyframeGroup.swift @@ -19,22 +19,35 @@ final class KeyframeGroup { // MARK: Lifecycle - init(keyframes: ContiguousArray>) { + init( + keyframes: ContiguousArray>, + unsupportedAfterEffectsExpression: String? = nil) + { self.keyframes = keyframes + self.unsupportedAfterEffectsExpression = unsupportedAfterEffectsExpression } - init(_ value: T) { + init( + _ value: T, + unsupportedAfterEffectsExpression: String? = nil) + { keyframes = [Keyframe(value)] + self.unsupportedAfterEffectsExpression = unsupportedAfterEffectsExpression } // MARK: Internal enum KeyframeWrapperKey: String, CodingKey { case keyframeData = "k" + case unsupportedAfterEffectsExpression = "x" } let keyframes: ContiguousArray> + /// lottie-ios doesn't support After Effects expressions, but we parse them so we can log diagnostics. + /// More info: https://helpx.adobe.com/after-effects/using/expression-basics.html + let unsupportedAfterEffectsExpression: String? + } // MARK: Decodable @@ -42,10 +55,13 @@ final class KeyframeGroup { extension KeyframeGroup: Decodable where T: Decodable { convenience init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: KeyframeWrapperKey.self) + let unsupportedAfterEffectsExpression = try? container.decode(String.self, forKey: .unsupportedAfterEffectsExpression) if let keyframeData: T = try? container.decode(T.self, forKey: .keyframeData) { /// Try to decode raw value; No keyframe data. - self.init(keyframes: [Keyframe(keyframeData)]) + self.init( + keyframes: [Keyframe(keyframeData)], + unsupportedAfterEffectsExpression: unsupportedAfterEffectsExpression) } else { // Decode and array of keyframes. // @@ -89,7 +105,9 @@ extension KeyframeGroup: Decodable where T: Decodable { spatialOutTangent: keyframeData.spatialOutTangent)) previousKeyframeData = keyframeData } - self.init(keyframes: keyframes) + self.init( + keyframes: keyframes, + unsupportedAfterEffectsExpression: unsupportedAfterEffectsExpression) } } } @@ -129,6 +147,7 @@ extension KeyframeGroup: Encodable where T: Encodable { extension KeyframeGroup: DictionaryInitializable where T: AnyInitializable { convenience init(dictionary: [String: Any]) throws { var keyframes = ContiguousArray>() + let unsupportedAfterEffectsExpression = dictionary[KeyframeWrapperKey.unsupportedAfterEffectsExpression.rawValue] as? String if let rawValue = dictionary[KeyframeWrapperKey.keyframeData.rawValue], let value = try? T(value: rawValue) @@ -162,7 +181,9 @@ extension KeyframeGroup: DictionaryInitializable where T: AnyInitializable { } } - self.init(keyframes: keyframes) + self.init( + keyframes: keyframes, + unsupportedAfterEffectsExpression: unsupportedAfterEffectsExpression) } } @@ -199,9 +220,11 @@ extension Keyframe { extension KeyframeGroup { /// Maps the values of each individual keyframe in this group func map(_ transformation: (T) throws -> NewValue) rethrows -> KeyframeGroup { - KeyframeGroup(keyframes: ContiguousArray(try keyframes.map { keyframe in - keyframe.withValue(try transformation(keyframe.value)) - })) + KeyframeGroup( + keyframes: ContiguousArray(try keyframes.map { keyframe in + keyframe.withValue(try transformation(keyframe.value)) + }), + unsupportedAfterEffectsExpression: unsupportedAfterEffectsExpression) } }