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

580 lines
23 KiB
Swift

// Created by Bryn Bodayle on 1/20/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
#if canImport(SwiftUI)
import SwiftUI
// MARK: - LottieView
/// A wrapper which exposes Lottie's `LottieAnimationView` to SwiftUI
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
// MARK: Lifecycle
/// Creates a `LottieView` that displays the given animation
public init(animation: LottieAnimation?) where Placeholder == EmptyView {
_animationSource = State(initialValue: animation.map(LottieAnimationSource.lottieAnimation))
placeholder = nil
}
/// Initializes a `LottieView` with the provided `DotLottieFile` for display.
///
/// - Important: Avoid using this initializer with the `SynchronouslyBlockingCurrentThread` APIs.
/// If decompression of a `.lottie` file is necessary, prefer using the `.init(_ loadAnimation:)`
/// initializer, which takes an asynchronous closure:
/// ```
/// LottieView {
/// try await DotLottieFile.named(name)
/// }
/// ```
public init(dotLottieFile: DotLottieFile?) where Placeholder == EmptyView {
_animationSource = State(initialValue: dotLottieFile.map(LottieAnimationSource.dotLottieFile))
placeholder = nil
}
/// Creates a `LottieView` that asynchronously loads and displays the given `LottieAnimation`.
/// The `loadAnimation` closure is called exactly once in `onAppear`.
/// If you wish to call `loadAnimation` again at a different time, you can use `.reloadAnimationTrigger(...)`.
public init(_ loadAnimation: @escaping () async throws -> LottieAnimation?) where Placeholder == EmptyView {
self.init(loadAnimation, placeholder: EmptyView.init)
}
/// Creates a `LottieView` that asynchronously loads and displays the given `LottieAnimation`.
/// The `loadAnimation` closure is called exactly once in `onAppear`.
/// If you wish to call `loadAnimation` again at a different time, you can use `.reloadAnimationTrigger(...)`.
/// While the animation is loading, the `placeholder` view is shown in place of the `LottieAnimationView`.
public init(
_ loadAnimation: @escaping () async throws -> LottieAnimation?,
@ViewBuilder placeholder: @escaping (() -> Placeholder))
{
self.init {
try await loadAnimation().map(LottieAnimationSource.lottieAnimation)
} placeholder: {
placeholder()
}
}
/// Creates a `LottieView` that asynchronously loads and displays the given `DotLottieFile`.
/// The `loadDotLottieFile` closure is called exactly once in `onAppear`.
/// If you wish to call `loadAnimation` again at a different time, you can use `.reloadAnimationTrigger(...)`.
/// You can use the `DotLottieFile` static methods API which use Swift concurrency to load your `.lottie` files:
/// ```
/// LottieView {
/// try await DotLottieFile.named(name)
/// }
/// ```
public init(_ loadDotLottieFile: @escaping () async throws -> DotLottieFile?) where Placeholder == EmptyView {
self.init(loadDotLottieFile, placeholder: EmptyView.init)
}
/// Creates a `LottieView` that asynchronously loads and displays the given `DotLottieFile`.
/// The `loadDotLottieFile` closure is called exactly once in `onAppear`.
/// If you wish to call `loadAnimation` again at a different time, you can use `.reloadAnimationTrigger(...)`.
/// While the animation is loading, the `placeholder` view is shown in place of the `LottieAnimationView`.
/// You can use the `DotLottieFile` static methods API which use Swift concurrency to load your `.lottie` files:
/// ```
/// LottieView {
/// try await DotLottieFile.named(name)
/// } placeholder: {
/// LoadingView()
/// }
/// ```
public init(
_ loadDotLottieFile: @escaping () async throws -> DotLottieFile?,
@ViewBuilder placeholder: @escaping (() -> Placeholder))
{
self.init {
try await loadDotLottieFile().map(LottieAnimationSource.dotLottieFile)
} placeholder: {
placeholder()
}
}
/// Creates a `LottieView` that asynchronously loads and displays the given `LottieAnimationSource`.
/// The `loadAnimation` closure is called exactly once in `onAppear`.
/// If you wish to call `loadAnimation` again at a different time, you can use `.reloadAnimationTrigger(...)`.
/// While the animation is loading, the `placeholder` view is shown in place of the `LottieAnimationView`.
public init(_ loadAnimation: @escaping () async throws -> LottieAnimationSource?) where Placeholder == EmptyView {
self.init(loadAnimation, placeholder: EmptyView.init)
}
/// Creates a `LottieView` that asynchronously loads and displays the given `LottieAnimationSource`.
/// The `loadAnimation` closure is called exactly once in `onAppear`.
/// If you wish to call `loadAnimation` again at a different time, you can use `.reloadAnimationTrigger(...)`.
/// While the animation is loading, the `placeholder` view is shown in place of the `LottieAnimationView`.
public init(
_ loadAnimation: @escaping () async throws -> LottieAnimationSource?,
@ViewBuilder placeholder: @escaping () -> Placeholder)
{
self.loadAnimation = loadAnimation
self.placeholder = placeholder
_animationSource = State(initialValue: nil)
}
// MARK: Public
public var body: some View {
LottieAnimationView.swiftUIView {
LottieAnimationView(
animationSource: animationSource,
imageProvider: imageProviderConfiguration?.imageProvider,
textProvider: textProvider,
fontProvider: fontProvider,
configuration: configuration,
logger: logger)
}
.sizing(sizing)
.configure { context in
applyCurrentAnimationConfiguration(to: context.view)
}
.configurations(configurations)
.opacity(animationSource == nil ? 0 : 1)
.overlay {
placeholder?()
.opacity(animationSource == nil ? 1 : 0)
}
.onAppear {
loadAnimationIfNecessary()
}
.valueChanged(value: reloadAnimationTrigger) { _ in
reloadAnimationTriggerDidChange()
}
}
/// Returns a copy of this `LottieView` updated to have the given closure applied to its
/// represented `LottieAnimationView` whenever it is updated via the `updateUIView()`
/// or `updateNSView()` method.
public func configure(_ configure: @escaping (LottieAnimationView) -> Void) -> Self {
var copy = self
copy.configurations.append { context in
configure(context.view)
}
return copy
}
/// Returns a copy of this view that can be resized by scaling its animation to fit the size
/// offered by its parent.
public func resizable() -> Self {
var copy = self
copy.sizing = .proposed
return copy
}
@available(*, deprecated, renamed: "playing()", message: "Will be removed in a future major release.")
public func play() -> Self {
playbackMode(.playing(.fromProgress(nil, toProgress: 1, loopMode: .playOnce)))
}
/// Returns a copy of this view that loops its animation from the start to end whenever visible
public func looping() -> Self {
playbackMode(.playing(.fromProgress(0, toProgress: 1, loopMode: .loop)))
}
@available(*, deprecated, renamed: "playing(_:)", message: "Will be removed in a future major release.")
public func play(loopMode: LottieLoopMode = .playOnce) -> Self {
playbackMode(.playing(.fromProgress(nil, toProgress: 1, loopMode: loopMode)))
}
@available(*, deprecated, renamed: "playbackMode(_:)", message: "Will be removed in a future major release.")
public func play(_ playbackMode: LottiePlaybackMode) -> Self {
self.playbackMode(playbackMode)
}
/// Returns a copy of this view playing with the given playback mode
public func playing(_ mode: LottiePlaybackMode.PlaybackMode) -> Self {
playbackMode(.playing(mode))
}
/// Returns a copy of this view playing from the current frame to the end frame,
/// with the given `LottiePlaybackMode`.
public func playing(loopMode: LottieLoopMode) -> Self {
playbackMode(.playing(.fromProgress(nil, toProgress: 1, loopMode: loopMode)))
}
// Returns a copy of this view playing once from the current frame to the end frame
public func playing() -> Self {
playbackMode(.playing(.fromProgress(nil, toProgress: 1, loopMode: .playOnce)))
}
/// Returns a copy of this view paused with the given state
public func paused(at state: LottiePlaybackMode.PausedState = .currentFrame) -> Self {
playbackMode(.paused(at: state))
}
/// Returns a copy of this view using the given `LottiePlaybackMode`
public func playbackMode(_ playbackMode: LottiePlaybackMode) -> Self {
var copy = self
copy.playbackMode = playbackMode
return copy
}
/// Returns a copy of this view playing its animation at the given speed
public func animationSpeed(_ animationSpeed: Double) -> Self {
var copy = self
copy.animationSpeed = animationSpeed
return copy
}
/// Returns a copy of this view with the given closure that is called whenever the
/// `LottieAnimationSource` provided via `init` is loaded and applied to the underlying `LottieAnimationView`.
public func animationDidLoad(_ animationDidLoad: @escaping (LottieAnimationSource) -> Void) -> Self {
var copy = self
copy.animationDidLoad = animationDidLoad
return copy
}
/// Returns a copy of this view with the given `LottieCompletionBlock` that is called
/// when an animation finishes playing.
public func animationDidFinish(_ animationCompletionHandler: LottieCompletionBlock?) -> Self {
var copy = self
copy.animationCompletionHandler = { [previousCompletionHandler = self.animationCompletionHandler] completed in
previousCompletionHandler?(completed)
animationCompletionHandler?(completed)
}
return copy
}
/// Returns a copy of this view updated to have the provided background behavior.
public func backgroundBehavior(_ value: LottieBackgroundBehavior) -> Self {
configure { view in
view.backgroundBehavior = value
}
}
/// Returns a copy of this view with its accessibility label updated to the given value.
public func accessibilityLabel(_ accessibilityLabel: String?) -> Self {
configure { view in
#if os(macOS)
view.setAccessibilityElement(accessibilityLabel != nil)
view.setAccessibilityLabel(accessibilityLabel)
#else
view.isAccessibilityElement = accessibilityLabel != nil
view.accessibilityLabel = accessibilityLabel
#endif
}
}
/// Returns a copy of this view with its `LottieConfiguration` updated to the given value.
public func configuration(_ configuration: LottieConfiguration) -> Self {
var copy = self
copy.configuration = configuration
copy = copy.configure { view in
if view.configuration != configuration {
view.configuration = configuration
}
}
return copy
}
/// Returns a copy of this view with its `LottieLogger` updated to the given value.
/// - The underlying `LottieAnimationView`'s `LottieLogger` is immutable after configured,
/// so this value is only used when initializing the `LottieAnimationView` for the first time.
public func logger(_ logger: LottieLogger) -> Self {
var copy = self
copy.logger = logger
return copy
}
/// Returns a copy of this view with its image provider updated to the given value.
/// The image provider must be `Equatable` to avoid unnecessary state updates / re-renders.
public func imageProvider<ImageProvider: AnimationImageProvider & Equatable>(_ imageProvider: ImageProvider) -> Self {
var copy = self
copy.imageProviderConfiguration = (
imageProvider: imageProvider,
imageProvidersAreEqual: { untypedLHS, untypedRHS in
guard
let lhs = untypedLHS as? ImageProvider,
let rhs = untypedRHS as? ImageProvider
else { return false }
return lhs == rhs
})
return copy
}
/// Returns a copy of this view with its text provider updated to the given value.
/// The image provider must be `Equatable` to avoid unnecessary state updates / re-renders.
public func textProvider<TextProvider: AnimationKeypathTextProvider & Equatable>(_ textProvider: TextProvider) -> Self {
var copy = self
copy.textProvider = textProvider
copy = copy.configure { view in
if (view.textProvider as? TextProvider) != textProvider {
view.textProvider = textProvider
}
}
return copy
}
/// Returns a copy of this view with its image provider updated to the given value.
/// The image provider must be `Equatable` to avoid unnecessary state updates / re-renders.
public func fontProvider<FontProvider: AnimationFontProvider & Equatable>(_ fontProvider: FontProvider) -> Self {
var copy = self
copy.fontProvider = fontProvider
copy = configure { view in
if (view.fontProvider as? FontProvider) != fontProvider {
view.fontProvider = fontProvider
}
}
return copy
}
/// Returns a copy of this view using the given value provider for the given keypath.
/// The value provider must be `Equatable` to avoid unnecessary state updates / re-renders.
public func valueProvider<ValueProvider: AnyValueProvider & Equatable>(
_ valueProvider: ValueProvider,
for keypath: AnimationKeypath)
-> Self
{
configure { view in
if (view.valueProviders[keypath] as? ValueProvider) != valueProvider {
view.setValueProvider(valueProvider, keypath: keypath)
}
}
}
/// Returns a copy of this view updated to display the given `AnimationProgressTime`.
/// - If the `currentProgress` value is provided, the `currentProgress` of the
/// underlying `LottieAnimationView` is updated. This will pause any existing animations.
/// - If the `animationProgress` is `nil`, no changes will be made and any existing animations
/// will continue playing uninterrupted.
public func currentProgress(_ currentProgress: AnimationProgressTime?) -> Self {
guard let currentProgress else { return self }
var copy = self
copy.playbackMode = .paused(at: .progress(currentProgress))
return copy
}
/// Returns a copy of this view updated to display the given `AnimationFrameTime`.
/// - If the `currentFrame` value is provided, the `currentFrame` of the
/// underlying `LottieAnimationView` is updated. This will pause any existing animations.
/// - If the `currentFrame` is `nil`, no changes will be made and any existing animations
/// will continue playing uninterrupted.
public func currentFrame(_ currentFrame: AnimationFrameTime?) -> Self {
guard let currentFrame else { return self }
var copy = self
copy.playbackMode = .paused(at: .frame(currentFrame))
return copy
}
/// Returns a copy of this view updated to display the given time value.
/// - If the `currentTime` value is provided, the `currentTime` of the
/// underlying `LottieAnimationView` is updated. This will pause any existing animations.
/// - If the `currentTime` is `nil`, no changes will be made and any existing animations
/// will continue playing uninterrupted.
public func currentTime(_ currentTime: TimeInterval?) -> Self {
guard let currentTime else { return self }
var copy = self
copy.playbackMode = .paused(at: .time(currentTime))
return copy
}
/// Returns a new instance of this view, which will invoke the provided `loadAnimation` closure
/// whenever the `binding` value is updated.
///
/// - Note: This function requires a valid `loadAnimation` closure provided during view initialization,
/// otherwise `reloadAnimationTrigger` will have no effect.
/// - Parameters:
/// - binding: The binding that triggers the reloading when its value changes.
/// - showPlaceholder: When `true`, the current animation will be removed before invoking `loadAnimation`,
/// displaying the `Placeholder` until the new animation loads.
/// When `false`, the previous animation remains visible while the new one loads.
public func reloadAnimationTrigger(_ value: some Equatable, showPlaceholder: Bool = true) -> Self {
var copy = self
copy.reloadAnimationTrigger = AnyEquatable(value)
copy.showPlaceholderWhileReloading = showPlaceholder
return copy
}
/// Returns a view that updates the given binding each frame with the animation's `realtimeAnimationProgress`.
/// The `LottieView` is wrapped in a `TimelineView` with the `.animation` schedule.
/// - This is a one-way binding. Its value is updated but never read.
/// - If provided, the binding will be updated each frame with the `realtimeAnimationProgress`
/// of the underlying `LottieAnimationView`. This is potentially expensive since it triggers
/// a state update every frame.
/// - If the binding is `nil`, the `TimelineView` will be paused and no updates will occur to the binding.
@available(iOS 15.0, tvOS 15.0, macOS 12.0, *)
public func getRealtimeAnimationProgress(_ realtimeAnimationProgress: Binding<AnimationProgressTime>?) -> some View {
TimelineView(.animation(paused: realtimeAnimationProgress == nil)) { _ in
configure { view in
if let realtimeAnimationProgress {
DispatchQueue.main.async {
realtimeAnimationProgress.wrappedValue = view.realtimeAnimationProgress
}
}
}
}
}
/// Returns a view that updates the given binding each frame with the animation's `realtimeAnimationProgress`.
/// The `LottieView` is wrapped in a `TimelineView` with the `.animation` schedule.
/// - This is a one-way binding. Its value is updated but never read.
/// - If provided, the binding will be updated each frame with the `realtimeAnimationProgress`
/// of the underlying `LottieAnimationView`. This is potentially expensive since it triggers
/// a state update every frame.
/// - If the binding is `nil`, the `TimelineView` will be paused and no updates will occur to the binding.
@available(iOS 15.0, tvOS 15.0, macOS 12.0, *)
public func getRealtimeAnimationFrame(_ realtimeAnimationFrame: Binding<AnimationFrameTime>?) -> some View {
TimelineView(.animation(paused: realtimeAnimationFrame == nil)) { _ in
configure { view in
if let realtimeAnimationFrame {
DispatchQueue.main.async {
realtimeAnimationFrame.wrappedValue = view.realtimeAnimationFrame
}
}
}
}
}
/// Returns a copy of this view with the `DotLottieConfigurationComponents`
/// updated to the given value.
/// - Defaults to `[.imageProvider]`
/// - If a component is specified here, that value in the `DotLottieConfiguration`
/// of an active dotLottie animation will override any value provided via other methods.
public func dotLottieConfigurationComponents(
_ dotLottieConfigurationComponents: DotLottieConfigurationComponents)
-> Self
{
var copy = self
copy.dotLottieConfigurationComponents = dotLottieConfigurationComponents
return copy
}
// MARK: Internal
var configurations = [SwiftUIView<LottieAnimationView, Void>.Configuration]()
// MARK: Private
@State private var animationSource: LottieAnimationSource?
private var playbackMode: LottiePlaybackMode?
private var animationSpeed: Double?
private var reloadAnimationTrigger: AnyEquatable?
private var loadAnimation: (() async throws -> LottieAnimationSource?)?
private var animationDidLoad: ((LottieAnimationSource) -> Void)?
private var animationCompletionHandler: LottieCompletionBlock?
private var showPlaceholderWhileReloading = false
private var textProvider: AnimationKeypathTextProvider = DefaultTextProvider()
private var fontProvider: AnimationFontProvider = DefaultFontProvider()
private var configuration: LottieConfiguration = .shared
private var dotLottieConfigurationComponents: DotLottieConfigurationComponents = .imageProvider
private var logger: LottieLogger = .shared
private var sizing = SwiftUIMeasurementContainerStrategy.automatic
private let placeholder: (() -> Placeholder)?
private var imageProviderConfiguration: (
imageProvider: AnimationImageProvider,
imageProvidersAreEqual: (AnimationImageProvider, AnimationImageProvider) -> Bool)?
private func loadAnimationIfNecessary() {
guard let loadAnimation else { return }
Task {
do {
animationSource = try await loadAnimation()
} catch {
logger.warn("Failed to load asynchronous Lottie animation with error: \(error)")
}
}
}
private func reloadAnimationTriggerDidChange() {
guard loadAnimation != nil else { return }
if showPlaceholderWhileReloading {
animationSource = nil
}
loadAnimationIfNecessary()
}
/// Applies playback configuration for the current animation to the `LottieAnimationView`
private func applyCurrentAnimationConfiguration(to view: LottieAnimationView) {
guard let animationSource else { return }
var imageProviderConfiguration = imageProviderConfiguration
var playbackMode = playbackMode
var animationSpeed = animationSpeed
// When playing a dotLottie animation, its `DotLottieConfiguration`
// can override some behavior of the animation.
if let dotLottieConfiguration = animationSource.dotLottieAnimation?.configuration {
// Only use the value from the `DotLottieConfiguration` is that component
// is specified in the list of `dotLottieConfigurationComponents`.
if dotLottieConfigurationComponents.contains(.loopMode) {
playbackMode = playbackMode?.loopMode(dotLottieConfiguration.loopMode)
}
if dotLottieConfigurationComponents.contains(.animationSpeed) {
animationSpeed = dotLottieConfiguration.speed
}
if
dotLottieConfigurationComponents.contains(.imageProvider),
let dotLottieImageProvider = dotLottieConfiguration.dotLottieImageProvider
{
imageProviderConfiguration = (
imageProvider: dotLottieImageProvider,
imageProvidersAreEqual: { untypedLHS, untypedRHS in
guard
let lhs = untypedLHS as? DotLottieImageProvider,
let rhs = untypedRHS as? DotLottieImageProvider
else { return false }
return lhs == rhs
})
}
}
// We check referential equality of the animation before updating as updating the
// animation has a side-effect of rebuilding the animation layer, and it would be
// prohibitive to do so on every state update.
if animationSource.animation !== view.animation {
view.loadAnimation(animationSource)
animationDidLoad?(animationSource)
}
if
let playbackMode,
playbackMode != view.currentPlaybackMode
{
view.setPlaybackMode(playbackMode, completion: animationCompletionHandler)
}
if
let (imageProvider, imageProvidersAreEqual) = imageProviderConfiguration,
!imageProvidersAreEqual(imageProvider, view.imageProvider)
{
view.imageProvider = imageProvider
}
if
let animationSpeed,
animationSpeed != view.animationSpeed
{
view.animationSpeed = animationSpeed
}
}
}
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension View {
/// The `.overlay` modifier that uses a `ViewBuilder` is available in iOS 15+, this helper function helps us to use the same API in older OSs
fileprivate func overlay(
@ViewBuilder content: () -> some View)
-> some View
{
overlay(content(), alignment: .center)
}
}
#endif