217 lines
7.2 KiB
Swift
217 lines
7.2 KiB
Swift
//
|
|
// ZIndefiniteAnimatedSpinner.swift
|
|
// 武极天下
|
|
//
|
|
// Created by zhl on 2021/7/22.
|
|
// Copyright © 2021 egret. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
public class ZIndefiniteAnimatedSpinner: UIView {
|
|
|
|
/// A structure is named "AnimationKey".
|
|
private struct AnimationKey {
|
|
static let stroke = "spinner.animkey.stroke"
|
|
static let rotation = "spinner.animkey.rotation"
|
|
}
|
|
|
|
/// The property indicates whether the view is currently animating.
|
|
public private(set) var isAnimating: Bool = false
|
|
|
|
/// Sets whether the view is hidden when not animating.
|
|
public var hidesWhenStopped: Bool = true
|
|
|
|
/// Specifies the timing function to use for the control's animation. Defaults to kCAMediaTimingFunctionEaseInEaseOut.
|
|
public var timingFunction: CAMediaTimingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
|
|
|
|
/// A layer that draws a arc progress in its coordinate space.
|
|
private lazy var progressLayer: CAShapeLayer = {
|
|
let layer = CAShapeLayer()
|
|
|
|
layer.strokeColor = nil
|
|
layer.fillColor = nil
|
|
layer.lineWidth = 1.0
|
|
|
|
return layer
|
|
}()
|
|
|
|
/// Sets the line width of the spinner's circle.
|
|
public var lineWidth: CGFloat {
|
|
get {
|
|
return self.progressLayer.lineWidth
|
|
}
|
|
|
|
set {
|
|
self.progressLayer.lineWidth = newValue
|
|
self.updatePath()
|
|
}
|
|
}
|
|
|
|
/// Sets the line color of the spinner's circle.
|
|
public var lineColor: UIColor? {
|
|
get {
|
|
guard let color = self.progressLayer.strokeColor else {
|
|
return nil
|
|
}
|
|
|
|
return UIColor.init(cgColor: color)
|
|
}
|
|
|
|
set (newColor) {
|
|
self.progressLayer.strokeColor = newColor?.cgColor
|
|
}
|
|
}
|
|
|
|
public override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
self.setup()
|
|
}
|
|
|
|
public required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
// Supports an Interface Builder archive, or nib file.
|
|
}
|
|
|
|
public override func awakeFromNib() {
|
|
super.awakeFromNib()
|
|
self.setup()
|
|
}
|
|
|
|
private func setup() {
|
|
self.layer.addSublayer(self.progressLayer)
|
|
|
|
let selector = #selector(ZIndefiniteAnimatedSpinner.resetAnimations)
|
|
let name = UIApplication.didBecomeActiveNotification
|
|
NotificationCenter.default.addObserver(self, selector: selector, name: name, object: nil)
|
|
}
|
|
|
|
/// Starts animation of the spinner.
|
|
public func startAnimating() {
|
|
if self.isAnimating {
|
|
return
|
|
}
|
|
|
|
self.isAnimating = true
|
|
self.addLayerAnimations()
|
|
self.isHidden = false
|
|
}
|
|
|
|
/// Stops animation of the spinnner.
|
|
public func stopAnimating() {
|
|
if !self.isAnimating {
|
|
return
|
|
}
|
|
|
|
self.isAnimating = false
|
|
|
|
self.progressLayer.removeAnimation(forKey: AnimationKey.rotation)
|
|
self.progressLayer.removeAnimation(forKey: AnimationKey.stroke)
|
|
|
|
if self.hidesWhenStopped {
|
|
self.isHidden = true
|
|
}
|
|
}
|
|
|
|
private func addLayerAnimations() {
|
|
let animation = CABasicAnimation()
|
|
animation.keyPath = "transform.rotation"
|
|
animation.duration = 2.0
|
|
animation.fromValue = NSNumber(value: 0.0)
|
|
animation.toValue = NSNumber(value: 2*Double.pi)
|
|
animation.repeatCount = Float.infinity
|
|
self.progressLayer.add(animation, forKey: AnimationKey.rotation)
|
|
|
|
let headAnimation = CABasicAnimation()
|
|
headAnimation.keyPath = "strokeStart"
|
|
headAnimation.duration = 1.0
|
|
headAnimation.fromValue = NSNumber(value: 0.0)
|
|
headAnimation.toValue = NSNumber(value: 0.25)
|
|
headAnimation.timingFunction = self.timingFunction
|
|
|
|
let tailAnimation = CABasicAnimation()
|
|
tailAnimation.keyPath = "strokeEnd"
|
|
tailAnimation.duration = 1.0
|
|
tailAnimation.fromValue = NSNumber(value: 0.0)
|
|
tailAnimation.toValue = NSNumber(value: 1.0)
|
|
tailAnimation.timingFunction = self.timingFunction
|
|
|
|
let endHeadAnimation = CABasicAnimation()
|
|
endHeadAnimation.keyPath = "strokeStart"
|
|
endHeadAnimation.beginTime = 1.0
|
|
endHeadAnimation.duration = 0.5
|
|
endHeadAnimation.fromValue = NSNumber(value: 0.25)
|
|
endHeadAnimation.toValue = NSNumber(value: 1.0)
|
|
endHeadAnimation.timingFunction = self.timingFunction
|
|
|
|
let endTailAnimation = CABasicAnimation()
|
|
endTailAnimation.keyPath = "strokeEnd"
|
|
endTailAnimation.beginTime = 1.0
|
|
endTailAnimation.duration = 0.5
|
|
endTailAnimation.fromValue = NSNumber(value: 1.0)
|
|
endTailAnimation.toValue = NSNumber(value: 1.0)
|
|
endTailAnimation.timingFunction = self.timingFunction
|
|
|
|
let animGroup = CAAnimationGroup()
|
|
animGroup.repeatCount = Float.infinity
|
|
animGroup.duration = 1.5
|
|
animGroup.animations = [headAnimation,
|
|
tailAnimation,
|
|
endHeadAnimation,
|
|
endTailAnimation]
|
|
self.progressLayer.add(animGroup, forKey: AnimationKey.stroke)
|
|
}
|
|
|
|
@objc private func resetAnimations() {
|
|
if self.isAnimating {
|
|
self.stopAnimating()
|
|
self.startAnimating()
|
|
}
|
|
}
|
|
|
|
public override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
let sW = self.bounds.size.width
|
|
let sH = self.bounds.size.height
|
|
self.progressLayer.frame = CGRect(x: 0, y: 0, width: sW, height: sH)
|
|
|
|
self.updatePath()
|
|
}
|
|
|
|
private func updatePath() {
|
|
let sW = self.bounds.size.width
|
|
let sH = self.bounds.size.height
|
|
|
|
let center = CGPoint(x: sW/2, y: sH/2)
|
|
let radius = min(sW/2, sH/2) - self.lineWidth/2
|
|
let startAngle = CGFloat(0.0)
|
|
let endAngle = CGFloat(2*Double.pi)
|
|
|
|
let path = UIBezierPath(arcCenter: center,
|
|
radius: radius,
|
|
startAngle: startAngle,
|
|
endAngle: endAngle,
|
|
clockwise: true)
|
|
self.progressLayer.path = path.cgPath
|
|
|
|
self.progressLayer.strokeStart = 0.0
|
|
self.progressLayer.strokeEnd = 0.0
|
|
}
|
|
|
|
private func executeWhenReleasing() {
|
|
self.stopAnimating()
|
|
|
|
let name = UIApplication.didBecomeActiveNotification
|
|
NotificationCenter.default.removeObserver(self, name: name, object: nil)
|
|
}
|
|
|
|
deinit {
|
|
#if DEBUG
|
|
print("[\((#file as NSString).lastPathComponent):\(#function)]")
|
|
#endif
|
|
self.executeWhenReleasing()
|
|
}
|
|
|
|
}
|