Swiftgram/submodules/RadialStatusNode/Sources/RadialCloudProgressContentNode.swift

303 lines
11 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
private final class RadialCloudProgressContentCancelNodeParameters: NSObject {
let color: UIColor
init(color: UIColor) {
self.color = color
}
}
private final class RadialCloudProgressContentSpinnerNodeParameters: NSObject {
let color: UIColor
let backgroundStrokeColor: UIColor
let progress: CGFloat
let lineWidth: CGFloat?
init(color: UIColor, backgroundStrokeColor: UIColor, progress: CGFloat, lineWidth: CGFloat?) {
self.color = color
self.backgroundStrokeColor = backgroundStrokeColor
self.progress = progress
self.lineWidth = lineWidth
}
}
private final class RadialCloudProgressContentSpinnerNode: ASDisplayNode {
var progressAnimationCompleted: (() -> Void)?
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
var backgroundStrokeColor: UIColor {
didSet {
self.setNeedsDisplay()
}
}
private var effectiveProgress: CGFloat = 0.0 {
didSet {
self.setNeedsDisplay()
}
}
var progress: CGFloat? {
didSet {
self.pop_removeAnimation(forKey: "progress")
if let progress = self.progress {
self.pop_removeAnimation(forKey: "indefiniteProgress")
let animation = POPBasicAnimation()
animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! RadialCloudProgressContentSpinnerNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! RadialCloudProgressContentSpinnerNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
animation.fromValue = CGFloat(self.effectiveProgress) as NSNumber
animation.toValue = CGFloat(progress) as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 0.2
animation.completionBlock = { [weak self] _, _ in
self?.progressAnimationCompleted?()
}
self.pop_add(animation, forKey: "progress")
} else if self.pop_animation(forKey: "indefiniteProgress") == nil {
let animation = POPBasicAnimation()
animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! RadialCloudProgressContentSpinnerNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! RadialCloudProgressContentSpinnerNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
animation.fromValue = CGFloat(0.0) as NSNumber
animation.toValue = CGFloat(2.0) as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 2.5
animation.repeatForever = true
self.pop_add(animation, forKey: "indefiniteProgress")
}
}
}
var isAnimatingProgress: Bool {
return self.pop_animation(forKey: "progress") != nil
}
let lineWidth: CGFloat?
init(color: UIColor, backgroundStrokeColor: UIColor, lineWidth: CGFloat?) {
self.color = color
self.backgroundStrokeColor = backgroundStrokeColor
self.lineWidth = lineWidth
super.init()
self.isLayerBacked = true
self.displaysAsynchronously = true
self.isOpaque = false
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return RadialCloudProgressContentSpinnerNodeParameters(color: self.color, backgroundStrokeColor: self.backgroundStrokeColor, progress: self.effectiveProgress, lineWidth: self.lineWidth)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? RadialCloudProgressContentSpinnerNodeParameters {
let factor = bounds.size.width / 50.0
var progress = parameters.progress
var startAngle = -CGFloat.pi / 2.0
var endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
if progress > 1.0 {
progress = 2.0 - progress
let tmp = startAngle
startAngle = endAngle
endAngle = tmp
}
progress = min(1.0, progress)
let lineWidth: CGFloat = parameters.lineWidth ?? max(1.6, 2.25 * factor)
let pathDiameter: CGFloat
if parameters.lineWidth != nil {
pathDiameter = bounds.size.width - lineWidth
} else {
pathDiameter = bounds.size.width - lineWidth - 2.5 * 2.0
}
context.setStrokeColor(parameters.backgroundStrokeColor.cgColor)
let backgroundPath = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: 0.0, endAngle: 2.0 * CGFloat.pi, clockwise:true)
backgroundPath.lineWidth = lineWidth
backgroundPath.stroke()
context.setStrokeColor(parameters.color.cgColor)
let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise:true)
path.lineWidth = lineWidth
path.lineCapStyle = .round
path.stroke()
}
}
override func willEnterHierarchy() {
super.willEnterHierarchy()
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
basicAnimation.duration = 2.0
basicAnimation.fromValue = NSNumber(value: Float(0.0))
basicAnimation.toValue = NSNumber(value: Float.pi * 2.0)
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
basicAnimation.beginTime = 1.0
self.layer.add(basicAnimation, forKey: "progressRotation")
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.layer.removeAnimation(forKey: "progressRotation")
}
}
private final class RadialCloudProgressContentCancelNode: ASDisplayNode {
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
init(color: UIColor) {
self.color = color
super.init()
self.isLayerBacked = true
self.displaysAsynchronously = true
self.isOpaque = false
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return RadialCloudProgressContentCancelNodeParameters(color: self.color)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? RadialCloudProgressContentCancelNodeParameters {
let size: CGFloat = 8.0
context.setFillColor(parameters.color.cgColor)
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: floor((bounds.size.width - size) / 2.0), y: floor((bounds.size.height - size) / 2.0)), size: CGSize(width: size, height: size)), cornerRadius: 2.0)
path.fill()
}
}
}
final class RadialCloudProgressContentNode: RadialStatusContentNode {
private let spinnerNode: RadialCloudProgressContentSpinnerNode
private let cancelNode: RadialCloudProgressContentCancelNode
var color: UIColor {
didSet {
self.setNeedsDisplay()
self.spinnerNode.color = self.color
}
}
var backgroundStrokeColor: UIColor {
didSet {
self.setNeedsDisplay()
self.spinnerNode.backgroundStrokeColor = self.backgroundStrokeColor
}
}
var progress: CGFloat? = 0.0 {
didSet {
self.spinnerNode.progress = self.progress
}
}
private var enqueuedReadyForTransition: (() -> Void)?
init(color: UIColor, backgroundStrokeColor: UIColor, lineWidth: CGFloat?) {
self.color = color
self.backgroundStrokeColor = backgroundStrokeColor
self.spinnerNode = RadialCloudProgressContentSpinnerNode(color: color, backgroundStrokeColor: backgroundStrokeColor, lineWidth: lineWidth)
self.cancelNode = RadialCloudProgressContentCancelNode(color: color)
super.init()
self.isLayerBacked = true
self.addSubnode(self.spinnerNode)
self.addSubnode(self.cancelNode)
self.spinnerNode.progressAnimationCompleted = { [weak self] in
if let strongSelf = self {
if let enqueuedReadyForTransition = strongSelf.enqueuedReadyForTransition {
strongSelf.enqueuedReadyForTransition = nil
enqueuedReadyForTransition()
}
}
}
}
override func enqueueReadyForTransition(_ f: @escaping () -> Void) {
if self.spinnerNode.isAnimatingProgress && self.progress == 1.0 {
self.enqueuedReadyForTransition = f
} else {
f()
}
}
override func layout() {
super.layout()
let bounds = self.bounds
self.spinnerNode.bounds = bounds
self.spinnerNode.position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
self.cancelNode.frame = bounds
}
override func animateOut(to: RadialStatusNodeState, completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
completion()
})
self.cancelNode.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false)
}
override func animateIn(from: RadialStatusNodeState, delay: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
self.cancelNode.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15, delay: delay)
}
}