246 lines
6.5 KiB
Swift
246 lines
6.5 KiB
Swift
//
|
|
// AnimatedControl.swift
|
|
// lottie-swift
|
|
//
|
|
// Created by Brandon Withrow on 2/4/19.
|
|
//
|
|
|
|
#if canImport(UIKit)
|
|
import UIKit
|
|
#elseif canImport(AppKit)
|
|
import AppKit
|
|
#endif
|
|
|
|
// MARK: - AnimatedControl
|
|
|
|
/// Lottie comes prepacked with a two Animated Controls, `AnimatedSwitch` and
|
|
/// `AnimatedButton`. Both of these controls are built on top of `AnimatedControl`
|
|
///
|
|
/// `AnimatedControl` is a subclass of `UIControl` that provides an interactive
|
|
/// mechanism for controlling the visual state of an animation in response to
|
|
/// user actions.
|
|
///
|
|
/// The `AnimatedControl` will show and hide layers depending on the current
|
|
/// `UIControl.State` of the control.
|
|
///
|
|
/// Users of `AnimationControl` can set a Layer Name for each `UIControl.State`.
|
|
/// When the state is change the `AnimationControl` will change the visibility
|
|
/// of its layers.
|
|
///
|
|
/// NOTE: Do not initialize directly. This is intended to be subclassed.
|
|
open class AnimatedControl: LottieControlType {
|
|
|
|
// MARK: Lifecycle
|
|
|
|
// MARK: Initializers
|
|
|
|
public init(
|
|
animation: LottieAnimation?,
|
|
configuration: LottieConfiguration = .shared)
|
|
{
|
|
animationView = LottieAnimationView(
|
|
animation: animation,
|
|
configuration: configuration)
|
|
|
|
super.init(frame: animation?.bounds ?? .zero)
|
|
commonInit()
|
|
}
|
|
|
|
public init() {
|
|
animationView = LottieAnimationView()
|
|
super.init(frame: .zero)
|
|
commonInit()
|
|
}
|
|
|
|
required public init?(coder aDecoder: NSCoder) {
|
|
animationView = LottieAnimationView()
|
|
super.init(coder: aDecoder)
|
|
commonInit()
|
|
}
|
|
|
|
// MARK: Open
|
|
|
|
// MARK: UIControl Overrides
|
|
|
|
open override var isEnabled: Bool {
|
|
didSet {
|
|
updateForState()
|
|
}
|
|
}
|
|
|
|
#if canImport(UIKit)
|
|
open override var isSelected: Bool {
|
|
didSet {
|
|
updateForState()
|
|
}
|
|
}
|
|
#endif
|
|
|
|
open override var isHighlighted: Bool {
|
|
didSet {
|
|
updateForState()
|
|
}
|
|
}
|
|
|
|
open override var intrinsicContentSize: CGSize {
|
|
animationView.intrinsicContentSize
|
|
}
|
|
|
|
#if canImport(UIKit)
|
|
open override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
|
updateForState()
|
|
return super.beginTracking(touch, with: event)
|
|
}
|
|
|
|
open override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
|
updateForState()
|
|
return super.continueTracking(touch, with: event)
|
|
}
|
|
|
|
open override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
|
updateForState()
|
|
return super.endTracking(touch, with: event)
|
|
}
|
|
|
|
open override func cancelTracking(with event: UIEvent?) {
|
|
updateForState()
|
|
super.cancelTracking(with: event)
|
|
}
|
|
|
|
#elseif canImport(AppKit)
|
|
open override func mouseDown(with mouseDownEvent: NSEvent) {
|
|
guard let window else { return }
|
|
|
|
currentState = .highlighted
|
|
updateForState()
|
|
handle(LottieControlEvent(mouseDownEvent.type, inside: eventIsInside(mouseDownEvent)))
|
|
|
|
// AppKit mouse-tracking loop from:
|
|
// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/HandlingMouseEvents/HandlingMouseEvents.html#//apple_ref/doc/uid/10000060i-CH6-SW4
|
|
var keepOn = true
|
|
while
|
|
keepOn,
|
|
let event = window.nextEvent(
|
|
matching: .any,
|
|
until: .distantFuture,
|
|
inMode: .eventTracking,
|
|
dequeue: true)
|
|
{
|
|
if event.type == .leftMouseUp {
|
|
keepOn = false
|
|
}
|
|
|
|
let isInside = eventIsInside(event)
|
|
handle(LottieControlEvent(event.type, inside: isInside))
|
|
|
|
let expectedState = (isInside && keepOn) ? LottieNSControlState.highlighted : .normal
|
|
if currentState != expectedState {
|
|
currentState = expectedState
|
|
updateForState()
|
|
}
|
|
}
|
|
}
|
|
|
|
func handle(_: LottieNSControlEvent) {
|
|
// To be overridden in subclasses
|
|
}
|
|
|
|
private func eventIsInside(_ event: NSEvent) -> Bool {
|
|
let mouseLocation = convert(event.locationInWindow, from: nil)
|
|
return isMousePoint(mouseLocation, in: bounds)
|
|
}
|
|
#endif
|
|
|
|
open func animationDidSet() { }
|
|
|
|
// MARK: Public
|
|
|
|
/// The animation view in which the animation is rendered.
|
|
public let animationView: LottieAnimationView
|
|
|
|
/// The animation backing the animated control.
|
|
public var animation: LottieAnimation? {
|
|
didSet {
|
|
animationView.animation = animation
|
|
animationView.bounds = animation?.bounds ?? .zero
|
|
#if canImport(UIKit)
|
|
setNeedsLayout()
|
|
#elseif canImport(AppKit)
|
|
needsLayout = true
|
|
#endif
|
|
updateForState()
|
|
animationDidSet()
|
|
}
|
|
}
|
|
|
|
/// The speed of the animation playback. Defaults to 1
|
|
public var animationSpeed: CGFloat {
|
|
set { animationView.animationSpeed = newValue }
|
|
get { animationView.animationSpeed }
|
|
}
|
|
|
|
/// Sets which Animation Layer should be visible for the given state.
|
|
public func setLayer(named: String, forState: LottieControlState) {
|
|
stateMap[forState.rawValue] = named
|
|
updateForState()
|
|
}
|
|
|
|
/// Sets a ValueProvider for the specified keypath
|
|
public func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) {
|
|
animationView.setValueProvider(valueProvider, keypath: keypath)
|
|
}
|
|
|
|
// MARK: Internal
|
|
|
|
var stateMap: [UInt: String] = [:]
|
|
|
|
#if canImport(UIKit)
|
|
var currentState: LottieControlState {
|
|
state
|
|
}
|
|
|
|
#elseif canImport(AppKit)
|
|
var currentState = LottieControlState.normal
|
|
#endif
|
|
|
|
func updateForState() {
|
|
guard let animationLayer = animationView.animationLayer else { return }
|
|
if
|
|
let layerName = stateMap[currentState.rawValue],
|
|
let stateLayer = animationLayer.layer(for: AnimationKeypath(keypath: layerName))
|
|
{
|
|
for layer in animationLayer._animationLayers {
|
|
layer.isHidden = true
|
|
}
|
|
stateLayer.isHidden = false
|
|
} else {
|
|
for layer in animationLayer._animationLayers {
|
|
layer.isHidden = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Private
|
|
|
|
private func commonInit() {
|
|
#if canImport(UIKit)
|
|
animationView.clipsToBounds = false
|
|
clipsToBounds = true
|
|
#endif
|
|
|
|
animationView.translatesAutoresizingMaskIntoConstraints = false
|
|
animationView.backgroundBehavior = .forceFinish
|
|
addSubview(animationView)
|
|
animationView.contentMode = .scaleAspectFit
|
|
|
|
#if canImport(UIKit)
|
|
animationView.isUserInteractionEnabled = false
|
|
#endif
|
|
|
|
animationView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
|
|
animationView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
|
|
animationView.topAnchor.constraint(equalTo: topAnchor).isActive = true
|
|
animationView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
|
|
}
|
|
}
|