Skip to content

Commit

Permalink
Log a warning when playing animation that uses unsupported After Effe…
Browse files Browse the repository at this point in the history
…cts expressions (#2006)
  • Loading branch information
calda authored Mar 27, 2023
1 parent 6952351 commit 9c2d64e
Show file tree
Hide file tree
Showing 12 changed files with 77 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ extension CALayer {
@nonobjc
func addAnimation<KeyframeValue, ValueRepresentation>(
for property: LayerProperty<ValueRepresentation>,
keyframes: ContiguousArray<Keyframe<KeyframeValue>>,
keyframes: KeyframeGroup<KeyframeValue>,
value keyframeValueMapping: (KeyframeValue) throws -> ValueRepresentation,
context: LayerAnimationContext)
throws
Expand Down Expand Up @@ -41,13 +41,27 @@ extension CALayer {
@nonobjc
private func defaultAnimation<KeyframeValue, ValueRepresentation>(
for property: LayerProperty<ValueRepresentation>,
keyframes: ContiguousArray<Keyframe<KeyframeValue>>,
keyframes keyframeGroup: KeyframeGroup<KeyframeValue>,
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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ 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 }
},
context: context)

try addAnimation(
for: .locations,
keyframes: gradient.colors.keyframes,
keyframes: gradient.colors,
value: { colorComponents in
gradient.colorConfiguration(from: colorComponents, type: type).map { $0.location }
},
Expand Down Expand Up @@ -94,15 +94,15 @@ extension GradientRenderLayer {

try addAnimation(
for: .startPoint,
keyframes: gradient.startPoint.keyframes,
keyframes: gradient.startPoint,
value: { absoluteStartPoint in
percentBasedPointInBounds(from: absoluteStartPoint.pointValue)
},
context: context)

try addAnimation(
for: .endPoint,
keyframes: gradient.endPoint.keyframes,
keyframes: gradient.endPoint,
value: { absoluteEndPoint in
percentBasedPointInBounds(from: absoluteEndPoint.pointValue)
},
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions Sources/Private/CoreAnimation/Animations/ShapeAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ extension CAShapeLayer {

try addAnimation(
for: .fillColor,
keyframes: fill.color.keyframes,
keyframes: fill.color,
value: \.cgColorValue,
context: context)

Expand All @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Sources/Private/CoreAnimation/Animations/StarAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -79,7 +79,7 @@ extension CAShapeLayer {

try addAnimation(
for: .lineDashPhase,
keyframes: dashPhase,
keyframes: KeyframeGroup(keyframes: dashPhase),
value: \.cgFloatValue,
context: context)
}
Expand Down
22 changes: 11 additions & 11 deletions Sources/Private/CoreAnimation/Animations/TransformAnimations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -263,23 +263,23 @@ extension CALayer {

try addAnimation(
for: .rotationX,
keyframes: transformModel.rotationX.keyframes,
keyframes: transformModel.rotationX,
value: { rotationDegrees in
rotationDegrees.cgFloatValue * .pi / 180
},
context: context)

try addAnimation(
for: .rotationY,
keyframes: transformModel.rotationY.keyframes,
keyframes: transformModel.rotationY,
value: { rotationDegrees in
rotationDegrees.cgFloatValue * .pi / 180
},
context: context)

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
Expand Down Expand Up @@ -321,7 +321,7 @@ extension CALayer {

try addAnimation(
for: .transform,
keyframes: combinedTransformKeyframes.keyframes,
keyframes: combinedTransformKeyframes,
value: { $0 },
context: context)
}
Expand Down
39 changes: 31 additions & 8 deletions Sources/Private/Model/Keyframes/KeyframeGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,49 @@ final class KeyframeGroup<T> {

// MARK: Lifecycle

init(keyframes: ContiguousArray<Keyframe<T>>) {
init(
keyframes: ContiguousArray<Keyframe<T>>,
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<Keyframe<T>>

/// 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

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<T>(keyframeData)])
self.init(
keyframes: [Keyframe<T>(keyframeData)],
unsupportedAfterEffectsExpression: unsupportedAfterEffectsExpression)
} else {
// Decode and array of keyframes.
//
Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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<Keyframe<T>>()
let unsupportedAfterEffectsExpression = dictionary[KeyframeWrapperKey.unsupportedAfterEffectsExpression.rawValue] as? String
if
let rawValue = dictionary[KeyframeWrapperKey.keyframeData.rawValue],
let value = try? T(value: rawValue)
Expand Down Expand Up @@ -162,7 +181,9 @@ extension KeyframeGroup: DictionaryInitializable where T: AnyInitializable {
}
}

self.init(keyframes: keyframes)
self.init(
keyframes: keyframes,
unsupportedAfterEffectsExpression: unsupportedAfterEffectsExpression)
}
}

Expand Down Expand Up @@ -199,9 +220,11 @@ extension Keyframe {
extension KeyframeGroup {
/// Maps the values of each individual keyframe in this group
func map<NewValue>(_ transformation: (T) throws -> NewValue) rethrows -> KeyframeGroup<NewValue> {
KeyframeGroup<NewValue>(keyframes: ContiguousArray(try keyframes.map { keyframe in
keyframe.withValue(try transformation(keyframe.value))
}))
KeyframeGroup<NewValue>(
keyframes: ContiguousArray(try keyframes.map { keyframe in
keyframe.withValue(try transformation(keyframe.value))
}),
unsupportedAfterEffectsExpression: unsupportedAfterEffectsExpression)
}
}

Expand Down

0 comments on commit 9c2d64e

Please sign in to comment.