Wallpaper_Home/wallpaper_project/Pods/lottie-ios/Sources/Public/Animation/LottieAnimationView.swift
2024-04-17 14:37:20 +08:00

1055 lines
42 KiB
Swift

//
// LottieAnimationView.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/23/19.
//
import QuartzCore
// MARK: - LottieBackgroundBehavior
/// Describes the behavior of an AnimationView when the app is moved to the background.
public enum LottieBackgroundBehavior {
/// Stop the animation and reset it to the beginning of its current play time. The completion block is called.
case stop
/// Pause the animation in its current state. The completion block is called.
case pause
/// Pause the animation and restart it when the application moves to the foreground.
/// The completion block is stored and called when the animation completes.
/// - This is the default when using the Main Thread rendering engine.
case pauseAndRestore
/// Stops the animation and sets it to the end of its current play time. The completion block is called.
case forceFinish
/// The animation continues playing in the background.
/// - This is the default when using the Core Animation rendering engine.
/// Playing an animation using the Core Animation engine doesn't come with any CPU overhead,
/// so using `.continuePlaying` avoids the need to stop and then resume the animation
/// (which does come with some CPU overhead).
/// - This mode should not be used with the Main Thread rendering engine.
case continuePlaying
// MARK: Public
/// The default background behavior, based on the rendering engine being used to play the animation.
/// - Playing an animation using the Main Thread rendering engine comes with CPU overhead,
/// so the animation should be paused or stopped when the `LottieAnimationView` is not visible.
/// - Playing an animation using the Core Animation rendering engine does not come with any
/// CPU overhead, so these animations do not need to be paused in the background.
public static func `default`(for renderingEngine: RenderingEngine) -> LottieBackgroundBehavior {
switch renderingEngine {
case .mainThread:
return .pauseAndRestore
case .coreAnimation:
return .continuePlaying
}
}
}
// MARK: - LottieLoopMode
/// Defines animation loop behavior
public enum LottieLoopMode: Hashable {
/// Animation is played once then stops.
case playOnce
/// Animation will loop from beginning to end until stopped.
case loop
/// Animation will play forward, then backwards and loop until stopped.
case autoReverse
/// Animation will loop from beginning to end up to defined amount of times.
case `repeat`(Float)
/// Animation will play forward, then backwards a defined amount of times.
case repeatBackwards(Float)
}
// MARK: Equatable
extension LottieLoopMode: Equatable {
public static func == (lhs: LottieLoopMode, rhs: LottieLoopMode) -> Bool {
switch (lhs, rhs) {
case (.repeat(let lhsAmount), .repeat(let rhsAmount)),
(.repeatBackwards(let lhsAmount), .repeatBackwards(let rhsAmount)):
return lhsAmount == rhsAmount
case (.playOnce, .playOnce),
(.loop, .loop),
(.autoReverse, .autoReverse):
return true
default:
return false
}
}
}
// MARK: - LottieAnimationView
/// A UIView subclass for rendering Lottie animations.
/// - Also available as a SwiftUI view (`LottieView`) and a CALayer subclass (`LottieAnimationLayer`)
@IBDesignable
open class LottieAnimationView: LottieAnimationViewBase {
// MARK: Lifecycle
// MARK: - Public (Initializers)
/// Initializes an AnimationView with an animation.
public init(
animation: LottieAnimation?,
imageProvider: AnimationImageProvider? = nil,
textProvider: AnimationKeypathTextProvider = DefaultTextProvider(),
fontProvider: AnimationFontProvider = DefaultFontProvider(),
configuration: LottieConfiguration = .shared,
logger: LottieLogger = .shared)
{
lottieAnimationLayer = LottieAnimationLayer(
animation: animation,
imageProvider: imageProvider,
textProvider: textProvider,
fontProvider: fontProvider,
configuration: configuration,
logger: logger)
self.logger = logger
super.init(frame: .zero)
commonInit()
if let animation {
frame = animation.bounds
}
}
/// Initializes an AnimationView with a .lottie file.
public init(
dotLottie: DotLottieFile?,
animationId: String? = nil,
textProvider: AnimationKeypathTextProvider = DefaultTextProvider(),
fontProvider: AnimationFontProvider = DefaultFontProvider(),
configuration: LottieConfiguration = .shared,
logger: LottieLogger = .shared)
{
lottieAnimationLayer = LottieAnimationLayer(
dotLottie: dotLottie,
animationId: animationId,
textProvider: textProvider,
fontProvider: fontProvider,
configuration: configuration,
logger: logger)
self.logger = logger
super.init(frame: .zero)
commonInit()
if let animation {
frame = animation.bounds
}
}
public init(
configuration: LottieConfiguration = .shared,
logger: LottieLogger = .shared)
{
lottieAnimationLayer = LottieAnimationLayer(configuration: configuration, logger: logger)
self.logger = logger
super.init(frame: .zero)
commonInit()
}
public override init(frame: CGRect) {
lottieAnimationLayer = LottieAnimationLayer(
animation: nil,
imageProvider: BundleImageProvider(bundle: Bundle.main, searchPath: nil),
textProvider: DefaultTextProvider(),
fontProvider: DefaultFontProvider(),
configuration: .shared,
logger: .shared)
logger = .shared
super.init(frame: frame)
commonInit()
}
required public init?(coder aDecoder: NSCoder) {
lottieAnimationLayer = LottieAnimationLayer(
animation: nil,
imageProvider: BundleImageProvider(bundle: Bundle.main, searchPath: nil),
textProvider: DefaultTextProvider(),
fontProvider: DefaultFontProvider(),
configuration: .shared,
logger: .shared)
logger = .shared
super.init(coder: aDecoder)
commonInit()
}
convenience init(
animationSource: LottieAnimationSource?,
imageProvider: AnimationImageProvider? = nil,
textProvider: AnimationKeypathTextProvider = DefaultTextProvider(),
fontProvider: AnimationFontProvider = DefaultFontProvider(),
configuration: LottieConfiguration = .shared,
logger: LottieLogger = .shared)
{
switch animationSource {
case .lottieAnimation(let animation):
self.init(
animation: animation,
imageProvider: imageProvider,
textProvider: textProvider,
fontProvider: fontProvider,
configuration: configuration,
logger: logger)
case .dotLottieFile(let dotLottieFile):
self.init(
dotLottie: dotLottieFile,
textProvider: textProvider,
fontProvider: fontProvider,
configuration: configuration,
logger: logger)
case nil:
self.init(
animation: nil,
imageProvider: imageProvider,
textProvider: textProvider,
fontProvider: fontProvider,
configuration: configuration,
logger: logger)
}
}
// MARK: Open
/// Applies the given `LottiePlaybackMode` to this layer.
/// - Parameter completion: A closure that is called after
/// an animation triggered by this method completes.
open func play(_ mode: LottiePlaybackMode.PlaybackMode, completion: LottieCompletionBlock? = nil) {
lottieAnimationLayer.play(mode, completion: completion)
}
/// Plays the animation from its current state to the end.
///
/// - Parameter completion: An optional completion closure to be called when the animation completes playing.
open func play(completion: LottieCompletionBlock? = nil) {
lottieAnimationLayer.play(completion: completion)
}
/// Plays the animation from a progress (0-1) to a progress (0-1).
///
/// - Parameter fromProgress: The start progress of the animation. If `nil` the animation will start at the current progress.
/// - Parameter toProgress: The end progress of the animation.
/// - Parameter loopMode: The loop behavior of the animation. If `nil` the view's `loopMode` property will be used.
/// - Parameter completion: An optional completion closure to be called when the animation stops.
open func play(
fromProgress: AnimationProgressTime? = nil,
toProgress: AnimationProgressTime,
loopMode: LottieLoopMode? = nil,
completion: LottieCompletionBlock? = nil)
{
lottieAnimationLayer.play(fromProgress: fromProgress, toProgress: toProgress, loopMode: loopMode, completion: completion)
}
/// Plays the animation from a start frame to an end frame in the animation's framerate.
///
/// - Parameter fromFrame: The start frame of the animation. If `nil` the animation will start at the current frame.
/// - Parameter toFrame: The end frame of the animation.
/// - Parameter loopMode: The loop behavior of the animation. If `nil` the view's `loopMode` property will be used.
/// - Parameter completion: An optional completion closure to be called when the animation stops.
open func play(
fromFrame: AnimationFrameTime? = nil,
toFrame: AnimationFrameTime,
loopMode: LottieLoopMode? = nil,
completion: LottieCompletionBlock? = nil)
{
lottieAnimationLayer.play(fromFrame: fromFrame, toFrame: toFrame, loopMode: loopMode, completion: completion)
}
/// Plays the animation from a named marker to another marker.
///
/// Markers are point in time that are encoded into the Animation data and assigned
/// a name.
///
/// NOTE: If markers are not found the play command will exit.
///
/// - Parameter fromMarker: The start marker for the animation playback. If `nil` the
/// animation will start at the current progress.
/// - Parameter toMarker: The end marker for the animation playback.
/// - Parameter playEndMarkerFrame: A flag to determine whether or not to play the frame of the end marker. If the
/// end marker represents the end of the section to play, it should be to true. If the provided end marker
/// represents the beginning of the next section, it should be false.
/// - Parameter loopMode: The loop behavior of the animation. If `nil` the view's `loopMode` property will be used.
/// - Parameter completion: An optional completion closure to be called when the animation stops.
open func play(
fromMarker: String? = nil,
toMarker: String,
playEndMarkerFrame: Bool = true,
loopMode: LottieLoopMode? = nil,
completion: LottieCompletionBlock? = nil)
{
lottieAnimationLayer.play(
fromMarker: fromMarker,
toMarker: toMarker,
playEndMarkerFrame: playEndMarkerFrame,
loopMode: loopMode,
completion: completion)
}
/// Plays the animation from a named marker to the end of the marker's duration.
///
/// A marker is a point in time with an associated duration that is encoded into the
/// animation data and assigned a name.
///
/// NOTE: If marker is not found the play command will exit.
///
/// - Parameter marker: The start marker for the animation playback.
/// - Parameter loopMode: The loop behavior of the animation. If `nil` the view's `loopMode` property will be used.
/// - Parameter completion: An optional completion closure to be called when the animation stops.
open func play(
marker: String,
loopMode: LottieLoopMode? = nil,
completion: LottieCompletionBlock? = nil)
{
lottieAnimationLayer.play(marker: marker, loopMode: loopMode, completion: completion)
}
/// Plays the given markers sequentially in order.
///
/// A marker is a point in time with an associated duration that is encoded into the
/// animation data and assigned a name. Multiple markers can be played sequentially
/// to create programmable animations.
///
/// If a marker is not found, it will be skipped.
///
/// If a marker doesn't have a duration value, it will play with a duration of 0
/// (effectively being skipped).
///
/// If another animation is played (by calling any `play` method) while this
/// marker sequence is playing, the marker sequence will be cancelled.
///
/// - Parameter markers: The list of markers to play sequentially.
/// - Parameter completion: An optional completion closure to be called when the animation stops.
open func play(
markers: [String],
completion: LottieCompletionBlock? = nil)
{
lottieAnimationLayer.play(markers: markers, completion: completion)
}
/// Stops the animation and resets the view to its start frame.
///
/// The completion closure will be called with `false`
open func stop() {
lottieAnimationLayer.stop()
}
/// Pauses the animation in its current state.
///
/// The completion closure will be called with `false`
open func pause() {
lottieAnimationLayer.pause()
}
@available(*, deprecated, renamed: "setPlaybackMode(_:completion:)", message: "Will be removed in a future major release.")
open func play(
_ playbackMode: LottiePlaybackMode,
animationCompletionHandler: LottieCompletionBlock? = nil)
{
lottieAnimationLayer.setPlaybackMode(playbackMode, completion: animationCompletionHandler)
}
/// Applies the given `LottiePlaybackMode` to this layer.
/// - Parameter completion: A closure that is called after
/// an animation triggered by this method completes.
open func setPlaybackMode(
_ playbackMode: LottiePlaybackMode,
completion: LottieCompletionBlock? = nil)
{
lottieAnimationLayer.setPlaybackMode(playbackMode, completion: completion)
}
// MARK: Public
/// Whether or not transform and position changes of the view should animate alongside
/// any existing animation context.
/// - Defaults to `true` which will grab the current animation context and animate position and
/// transform changes matching the current context's curve and duration.
/// `false` will cause transform and position changes to happen unanimated
public var animateLayoutChangesWithCurrentCoreAnimationContext = true
/// The configuration that this `LottieAnimationView` uses when playing its animation
public var configuration: LottieConfiguration {
get { lottieAnimationLayer.configuration }
set { lottieAnimationLayer.configuration = newValue }
}
/// Value Providers that have been registered using `setValueProvider(_:keypath:)`
public var valueProviders: [AnimationKeypath: AnyValueProvider] {
lottieAnimationLayer.valueProviders
}
/// Describes the behavior of an AnimationView when the app is moved to the background.
///
/// The default for the Main Thread animation engine is `pause`,
/// which pauses the animation when the application moves to
/// the background. This prevents the animation from consuming CPU
/// resources when not on-screen. The completion block is called with
/// `false` for completed.
///
/// The default for the Core Animation engine is `continuePlaying`,
/// since the Core Animation engine does not have any CPU overhead.
public var backgroundBehavior: LottieBackgroundBehavior {
get { lottieAnimationLayer.backgroundBehavior }
set { lottieAnimationLayer.backgroundBehavior = newValue }
}
/// Sets the animation backing the animation view. Setting this will clear the
/// view's contents, completion blocks and current state. The new animation will
/// be loaded up and set to the beginning of its timeline.
public var animation: LottieAnimation? {
get { lottieAnimationLayer.animation }
set { lottieAnimationLayer.animation = newValue }
}
/// A closure that is called when `self.animation` is loaded. When setting this closure,
/// it is called immediately if `self.animation` is non-nil.
///
/// When initializing a `LottieAnimationView`, the animation will either be loaded
/// synchronously (when loading a `LottieAnimation` from a .json file on disk)
/// or asynchronously (when loading a `DotLottieFile` from disk, or downloading
/// an animation from a URL). This closure is called in both cases once the
/// animation is loaded and applied, so can be a useful way to configure this
/// `LottieAnimationView` regardless of which initializer was used. For example:
///
/// ```
/// let animationView: LottieAnimationView
///
/// if loadDotLottieFile {
/// // Loads the .lottie file asynchronously
/// animationView = LottieAnimationView(dotLottieName: "animation")
/// } else {
/// // Loads the .json file synchronously
/// animationView = LottieAnimationView(name: "animation")
/// }
///
/// animationView.animationLoaded = { animationView, animation in
/// // If using a .lottie file, this is called once the file finishes loading.
/// // If using a .json file, this is called immediately (since the animation is loaded synchronously).
/// animationView.play()
/// }
/// ```
public var animationLoaded: ((_ animationView: LottieAnimationView, _ animation: LottieAnimation) -> Void)? {
didSet {
if let animation {
animationLoaded?(self, animation)
}
}
}
/// Sets the image provider for the animation view. An image provider provides the
/// animation with its required image data.
///
/// Setting this will cause the animation to reload its image contents.
public var imageProvider: AnimationImageProvider {
get { lottieAnimationLayer.imageProvider }
set { lottieAnimationLayer.imageProvider = newValue }
}
/// Sets the text provider for animation view. A text provider provides the
/// animation with values for text layers
public var textProvider: AnimationKeypathTextProvider {
get { lottieAnimationLayer.textProvider }
set { lottieAnimationLayer.textProvider = newValue }
}
/// Sets the text provider for animation view. A text provider provides the
/// animation with values for text layers
public var fontProvider: AnimationFontProvider {
get { lottieAnimationLayer.fontProvider }
set { lottieAnimationLayer.fontProvider = newValue }
}
/// Whether or not the animation is masked to the bounds. Defaults to true.
public var maskAnimationToBounds: Bool {
get { lottieAnimationLayer.maskAnimationToBounds }
set { lottieAnimationLayer.maskAnimationToBounds = newValue }
}
/// Returns `true` if the animation is currently playing.
public var isAnimationPlaying: Bool {
lottieAnimationLayer.isAnimationPlaying
}
/// Returns `true` if the animation will start playing when this view is added to a window.
public var isAnimationQueued: Bool {
lottieAnimationLayer.hasAnimationContext && waitingToPlayAnimation
}
/// Sets the loop behavior for `play` calls. Defaults to `playOnce`
public var loopMode: LottieLoopMode {
get { lottieAnimationLayer.loopMode }
set { lottieAnimationLayer.loopMode = newValue }
}
/// When `true` the animation view will rasterize its contents when not animating.
/// Rasterizing will improve performance of static animations.
///
/// Note: this will not produce crisp results at resolutions above the animations natural resolution.
///
/// Defaults to `false`
public var shouldRasterizeWhenIdle: Bool {
get { lottieAnimationLayer.shouldRasterizeWhenIdle }
set { lottieAnimationLayer.shouldRasterizeWhenIdle = newValue }
}
/// Sets the current animation time with a Progress Time
///
/// Note: Setting this will stop the current animation, if any.
/// Note 2: If `animation` is nil, setting this will fallback to 0
public var currentProgress: AnimationProgressTime {
get { lottieAnimationLayer.currentProgress }
set { lottieAnimationLayer.currentProgress = newValue }
}
/// Sets the current animation time with a time in seconds.
///
/// Note: Setting this will stop the current animation, if any.
/// Note 2: If `animation` is nil, setting this will fallback to 0
public var currentTime: TimeInterval {
get { lottieAnimationLayer.currentTime }
set { lottieAnimationLayer.currentTime = newValue }
}
/// Sets the current animation time with a frame in the animations framerate.
///
/// Note: Setting this will stop the current animation, if any.
public var currentFrame: AnimationFrameTime {
get { lottieAnimationLayer.currentFrame }
set { lottieAnimationLayer.currentFrame = newValue }
}
/// Returns the current animation frame while an animation is playing.
public var realtimeAnimationFrame: AnimationFrameTime {
lottieAnimationLayer.realtimeAnimationFrame
}
/// Returns the current animation frame while an animation is playing.
public var realtimeAnimationProgress: AnimationProgressTime {
lottieAnimationLayer.realtimeAnimationProgress
}
/// Sets the speed of the animation playback. Defaults to 1
public var animationSpeed: CGFloat {
get { lottieAnimationLayer.animationSpeed }
set { lottieAnimationLayer.animationSpeed = newValue }
}
/// When `true` the animation will play back at the framerate encoded in the
/// `LottieAnimation` model. When `false` the animation will play at the framerate
/// of the device.
///
/// Defaults to false
public var respectAnimationFrameRate: Bool {
get { lottieAnimationLayer.respectAnimationFrameRate }
set { lottieAnimationLayer.respectAnimationFrameRate = newValue }
}
/// Controls the cropping of an Animation. Setting this property will crop the animation
/// to the current views bounds by the viewport frame. The coordinate space is specified
/// in the animation's coordinate space.
///
/// Animatable.
public var viewportFrame: CGRect? {
didSet {
// This is really ugly, but is needed to trigger a layout pass within an animation block.
// Typically this happens automatically, when layout objects are UIView based.
// The animation layer is a CALayer which will not implicitly grab the animation
// duration of a UIView animation block.
//
// By setting bounds and then resetting bounds the UIView animation block's
// duration and curve are captured and added to the layer. This is used in the
// layout block to animate the animationLayer's position and size.
let rect = bounds
self.bounds = CGRect.zero
self.bounds = rect
self.setNeedsLayout()
}
}
override public var intrinsicContentSize: CGSize {
if let animation = lottieAnimationLayer.animation {
return animation.bounds.size
}
return .zero
}
/// The rendering engine currently being used by this view.
/// - This will only be `nil` in cases where the configuration is `automatic`
/// but a `RootAnimationLayer` hasn't been constructed yet
public var currentRenderingEngine: RenderingEngine? {
lottieAnimationLayer.currentRenderingEngine
}
/// The current `LottiePlaybackMode` that is being used
public var currentPlaybackMode: LottiePlaybackMode? {
lottieAnimationLayer.currentPlaybackMode
}
/// Whether or not the Main Thread rendering engine should use `forceDisplayUpdate()`
/// when rendering each individual frame.
/// - The main thread rendering engine implements optimizations to decrease the amount
/// of properties that have to be re-rendered on each frame. There are some cases
/// where this can result in bugs / incorrect behavior, so we allow it to be disabled.
/// - Forcing a full render on every frame will decrease performance, and is not recommended
/// except as a workaround to a bug in the main thread rendering engine.
/// - Has no effect when using the Core Animation rendering engine.
public var mainThreadRenderingEngineShouldForceDisplayUpdateOnEachFrame: Bool {
get { lottieAnimationLayer.mainThreadRenderingEngineShouldForceDisplayUpdateOnEachFrame }
set { lottieAnimationLayer.mainThreadRenderingEngineShouldForceDisplayUpdateOnEachFrame = newValue }
}
/// Sets the lottie file backing the animation view. Setting this will clear the
/// view's contents, completion blocks and current state. The new animation will
/// be loaded up and set to the beginning of its timeline.
/// The loopMode, animationSpeed and imageProvider will be set according
/// to lottie file settings
/// - Parameters:
/// - animationId: Internal animation id to play. Optional
/// Defaults to play first animation in file.
/// - dotLottieFile: Lottie file to play
public func loadAnimation(
_ animationId: String? = nil,
from dotLottieFile: DotLottieFile)
{
lottieAnimationLayer.loadAnimation(animationId, from: dotLottieFile)
}
/// Sets the lottie file backing the animation view. Setting this will clear the
/// view's contents, completion blocks and current state. The new animation will
/// be loaded up and set to the beginning of its timeline.
/// The loopMode, animationSpeed and imageProvider will be set according
/// to lottie file settings
/// - Parameters:
/// - atIndex: Internal animation index to play. Optional
/// Defaults to play first animation in file.
/// - dotLottieFile: Lottie file to play
public func loadAnimation(
atIndex index: Int,
from dotLottieFile: DotLottieFile)
{
lottieAnimationLayer.loadAnimation(atIndex: index, from: dotLottieFile)
}
/// Reloads the images supplied to the animation from the `imageProvider`
public func reloadImages() {
lottieAnimationLayer.reloadImages()
}
/// Forces the LottieAnimationView to redraw its contents.
public func forceDisplayUpdate() {
lottieAnimationLayer.forceDisplayUpdate()
}
/// Sets a ValueProvider for the specified keypath. The value provider will be set
/// on all properties that match the keypath.
///
/// Nearly all properties of a Lottie animation can be changed at runtime using a
/// combination of `Animation Keypaths` and `Value Providers`.
/// Setting a ValueProvider on a keypath will cause the animation to update its
/// contents and read the new Value Provider.
///
/// A value provider provides a typed value on a frame by frame basis.
///
/// - Parameter valueProvider: The new value provider for the properties.
/// - Parameter keypath: The keypath used to search for properties.
///
/// Example:
/// ```
/// /// A keypath that finds the color value for all `Fill 1` nodes.
/// let fillKeypath = AnimationKeypath(keypath: "**.Fill 1.Color")
/// /// A Color Value provider that returns a reddish color.
/// let redValueProvider = ColorValueProvider(Color(r: 1, g: 0.2, b: 0.3, a: 1))
/// /// Set the provider on the animationView.
/// animationView.setValueProvider(redValueProvider, keypath: fillKeypath)
/// ```
public func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) {
lottieAnimationLayer.setValueProvider(valueProvider, keypath: keypath)
}
/// Reads the value of a property specified by the Keypath.
/// Returns nil if no property is found.
///
/// - Parameter for: The keypath used to search for the property.
/// - Parameter atFrame: The Frame Time of the value to query. If nil then the current frame is used.
public func getValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime?) -> Any? {
lottieAnimationLayer.getValue(for: keypath, atFrame: atFrame)
}
/// Reads the original value of a property specified by the Keypath.
/// This will ignore any value providers and can be useful when implementing a value providers that makes change to the original value from the animation.
/// Returns nil if no property is found.
///
/// - Parameter for: The keypath used to search for the property.
/// - Parameter atFrame: The Frame Time of the value to query. If nil then the current frame is used.
public func getOriginalValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime?) -> Any? {
lottieAnimationLayer.getOriginalValue(for: keypath, atFrame: atFrame)
}
/// Logs all child keypaths.
/// Logs the result of `allHierarchyKeypaths()` to the `LottieLogger`.
public func logHierarchyKeypaths() {
lottieAnimationLayer.logHierarchyKeypaths()
}
/// Computes and returns a list of all child keypaths in the current animation.
/// The returned list is the same as the log output of `logHierarchyKeypaths()`
public func allHierarchyKeypaths() -> [String] {
lottieAnimationLayer.allHierarchyKeypaths()
}
/// Searches for the nearest child layer to the first Keypath and adds the subview
/// to that layer. The subview will move and animate with the child layer.
/// Furthermore the subview will be in the child layers coordinate space.
///
/// Note: if no layer is found for the keypath, then nothing happens.
///
/// - Parameter subview: The subview to add to the found animation layer.
/// - Parameter keypath: The keypath used to find the animation layer.
///
/// Example:
/// ```
/// /// A keypath that finds `Layer 1`
/// let layerKeypath = AnimationKeypath(keypath: "Layer 1")
///
/// /// Wrap the custom view in an `AnimationSubview`
/// let subview = AnimationSubview()
/// subview.addSubview(customView)
///
/// /// Set the provider on the animationView.
/// animationView.addSubview(subview, forLayerAt: layerKeypath)
/// ```
public func addSubview(_ subview: AnimationSubview, forLayerAt keypath: AnimationKeypath) {
guard let sublayer = lottieAnimationLayer.rootAnimationLayer?.layer(for: keypath) else {
return
}
setNeedsLayout()
layoutIfNeeded()
lottieAnimationLayer.forceDisplayUpdate()
addSubview(subview)
if let subViewLayer = subview.viewLayer {
sublayer.addSublayer(subViewLayer)
}
}
/// Converts a CGRect from the LottieAnimationView's coordinate space into the
/// coordinate space of the layer found at Keypath.
///
/// If no layer is found, nil is returned
///
/// - Parameter rect: The CGRect to convert.
/// - Parameter toLayerAt: The keypath used to find the layer.
public func convert(_ rect: CGRect, toLayerAt keypath: AnimationKeypath?) -> CGRect? {
let convertedRect = lottieAnimationLayer.convert(rect, toLayerAt: keypath)
setNeedsLayout()
layoutIfNeeded()
return convertedRect
}
/// Converts a CGPoint from the LottieAnimationView's coordinate space into the
/// coordinate space of the layer found at Keypath.
///
/// If no layer is found, nil is returned
///
/// - Parameter point: The CGPoint to convert.
/// - Parameter toLayerAt: The keypath used to find the layer.
public func convert(_ point: CGPoint, toLayerAt keypath: AnimationKeypath?) -> CGPoint? {
let convertedRect = lottieAnimationLayer.convert(point, toLayerAt: keypath)
setNeedsLayout()
layoutIfNeeded()
return convertedRect
}
/// Sets the enabled state of all animator nodes found with the keypath search.
/// This can be used to interactively enable / disable parts of the animation.
///
/// - Parameter isEnabled: When true the animator nodes affect the rendering tree. When false the node is removed from the tree.
/// - Parameter keypath: The keypath used to find the node(s).
public func setNodeIsEnabled(isEnabled: Bool, keypath: AnimationKeypath) {
lottieAnimationLayer.setNodeIsEnabled(isEnabled: isEnabled, keypath: keypath)
}
/// Markers are a way to describe a point in time by a key name.
///
/// Markers are encoded into animation JSON. By using markers a designer can mark
/// playback points for a developer to use without having to worry about keeping
/// track of animation frames. If the animation file is updated, the developer
/// does not need to update playback code.
///
/// Returns the Progress Time for the marker named. Returns nil if no marker found.
public func progressTime(forMarker named: String) -> AnimationProgressTime? {
lottieAnimationLayer.progressTime(forMarker: named)
}
/// Markers are a way to describe a point in time by a key name.
///
/// Markers are encoded into animation JSON. By using markers a designer can mark
/// playback points for a developer to use without having to worry about keeping
/// track of animation frames. If the animation file is updated, the developer
/// does not need to update playback code.
///
/// Returns the Frame Time for the marker named. Returns nil if no marker found.
public func frameTime(forMarker named: String) -> AnimationFrameTime? {
lottieAnimationLayer.frameTime(forMarker: named)
}
/// Markers are a way to describe a point in time and a duration by a key name.
///
/// Markers are encoded into animation JSON. By using markers a designer can mark
/// playback points for a developer to use without having to worry about keeping
/// track of animation frames. If the animation file is updated, the developer
/// does not need to update playback code.
///
/// - Returns: The duration frame time for the marker, or `nil` if no marker found.
public func durationFrameTime(forMarker named: String) -> AnimationFrameTime? {
lottieAnimationLayer.durationFrameTime(forMarker: named)
}
// MARK: Internal
// The backing CALayer for this animation view.
let lottieAnimationLayer: LottieAnimationLayer
var animationLayer: RootAnimationLayer? {
lottieAnimationLayer.rootAnimationLayer
}
/// Set animation name from Interface Builder
@IBInspectable var animationName: String? {
didSet {
self.lottieAnimationLayer.animation = animationName.flatMap { LottieAnimation.named($0, animationCache: nil)
}
}
}
override func commonInit() {
super.commonInit()
lottieAnimationLayer.screenScale = screenScale
viewLayer?.addSublayer(lottieAnimationLayer)
lottieAnimationLayer.animationLoaded = { [weak self] _, animation in
guard let self else { return }
self.animationLoaded?(self, animation)
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
lottieAnimationLayer.animationLayerDidLoad = { [weak self] _, _ in
guard let self else { return }
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
}
override func layoutAnimation() {
guard let animation = lottieAnimationLayer.animation, let animationLayer = lottieAnimationLayer.animationLayer else { return }
var position = animation.bounds.center
let xform: CATransform3D
var shouldForceUpdates = false
if let viewportFrame {
shouldForceUpdates = contentMode == .redraw
let compAspect = viewportFrame.size.width / viewportFrame.size.height
let viewAspect = bounds.size.width / bounds.size.height
let dominantDimension = compAspect > viewAspect ? bounds.size.width : bounds.size.height
let compDimension = compAspect > viewAspect ? viewportFrame.size.width : viewportFrame.size.height
let scale = dominantDimension / compDimension
let viewportOffset = animation.bounds.center - viewportFrame.center
xform = CATransform3DTranslate(CATransform3DMakeScale(scale, scale, 1), viewportOffset.x, viewportOffset.y, 0)
position = bounds.center
} else {
switch contentMode {
case .scaleToFill:
position = bounds.center
xform = CATransform3DMakeScale(
bounds.size.width / animation.size.width,
bounds.size.height / animation.size.height,
1)
case .scaleAspectFit:
position = bounds.center
let compAspect = animation.size.width / animation.size.height
let viewAspect = bounds.size.width / bounds.size.height
let dominantDimension = compAspect > viewAspect ? bounds.size.width : bounds.size.height
let compDimension = compAspect > viewAspect ? animation.size.width : animation.size.height
let scale = dominantDimension / compDimension
xform = CATransform3DMakeScale(scale, scale, 1)
case .scaleAspectFill:
position = bounds.center
let compAspect = animation.size.width / animation.size.height
let viewAspect = bounds.size.width / bounds.size.height
let scaleWidth = compAspect < viewAspect
let dominantDimension = scaleWidth ? bounds.size.width : bounds.size.height
let compDimension = scaleWidth ? animation.size.width : animation.size.height
let scale = dominantDimension / compDimension
xform = CATransform3DMakeScale(scale, scale, 1)
case .redraw:
shouldForceUpdates = true
xform = CATransform3DIdentity
case .center:
position = bounds.center
xform = CATransform3DIdentity
case .top:
position.x = bounds.center.x
xform = CATransform3DIdentity
case .bottom:
position.x = bounds.center.x
position.y = bounds.maxY - animation.bounds.midY
xform = CATransform3DIdentity
case .left:
position.y = bounds.center.y
xform = CATransform3DIdentity
case .right:
position.y = bounds.center.y
position.x = bounds.maxX - animation.bounds.midX
xform = CATransform3DIdentity
case .topLeft:
xform = CATransform3DIdentity
case .topRight:
position.x = bounds.maxX - animation.bounds.midX
xform = CATransform3DIdentity
case .bottomLeft:
position.y = bounds.maxY - animation.bounds.midY
xform = CATransform3DIdentity
case .bottomRight:
position.x = bounds.maxX - animation.bounds.midX
position.y = bounds.maxY - animation.bounds.midY
xform = CATransform3DIdentity
#if canImport(UIKit)
@unknown default:
logger.assertionFailure("unsupported contentMode: \(contentMode.rawValue)")
xform = CATransform3DIdentity
#endif
}
}
// UIView Animation does not implicitly set CAAnimation time or timing fuctions.
// If layout is changed in an animation we must get the current animation duration
// and timing function and then manually create a CAAnimation to match the UIView animation.
// If layout is changed without animation, explicitly set animation duration to 0.0
// inside CATransaction to avoid unwanted artifacts.
/// Check if any animation exist on the view's layer, and match it.
if
let key = lottieAnimationLayer.animationKeys()?.first,
let animation = lottieAnimationLayer.animation(forKey: key),
animateLayoutChangesWithCurrentCoreAnimationContext
{
// The layout is happening within an animation block. Grab the animation data.
let positionKey = "LayoutPositionAnimation"
let transformKey = "LayoutTransformAnimation"
animationLayer.removeAnimation(forKey: positionKey)
animationLayer.removeAnimation(forKey: transformKey)
let positionAnimation = animation.copy() as? CABasicAnimation ?? CABasicAnimation(keyPath: "position")
positionAnimation.keyPath = "position"
positionAnimation.isAdditive = false
positionAnimation.fromValue = (animationLayer.presentation() ?? animationLayer).position
positionAnimation.toValue = position
positionAnimation.isRemovedOnCompletion = true
let xformAnimation = animation.copy() as? CABasicAnimation ?? CABasicAnimation(keyPath: "transform")
xformAnimation.keyPath = "transform"
xformAnimation.isAdditive = false
xformAnimation.fromValue = (animationLayer.presentation() ?? animationLayer).transform
xformAnimation.toValue = xform
xformAnimation.isRemovedOnCompletion = true
animationLayer.position = position
animationLayer.transform = xform
animationLayer.anchorPoint = lottieAnimationLayer.anchorPoint
animationLayer.add(positionAnimation, forKey: positionKey)
animationLayer.add(xformAnimation, forKey: transformKey)
} else {
// In performance tests, we have to wrap the animation view setup
// in a `CATransaction` in order for the layers to be deallocated at
// the correct time. The `CATransaction`s in this method interfere
// with the ones managed by the performance test, and aren't actually
// necessary in a headless environment, so we disable them.
if TestHelpers.performanceTestsAreRunning {
animationLayer.position = position
animationLayer.transform = xform
} else {
CATransaction.begin()
CATransaction.setAnimationDuration(0.0)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .linear))
animationLayer.position = position
animationLayer.transform = xform
CATransaction.commit()
}
}
if shouldForceUpdates {
lottieAnimationLayer.forceDisplayUpdate()
}
}
func updateRasterizationState() {
lottieAnimationLayer.updateRasterizationState()
}
/// Updates the animation frame. Does not affect any current animations
func updateAnimationFrame(_ newFrame: CGFloat) {
lottieAnimationLayer.updateAnimationFrame(newFrame)
}
@objc
override func animationWillMoveToBackground() {
updateAnimationForBackgroundState()
}
@objc
override func animationWillEnterForeground() {
updateAnimationForForegroundState()
}
override func animationMovedToWindow() {
/// Don't update any state if the `superview` is `nil`
/// When A viewA owns superViewB, it removes the superViewB from the window. At this point, viewA still owns superViewB and triggers the viewA method: -didmovetowindow
guard superview != nil else { return }
if window != nil {
updateAnimationForForegroundState()
} else {
updateAnimationForBackgroundState()
}
}
func updateInFlightAnimation() {
lottieAnimationLayer.updateInFlightAnimation()
}
func loadAnimation(_ animationSource: LottieAnimationSource?) {
lottieAnimationLayer.loadAnimation(animationSource)
}
// MARK: Fileprivate
fileprivate var waitingToPlayAnimation = false
fileprivate func updateAnimationForBackgroundState() {
lottieAnimationLayer.updateAnimationForBackgroundState()
}
fileprivate func updateAnimationForForegroundState() {
let wasWaitingToPlayAnimation = waitingToPlayAnimation
if waitingToPlayAnimation {
waitingToPlayAnimation = false
}
lottieAnimationLayer.updateAnimationForForegroundState(wasWaitingToPlayAnimation: wasWaitingToPlayAnimation)
}
// MARK: Private
private let logger: LottieLogger
}