LottieAnimationView.swift 41.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054
//
//  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:
      .pauseAndRestore
    case .coreAnimation:
      .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)):
      lhsAmount == rhsAmount
    case (.playOnce, .playOnce),
         (.loop, .loop),
         (.autoReverse, .autoReverse):
      true
    default:
      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
      bounds = CGRect.zero
      bounds = rect
      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 {
      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 }
      animationLoaded?(self, animation)
      invalidateIntrinsicContentSize()
      setNeedsLayout()
    }

    lottieAnimationLayer.animationLayerDidLoad = { [weak self] _, _ in
      guard let self else { return }
      invalidateIntrinsicContentSize()
      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
}