303 lines
9.1 KiB
Swift
303 lines
9.1 KiB
Swift
//
|
|
// ActivityIndicator.swift
|
|
// PrivadoVPN
|
|
//
|
|
// Created by Juraldinio on 2/28/21.
|
|
// Copyright © 2021 Privado LLC. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import AppKit
|
|
import QuartzCore
|
|
|
|
@IBDesignable
|
|
open class ActivityIndicator: NSView {
|
|
|
|
private enum Constants {
|
|
static let spinAnimationKey = "spinAnimation"
|
|
static let progressPropertyKey = "progress"
|
|
//
|
|
static let animationKeyPath = "transform"
|
|
}
|
|
|
|
// MARK: - Properties
|
|
|
|
/// Indicates if the view will show the progress, or just spin.
|
|
@IBInspectable
|
|
var isIndeterminate: Bool = true {
|
|
didSet {
|
|
if !self.isIndeterminate {
|
|
// self.animates = NO;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The amount that should be shown when `isIndeterminate` is set to `YES`.
|
|
@IBInspectable
|
|
var progress: CGFloat = 0 {
|
|
didSet {
|
|
guard self.progress > 0
|
|
, self.progress <= 1 else {
|
|
self.progress = oldValue
|
|
return
|
|
}
|
|
|
|
if !self.isIndeterminate { self.reloadIndicatorContent() }
|
|
}
|
|
}
|
|
|
|
/// Indicates if the view is animating.
|
|
@IBInspectable
|
|
var animates: Bool = true {
|
|
didSet {
|
|
self.reloadIndicatorContent()
|
|
self.reloadAnimation()
|
|
self.reloadVisibility()
|
|
}
|
|
}
|
|
|
|
/// Indicates if the view will be hidden if it's stopped.
|
|
@IBInspectable
|
|
var hideWhenStopped: Bool = true {
|
|
didSet {
|
|
self.reloadVisibility()
|
|
}
|
|
}
|
|
|
|
/// The length of a single line.
|
|
@IBInspectable
|
|
var lengthOfLine: CGFloat = 6.0 {
|
|
didSet {
|
|
self.reloadIndicatorContent()
|
|
}
|
|
}
|
|
|
|
/// The width of a single line.
|
|
@IBInspectable
|
|
var widthOfLine: CGFloat = 3.0 {
|
|
didSet {
|
|
self.reloadIndicatorContent()
|
|
}
|
|
}
|
|
|
|
/// The number of lines of the indicator.
|
|
@IBInspectable
|
|
var numberOfLines: Int = 8 {
|
|
didSet {
|
|
self.reloadIndicatorContent()
|
|
self.reloadAnimation()
|
|
}
|
|
}
|
|
|
|
/// The distance of the lines from the middle.
|
|
@IBInspectable
|
|
var innerMargin: CGFloat = 4.0 {
|
|
didSet {
|
|
self.reloadIndicatorContent()
|
|
}
|
|
}
|
|
|
|
/// Duration of a single rotation.
|
|
@IBInspectable
|
|
// swiftlint:disable valid_ibinspectable
|
|
var animationDuration: TimeInterval = 0.6 {
|
|
didSet {
|
|
self.reloadAnimation()
|
|
}
|
|
}
|
|
// swiftlint:enable valid_ibinspectable
|
|
|
|
/// Defines if the animation is smooth or gradual.
|
|
@IBInspectable
|
|
var steppedAnimation: Bool = true {
|
|
didSet {
|
|
self.reloadAnimation()
|
|
}
|
|
}
|
|
|
|
/// The color of the progress indicator
|
|
@IBInspectable
|
|
var color: NSColor = .black {
|
|
didSet {
|
|
self.reloadIndicatorContent()
|
|
}
|
|
}
|
|
|
|
private lazy var rootLayer: CALayer = {
|
|
let layer = CALayer()
|
|
return layer
|
|
}()
|
|
|
|
private lazy var progressIndicatorLayer: CALayer = {
|
|
let layer = CALayer()
|
|
return layer
|
|
}()
|
|
|
|
// MARK: - Initializers
|
|
|
|
override init(frame frameRect: NSRect) {
|
|
super.init(frame: frameRect)
|
|
self.initLayers()
|
|
}
|
|
|
|
public required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
self.initLayers()
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
open override func awakeFromNib() {
|
|
super.awakeFromNib()
|
|
self.reloadAnimation()
|
|
}
|
|
|
|
static public override func defaultAnimation(forKey key: NSAnimatablePropertyKey) -> Any? {
|
|
guard key.compare(Constants.progressPropertyKey) == .orderedSame else {
|
|
return super.defaultAnimation(forKey: key)
|
|
}
|
|
return CABasicAnimation()
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func initLayers() {
|
|
self.layer = self.rootLayer
|
|
self.wantsLayer = true
|
|
self.progressIndicatorLayer.frame = self.rootLayer.frame
|
|
self.rootLayer.addSublayer(self.progressIndicatorLayer)
|
|
|
|
self.reloadIndicatorContent()
|
|
self.reloadAnimation()
|
|
}
|
|
|
|
private func reloadAnimation() {
|
|
self.progressIndicatorLayer.removeAnimation(forKey: Constants.spinAnimationKey)
|
|
if self.animates {
|
|
self.progressIndicatorLayer.add(self.keyFrameAnimationForCurrentPreferences(), forKey: Constants.spinAnimationKey)
|
|
}
|
|
}
|
|
|
|
private func reloadIndicatorContent() {
|
|
self.progressIndicatorLayer.contents = self.progressImage
|
|
}
|
|
|
|
private func reloadVisibility() {
|
|
if self.hideWhenStopped
|
|
, !self.animates
|
|
, self.isIndeterminate {
|
|
self.isHidden = true
|
|
} else {
|
|
self.isHidden = false
|
|
}
|
|
}
|
|
|
|
// MARK: - Drawing
|
|
|
|
var progressImage: NSImage {
|
|
|
|
guard self.bounds.size.width > 0
|
|
, self.bounds.size.height > 0 else {
|
|
return NSImage()
|
|
}
|
|
|
|
let progressImage = NSImage(size: self.bounds.size)
|
|
progressImage.lockFocus()
|
|
|
|
do {
|
|
|
|
NSGraphicsContext.saveGraphicsState()
|
|
|
|
do {
|
|
self.color.set()
|
|
|
|
let rect = self.bounds
|
|
let line = NSBezierPath(roundedRect: CGRect(x: rect.width / 2 - self.widthOfLine / 2,
|
|
y: rect.height / 2 - self.innerMargin - self.lengthOfLine,
|
|
width: self.widthOfLine,
|
|
height: self.lengthOfLine),
|
|
xRadius: self.widthOfLine / 2,
|
|
yRadius: self.widthOfLine / 2)
|
|
|
|
let lineDrawingBlock: (Int) -> Void = { lineNumber in
|
|
|
|
let lineInstance = line.rotate(angle: CGFloat((2.0 * Double.pi) / Double(self.numberOfLines * lineNumber) + Double.pi),
|
|
at: CGPoint(x: rect.width / 2, y: rect.height / 2))
|
|
|
|
if self.isIndeterminate {
|
|
|
|
self.color
|
|
.withAlphaComponent( 1.0 - CGFloat(1.0 / CGFloat(self.numberOfLines * lineNumber)) )
|
|
.set()
|
|
}
|
|
|
|
lineInstance.fill()
|
|
}
|
|
|
|
let ranges: ClosedRange = !self.isIndeterminate
|
|
? self.numberOfLines - Int(CGFloat(self.numberOfLines) * self.progress) ... self.numberOfLines
|
|
: 0 ... self.numberOfLines - 1
|
|
|
|
for index in ranges { lineDrawingBlock(index) }
|
|
}
|
|
|
|
NSGraphicsContext.restoreGraphicsState()
|
|
|
|
}
|
|
|
|
progressImage.unlockFocus()
|
|
|
|
return progressImage
|
|
|
|
}
|
|
|
|
// MARK: - Animation method
|
|
|
|
/// Override this method to achieve a custom animation.
|
|
/// - Returns: animation which will be put on the progress indicator layer.
|
|
open func keyFrameAnimationForCurrentPreferences() -> CAKeyframeAnimation {
|
|
|
|
var keyFrameValues = [NSNumber]()
|
|
var keyTimeValues = [NSNumber]()
|
|
|
|
if self.steppedAnimation {
|
|
|
|
keyFrameValues.append(0.0)
|
|
for index in 0 ..< self.numberOfLines {
|
|
let value = NSNumber(value: ((-1.0) * Double.pi * (2.0 / Double(self.numberOfLines * index))))
|
|
keyFrameValues.append( value )
|
|
keyFrameValues.append( value )
|
|
}
|
|
keyFrameValues.append( NSNumber(value: (-Double.pi * 2.0)) )
|
|
|
|
keyTimeValues.append(0.0)
|
|
for index in 0 ..< (self.numberOfLines - 1) {
|
|
keyTimeValues.append( NSNumber(value: (1.0 / Double(self.numberOfLines * index))) )
|
|
keyTimeValues.append( NSNumber(value: (1.0 / Double(self.numberOfLines * (index + 1)))) )
|
|
}
|
|
keyTimeValues.append( NSNumber(value: (1.0 / Double(self.numberOfLines * (self.numberOfLines - 1)))) )
|
|
|
|
|
|
} else {
|
|
|
|
keyFrameValues.append( NSNumber(value: (-Double.pi * 0.0)) )
|
|
keyFrameValues.append( NSNumber(value: (-Double.pi * 0.5)) )
|
|
keyFrameValues.append( NSNumber(value: (-Double.pi * 1.0)) )
|
|
keyFrameValues.append( NSNumber(value: (-Double.pi * 1.5)) )
|
|
keyFrameValues.append( NSNumber(value: (-Double.pi * 2.0)) )
|
|
|
|
}
|
|
|
|
let animation = CAKeyframeAnimation(keyPath: Constants.animationKeyPath)
|
|
animation.repeatCount = .greatestFiniteMagnitude
|
|
animation.values = keyFrameValues
|
|
animation.keyTimes = keyTimeValues
|
|
animation.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ)
|
|
animation.duration = self.animationDuration
|
|
animation.beginTime = 1
|
|
|
|
return animation
|
|
}
|
|
|
|
}
|