From fadcc6372956b0ce0da260015e4fc199a2a96d05 Mon Sep 17 00:00:00 2001 From: Cal Stephens Date: Thu, 20 Jul 2023 15:13:32 -0700 Subject: [PATCH] Play "reduced motion" marker if `UIAccessibility.isReduceMotionEnabled` is true (#2110) --- Lottie.xcodeproj/project.pbxproj | 34 +++++- .../Public/Animation/LottieAnimation.swift | 19 +++ .../Animation/LottieAnimationLayer.swift | 61 +++++++++- .../Configuration/DecodingStrategy.swift | 15 +++ .../Configuration/LottieConfiguration.swift | 47 ++++++++ .../Configuration/ReducedMotionOption.swift | 114 ++++++++++++++++++ .../RenderingEngineOption.swift} | 59 +-------- 7 files changed, 289 insertions(+), 60 deletions(-) create mode 100644 Sources/Public/Configuration/DecodingStrategy.swift create mode 100644 Sources/Public/Configuration/LottieConfiguration.swift create mode 100644 Sources/Public/Configuration/ReducedMotionOption.swift rename Sources/Public/{LottieConfiguration.swift => Configuration/RenderingEngineOption.swift} (61%) diff --git a/Lottie.xcodeproj/project.pbxproj b/Lottie.xcodeproj/project.pbxproj index 1a892942c0..3da54967f2 100644 --- a/Lottie.xcodeproj/project.pbxproj +++ b/Lottie.xcodeproj/project.pbxproj @@ -19,6 +19,15 @@ 0887347B28F0CCDD00458627 /* LottieAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0887347428F0CCDD00458627 /* LottieAnimationView.swift */; }; 0887347C28F0CCDD00458627 /* LottieAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0887347428F0CCDD00458627 /* LottieAnimationView.swift */; }; 0887347D28F0CCDD00458627 /* LottieAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0887347428F0CCDD00458627 /* LottieAnimationView.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 */; }; + 08AB05592A61C5B700DE86FD /* DecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB05582A61C5B700DE86FD /* DecodingStrategy.swift */; }; + 08AB055A2A61C5B700DE86FD /* DecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB05582A61C5B700DE86FD /* DecodingStrategy.swift */; }; + 08AB055B2A61C5B700DE86FD /* DecodingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB05582A61C5B700DE86FD /* DecodingStrategy.swift */; }; + 08AB055D2A61C5CC00DE86FD /* RenderingEngineOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB055C2A61C5CC00DE86FD /* RenderingEngineOption.swift */; }; + 08AB055E2A61C5CC00DE86FD /* RenderingEngineOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB055C2A61C5CC00DE86FD /* RenderingEngineOption.swift */; }; + 08AB055F2A61C5CC00DE86FD /* RenderingEngineOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB055C2A61C5CC00DE86FD /* RenderingEngineOption.swift */; }; 08C001F32A46150D00AB54BA /* Archive+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C001E02A46150D00AB54BA /* Archive+Helpers.swift */; }; 08C001F42A46150D00AB54BA /* Archive+MemoryFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C001E12A46150D00AB54BA /* Archive+MemoryFile.swift */; }; 08C001F52A46150D00AB54BA /* Archive+BackingConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C001E22A46150D00AB54BA /* Archive+BackingConfiguration.swift */; }; @@ -828,6 +837,9 @@ 0887347228F0CCDD00458627 /* LottieAnimationHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LottieAnimationHelpers.swift; sourceTree = ""; }; 0887347328F0CCDD00458627 /* LottieAnimationViewInitializers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LottieAnimationViewInitializers.swift; sourceTree = ""; }; 0887347428F0CCDD00458627 /* LottieAnimationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LottieAnimationView.swift; sourceTree = ""; }; + 08AB05542A61C20400DE86FD /* ReducedMotionOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReducedMotionOption.swift; sourceTree = ""; }; + 08AB05582A61C5B700DE86FD /* DecodingStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingStrategy.swift; sourceTree = ""; }; + 08AB055C2A61C5CC00DE86FD /* RenderingEngineOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingEngineOption.swift; sourceTree = ""; }; 08C001E02A46150D00AB54BA /* Archive+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Archive+Helpers.swift"; sourceTree = ""; }; 08C001E12A46150D00AB54BA /* Archive+MemoryFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Archive+MemoryFile.swift"; sourceTree = ""; }; 08C001E22A46150D00AB54BA /* Archive+BackingConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Archive+BackingConfiguration.swift"; sourceTree = ""; }; @@ -1148,6 +1160,17 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 08AB05532A61C1F000DE86FD /* Configuration */ = { + isa = PBXGroup; + children = ( + 2EAF59C727A0798700E00531 /* LottieConfiguration.swift */, + 08AB05542A61C20400DE86FD /* ReducedMotionOption.swift */, + 08AB05582A61C5B700DE86FD /* DecodingStrategy.swift */, + 08AB055C2A61C5CC00DE86FD /* RenderingEngineOption.swift */, + ); + path = Configuration; + sourceTree = ""; + }; 08C001DE2A4614CF00AB54BA /* EmbeddedLibraries */ = { isa = PBXGroup; children = ( @@ -1807,8 +1830,8 @@ 2EAF59C127A0798700E00531 /* Public */ = { isa = PBXGroup; children = ( + 08AB05532A61C1F000DE86FD /* Configuration */, 2EAF59C227A0798700E00531 /* macOS */, - 2EAF59C727A0798700E00531 /* LottieConfiguration.swift */, 2EAF59C827A0798700E00531 /* Animation */, 6C4877E028FF20140005AF07 /* DotLottie */, 2EAF59CC27A0798700E00531 /* ImageProvider */, @@ -2220,6 +2243,7 @@ 08E207272A56014E002DCE17 /* SetContentProviding.swift in Sources */, 2E9C96DE2822F43100677516 /* GradientRenderLayer.swift in Sources */, D453D8B428FF9EAA00D3F49C /* DefaultAnimationCache.swift in Sources */, + 08AB055D2A61C5CC00DE86FD /* RenderingEngineOption.swift in Sources */, 2E9C966C2822F43100677516 /* LayerImageProvider.swift in Sources */, 2EAF5ABC27A0798700E00531 /* FilepathImageProvider.swift in Sources */, 2EAF5AE927A0798700E00531 /* AnimationTextProvider.swift in Sources */, @@ -2252,6 +2276,7 @@ 2E9C96F92822F43100677516 /* BaseCompositionLayer.swift in Sources */, 2EAF5A9B27A0798700E00531 /* BundleImageProvider.macOS.swift in Sources */, 2E9C969F2822F43100677516 /* TextAnimatorNode.swift in Sources */, + 08AB05552A61C20400DE86FD /* ReducedMotionOption.swift in Sources */, 2EAF5AFB27A0798700E00531 /* SizeValueProvider.swift in Sources */, 2E9C97562822F43100677516 /* MathKit.swift in Sources */, 2E9C96902822F43100677516 /* EllipseNode.swift in Sources */, @@ -2289,6 +2314,7 @@ 2E9C96B72822F43100677516 /* NodePropertyMap.swift in Sources */, 2E9C97682822F43100677516 /* VectorsExtensions.swift in Sources */, 2E9C97232822F43100677516 /* RectangleAnimation.swift in Sources */, + 08AB05592A61C5B700DE86FD /* DecodingStrategy.swift in Sources */, 2E450DAC283415D500E56D19 /* OpacityAnimation.swift in Sources */, 08E206E22A56014E002DCE17 /* ViewType.swift in Sources */, 2E9C96FC2822F43100677516 /* CALayer+setupLayerHierarchy.swift in Sources */, @@ -2633,6 +2659,7 @@ 2E9C970F2822F43100677516 /* CALayer+fillBounds.swift in Sources */, 08C002D12A46196300AB54BA /* Archive.swift in Sources */, 2E9C95FE2822F43100677516 /* SolidLayerModel.swift in Sources */, + 08AB055E2A61C5CC00DE86FD /* RenderingEngineOption.swift in Sources */, 2E9C970C2822F43100677516 /* ValueProviderStore.swift in Sources */, 6C48780028FF20140005AF07 /* DotLottieAnimation.swift in Sources */, 2E9C97272822F43100677516 /* StrokeAnimation.swift in Sources */, @@ -2702,6 +2729,7 @@ 2E9C96AC2822F43100677516 /* GradientStrokeNode.swift in Sources */, 2EAF5AC027A0798700E00531 /* AnimatedSwitch.swift in Sources */, 2EAF5AC327A0798700E00531 /* BundleImageProvider.swift in Sources */, + 08AB055A2A61C5B700DE86FD /* DecodingStrategy.swift in Sources */, 08E207492A56014E002DCE17 /* AnimatedProviding.swift in Sources */, 2E9C976C2822F43100677516 /* InterpolatableExtensions.swift in Sources */, 2E9C96EE2822F43100677516 /* ShapeItemLayer.swift in Sources */, @@ -2710,6 +2738,7 @@ 2E9C96312822F43100677516 /* TextAnimator.swift in Sources */, 2E9C96E82822F43100677516 /* ImageLayer.swift in Sources */, 2E9C972D2822F43100677516 /* StarAnimation.swift in Sources */, + 08AB05562A61C20400DE86FD /* ReducedMotionOption.swift in Sources */, 2E9C96E22822F43100677516 /* LayerModel+makeAnimationLayer.swift in Sources */, 08E207372A56014E002DCE17 /* MakeViewProviding.swift in Sources */, 2E9C96A92822F43100677516 /* FillNode.swift in Sources */, @@ -2779,6 +2808,7 @@ 08E207292A56014E002DCE17 /* SetContentProviding.swift in Sources */, 2E9C96E02822F43100677516 /* GradientRenderLayer.swift in Sources */, D453D8B328FF9EAA00D3F49C /* DefaultAnimationCache.swift in Sources */, + 08AB055F2A61C5CC00DE86FD /* RenderingEngineOption.swift in Sources */, 2E9C966E2822F43100677516 /* LayerImageProvider.swift in Sources */, 2EAF5ABE27A0798700E00531 /* FilepathImageProvider.swift in Sources */, 2EAF5AEB27A0798700E00531 /* AnimationTextProvider.swift in Sources */, @@ -2811,6 +2841,7 @@ 2EAF5A9D27A0798700E00531 /* BundleImageProvider.macOS.swift in Sources */, 2E9C96A12822F43100677516 /* TextAnimatorNode.swift in Sources */, 2EAF5AFD27A0798700E00531 /* SizeValueProvider.swift in Sources */, + 08AB05572A61C20400DE86FD /* ReducedMotionOption.swift in Sources */, 2E9C97582822F43100677516 /* MathKit.swift in Sources */, 2E9C96922822F43100677516 /* EllipseNode.swift in Sources */, 2E9C975B2822F43100677516 /* BezierPath.swift in Sources */, @@ -2848,6 +2879,7 @@ 2E9C97252822F43100677516 /* RectangleAnimation.swift in Sources */, 2E450DAE283415D500E56D19 /* OpacityAnimation.swift in Sources */, 2E9C96FE2822F43100677516 /* CALayer+setupLayerHierarchy.swift in Sources */, + 08AB055B2A61C5B700DE86FD /* DecodingStrategy.swift in Sources */, 2E9C96A72822F43100677516 /* StrokeNode.swift in Sources */, 08E206E42A56014E002DCE17 /* ViewType.swift in Sources */, 2E9C95E72822F43100677516 /* ShapeTransform.swift in Sources */, diff --git a/Sources/Public/Animation/LottieAnimation.swift b/Sources/Public/Animation/LottieAnimation.swift index 98cdef1249..9f7b82c439 100644 --- a/Sources/Public/Animation/LottieAnimation.swift +++ b/Sources/Public/Animation/LottieAnimation.swift @@ -157,4 +157,23 @@ public final class LottieAnimation: Codable, DictionaryInitializable { /// Markers let markers: [Marker]? let markerMap: [String: Marker]? + + /// The marker to use if "reduced motion" is enabled. + /// Supported marker names are case insensitive, and include: + /// - reduced motion + /// - reducedMotion + /// - reduced_motion + /// - reduced-motion + lazy private(set) var reducedMotionMarker: Marker? = { + let allowedReducedMotionMarkerNames = Set([ + "reduced motion", + "reduced_motion", + "reduced-motion", + "reducedmotion", + ]) + + return markers?.first(where: { marker in + allowedReducedMotionMarkerNames.contains(marker.name.lowercased()) + }) + }() } diff --git a/Sources/Public/Animation/LottieAnimationLayer.swift b/Sources/Public/Animation/LottieAnimationLayer.swift index c13ca8193a..406b13e8c2 100644 --- a/Sources/Public/Animation/LottieAnimationLayer.swift +++ b/Sources/Public/Animation/LottieAnimationLayer.swift @@ -104,7 +104,10 @@ public class LottieAnimationLayer: CALayer { /// /// - Parameter completion: An optional completion closure to be called when the animation completes playing. open func play(completion: LottieCompletionBlock? = nil) { - guard let animation = animation else { + guard let animation = animation else { return } + + if shouldOverrideWithReducedMotionAnimation { + playReducedMotionAnimation(completion: completion) return } @@ -129,7 +132,10 @@ public class LottieAnimationLayer: CALayer { loopMode: LottieLoopMode? = nil, completion: LottieCompletionBlock? = nil) { - guard let animation = animation else { + guard let animation = animation else { return } + + if shouldOverrideWithReducedMotionAnimation { + playReducedMotionAnimation(completion: completion) return } @@ -157,6 +163,11 @@ public class LottieAnimationLayer: CALayer { loopMode: LottieLoopMode? = nil, completion: LottieCompletionBlock? = nil) { + if shouldOverrideWithReducedMotionAnimation { + playReducedMotionAnimation(completion: completion) + return + } + removeCurrentAnimationIfNecessary() if let loopMode = loopMode { /// Set the loop mode, if one was supplied @@ -192,6 +203,11 @@ public class LottieAnimationLayer: CALayer { loopMode: LottieLoopMode? = nil, completion: LottieCompletionBlock? = nil) { + if shouldOverrideWithReducedMotionAnimation { + playReducedMotionAnimation(completion: completion) + return + } + guard let animation = animation, let markers = animation.markerMap, let to = markers[toMarker] else { return } @@ -232,6 +248,11 @@ public class LottieAnimationLayer: CALayer { loopMode: LottieLoopMode? = nil, completion: LottieCompletionBlock? = nil) { + if shouldOverrideWithReducedMotionAnimation { + playReducedMotionAnimation(completion: completion) + return + } + guard let from = animation?.markerMap?[marker] else { return } @@ -259,6 +280,11 @@ public class LottieAnimationLayer: CALayer { /// /// - Parameter markers: The list of markers to play sequentially. open func play(markers: [String]) { + if shouldOverrideWithReducedMotionAnimation { + playReducedMotionAnimation(completion: nil) + return + } + guard !markers.isEmpty else { return } let markerToPlay = markers[0] @@ -1241,6 +1267,22 @@ public class LottieAnimationLayer: CALayer { /// The `LottieBackgroundBehavior` that was specified manually by setting `self.backgroundBehavior` private var _backgroundBehavior: LottieBackgroundBehavior? + /// Whether or not the current animation should be overridden with + /// the marker matching the current "reduced motion" mode. + private var shouldOverrideWithReducedMotionAnimation: Bool { + reducedMotionMarker != nil + } + + /// The marker that corresponds to the current "reduced motion" mode. + private var reducedMotionMarker: Marker? { + switch configuration.reducedMotionOption.currentReducedMotionMode { + case .standardMotion: + return nil + case .reducedMotion: + return animation?.reducedMotionMarker + } + } + private func loadAnimation(_ dotLottieAnimation: DotLottieFile.Animation) { loopMode = dotLottieAnimation.configuration.loopMode animationSpeed = CGFloat(dotLottieAnimation.configuration.speed) @@ -1252,6 +1294,21 @@ public class LottieAnimationLayer: CALayer { animation = dotLottieAnimation.animation } + /// Plays the marker that corresponds to the current "reduced motion" mode if present. + private func playReducedMotionAnimation(completion: LottieCompletionBlock?) { + guard let reducedMotionMarker = reducedMotionMarker else { return } + + // `play(marker:)` calls the `play(fromFrame:toFrame:)` method which calls this + // `playReducedMotionAnimation` method when `shouldOverrideWithReducedMotionAnimation` + // is `true`. To prevent infinite recursion, disable the reduced motion functionality + // until the end of this function. + let currentConfiguration = configuration + configuration.reducedMotionOption = .standardMotion + defer { configuration = currentConfiguration } + + play(marker: reducedMotionMarker.name, completion: completion) + } + } // MARK: - LottieLoopMode + caAnimationConfiguration diff --git a/Sources/Public/Configuration/DecodingStrategy.swift b/Sources/Public/Configuration/DecodingStrategy.swift new file mode 100644 index 0000000000..f1cfe3c4b0 --- /dev/null +++ b/Sources/Public/Configuration/DecodingStrategy.swift @@ -0,0 +1,15 @@ +// Created by Cal Stephens on 7/14/23. +// Copyright © 2023 Airbnb Inc. All rights reserved. + +/// How animation files should be decoded +public enum DecodingStrategy: Hashable { + /// Use Codable. This is was the default strategy introduced on Lottie 3, but should be rarely + /// used as it's slower than `dictionaryBased`. Kept here for any possible compatibility issues + /// that may come up, but consider it soft-deprecated. + case legacyCodable + + /// Manually deserialize a dictionary into an Animation. + /// This should be at least 2-3x faster than using Codable and due to that + /// it's the default as of Lottie 4.x. + case dictionaryBased +} diff --git a/Sources/Public/Configuration/LottieConfiguration.swift b/Sources/Public/Configuration/LottieConfiguration.swift new file mode 100644 index 0000000000..2160fa3348 --- /dev/null +++ b/Sources/Public/Configuration/LottieConfiguration.swift @@ -0,0 +1,47 @@ +// Created by Cal Stephens on 12/13/21. +// Copyright © 2021 Airbnb Inc. All rights reserved. + +import QuartzCore + +/// Global configuration options for Lottie animations +public struct LottieConfiguration: Hashable { + + // MARK: Lifecycle + + public init( + renderingEngine: RenderingEngineOption = .automatic, + decodingStrategy: DecodingStrategy = .dictionaryBased, + colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB(), + reducedMotionOption: ReducedMotionOption = .reducedMotion) + { + self.renderingEngine = renderingEngine + self.decodingStrategy = decodingStrategy + self.colorSpace = colorSpace + self.reducedMotionOption = reducedMotionOption + } + + // MARK: Public + + /// The global configuration of Lottie, + /// which applies to all `LottieAnimationView`s by default. + public static var shared = LottieConfiguration() + + /// The rendering engine implementation to use when displaying an animation + /// - Defaults to `RenderingEngineOption.automatic`, which uses the + /// Core Animation rendering engine for supported animations, and + /// falls back to using the Main Thread rendering engine for + /// animations that use features not supported by the Core Animation engine. + public var renderingEngine: RenderingEngineOption + + /// The decoding implementation to use when parsing an animation JSON file + public var decodingStrategy: DecodingStrategy + + /// Options for controlling animation behavior in response to user / system "reduced motion" configuration. + /// - Defaults to `ReducedMotionOption.systemReducedMotionToggle`, which returns `.reducedMotion` + /// when the system `UIAccessibility.isReduceMotionEnabled` option is `true`. + public var reducedMotionOption: ReducedMotionOption + + /// The color space to be used for rendering + /// - Defaults to `CGColorSpaceCreateDeviceRGB()` + public var colorSpace: CGColorSpace +} diff --git a/Sources/Public/Configuration/ReducedMotionOption.swift b/Sources/Public/Configuration/ReducedMotionOption.swift new file mode 100644 index 0000000000..0101dc9bda --- /dev/null +++ b/Sources/Public/Configuration/ReducedMotionOption.swift @@ -0,0 +1,114 @@ +// Created by Cal Stephens on 7/14/23. +// Copyright © 2023 Airbnb Inc. All rights reserved. + +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + +// MARK: - ReducedMotionOption + +/// Options for controlling animation behavior in response to user / system "reduced motion" configuration +public enum ReducedMotionOption { + /// Always use the specific given `ReducedMotionMode` value. + case specific(ReducedMotionMode) + + /// Dynamically check the given `ReducedMotionOptionProvider` each time an animation begins. + /// - Includes a Hashable `dataID` to support `ReducedMotionOption`'s `Hashable` requirement, + /// which is required due to `LottieConfiguration`'s existing `Hashable` requirement. + case dynamic(ReducedMotionOptionProvider, dataID: AnyHashable) +} + +extension ReducedMotionOption { + /// The standard behavior where Lottie animations play normally with no overrides. + /// By default this mode is used when the system "reduced motion" option is disabled. + public static var standardMotion: ReducedMotionOption { .specific(.standardMotion) } + + /// Lottie animations with a "reduced motion" marker will play that marker instead of any other animations. + /// By default this mode is used when the system "reduced motion" option is enabled. + /// - Valid marker names include "reduced motion", "reducedMotion", "reduced_motion" (case insensitive). + public static var reducedMotion: ReducedMotionOption { .specific(.reducedMotion) } + + /// A `ReducedMotionOptionProvider` that returns `.reducedMotion` when + /// the system `UIAccessibility.isReduceMotionEnabled` option is `true`. + /// This is the default option of `LottieConfiguration`. + public static var systemReducedMotionToggle: ReducedMotionOption { + .dynamic(SystemReducedMotionOptionProvider(), dataID: ObjectIdentifier(SystemReducedMotionOptionProvider.self)) + } +} + +extension ReducedMotionOption { + /// The current `ReducedMotionMode` based on the currently selected option. + public var currentReducedMotionMode: ReducedMotionMode { + switch self { + case .specific(let specificMode): + return specificMode + case .dynamic(let optionProvider, _): + return optionProvider.currentReducedMotionMode + } + } +} + +// MARK: Hashable + +extension ReducedMotionOption: Hashable { + public static func ==(_ lhs: ReducedMotionOption, _ rhs: ReducedMotionOption) -> Bool { + switch (lhs, rhs) { + case (.specific(let lhsMode), .specific(let rhsMode)): + return lhsMode == rhsMode + case (.dynamic(_, let lhsDataID), .dynamic(_, dataID: let rhsDataID)): + return lhsDataID == rhsDataID + case (.dynamic, .specific), (.specific, .dynamic): + return false + } + } + + public func hash(into hasher: inout Hasher) { + switch self { + case .specific(let mode): + hasher.combine(mode) + case .dynamic(_, let dataID): + hasher.combine(dataID) + } + } +} + +// MARK: - ReducedMotionMode + +public enum ReducedMotionMode: Hashable { + /// The default behavior where Lottie animations play normally with no overrides + /// By default this mode is used when the system "reduced motion" option is disabled. + case standardMotion + + /// Lottie animations with a "reduced motion" marker will play that marker instead of any other animations. + /// By default this mode is used when the system "reduced motion" option is enabled. + case reducedMotion +} + +// MARK: - ReducedMotionOptionProvider + +/// A type that returns a dynamic `ReducedMotionMode` which is checked when playing a Lottie animation. +public protocol ReducedMotionOptionProvider { + var currentReducedMotionMode: ReducedMotionMode { get } +} + +// MARK: - SystemReducedMotionOptionProvider + +/// A `ReducedMotionOptionProvider` that returns `.reducedMotion` when +/// the system `UIAccessibility.isReduceMotionEnabled` option is `true`. +public struct SystemReducedMotionOptionProvider: ReducedMotionOptionProvider { + public init() { } + + public var currentReducedMotionMode: ReducedMotionMode { + #if canImport(UIKit) + if UIAccessibility.isReduceMotionEnabled { + return .reducedMotion + } else { + return .standardMotion + } + #else + return .standardMotion + #endif + } +} diff --git a/Sources/Public/LottieConfiguration.swift b/Sources/Public/Configuration/RenderingEngineOption.swift similarity index 61% rename from Sources/Public/LottieConfiguration.swift rename to Sources/Public/Configuration/RenderingEngineOption.swift index 5059aee812..187303d8c5 100644 --- a/Sources/Public/LottieConfiguration.swift +++ b/Sources/Public/Configuration/RenderingEngineOption.swift @@ -1,45 +1,5 @@ -// Created by Cal Stephens on 12/13/21. -// Copyright © 2021 Airbnb Inc. All rights reserved. - -import QuartzCore - -// MARK: - LottieConfiguration - -/// Global configuration options for Lottie animations -public struct LottieConfiguration: Hashable { - - // MARK: Lifecycle - - public init( - renderingEngine: RenderingEngineOption = .automatic, - decodingStrategy: DecodingStrategy = .dictionaryBased, - colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB()) - { - self.renderingEngine = renderingEngine - self.decodingStrategy = decodingStrategy - self.colorSpace = colorSpace - } - - // MARK: Public - - /// The global configuration of Lottie, - /// which applies to all `LottieAnimationView`s by default. - public static var shared = LottieConfiguration() - - /// The rendering engine implementation to use when displaying an animation - /// - Defaults to `RenderingEngineOption.automatic`, which uses the - /// Core Animation rendering engine for supported animations, and - /// falls back to using the Main Thread rendering engine for - /// animations that use features not supported by the Core Animation engine. - public var renderingEngine: RenderingEngineOption - - /// The decoding implementation to use when parsing an animation JSON file - public var decodingStrategy: DecodingStrategy - - /// The color space to be used for rendering - /// - Defaults to `CGColorSpaceCreateDeviceRGB()` - public var colorSpace: CGColorSpace -} +// Created by Cal Stephens on 7/14/23. +// Copyright © 2023 Airbnb Inc. All rights reserved. // MARK: - RenderingEngineOption @@ -151,18 +111,3 @@ extension RenderingEngine: RawRepresentable, CustomStringConvertible { rawValue } } - -// MARK: - DecodingStrategy - -/// How animation files should be decoded -public enum DecodingStrategy: Hashable { - /// Use Codable. This is was the default strategy introduced on Lottie 3, but should be rarely - /// used as it's slower than `dictionaryBased`. Kept here for any possible compatibility issues - /// that may come up, but consider it soft-deprecated. - case legacyCodable - - /// Manually deserialize a dictionary into an Animation. - /// This should be at least 2-3x faster than using Codable and due to that - /// it's the default as of Lottie 4.x. - case dictionaryBased -}