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) } }