import Foundation import UIKit import Display import AsyncDisplayKit import LegacyComponents import SwiftSignalKit private final class RadialCheckContentNodeParameters: NSObject { let color: UIColor let progress: CGFloat init(color: UIColor, progress: CGFloat) { self.color = color self.progress = progress super.init() } } final class RadialCheckContentNode: RadialStatusContentNode { var color: UIColor { didSet { self.setNeedsDisplay() } } private var effectiveProgress: CGFloat = 1.0 { didSet { self.setNeedsDisplay() } } private var animationCompletionTimer: SwiftSignalKit.Timer? private var isAnimatingProgress: Bool { return self.pop_animation(forKey: "progress") != nil || self.animationCompletionTimer != nil } private var enqueuedReadyForTransition: (() -> Void)? init(color: UIColor) { self.color = color super.init() self.displaysAsynchronously = true self.isOpaque = false self.isLayerBacked = true } func animateProgress(delay: Double) { self.animationCompletionTimer?.invalidate() self.animationCompletionTimer = nil let animation = POPBasicAnimation() animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in property?.readBlock = { node, values in values?.pointee = (node as! RadialCheckContentNode).effectiveProgress } property?.writeBlock = { node, values in (node as! RadialCheckContentNode).effectiveProgress = values!.pointee } property?.threshold = 0.01 }) as! POPAnimatableProperty) animation.fromValue = 0.0 as NSNumber animation.toValue = 1.0 as NSNumber animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) animation.duration = 0.25 animation.beginTime = delay animation.completionBlock = { [weak self] _, _ in if let strongSelf = self { strongSelf.animationCompletionTimer?.invalidate() if let strongSelf = self { strongSelf.animationCompletionTimer = nil if let enqueuedReadyForTransition = strongSelf.enqueuedReadyForTransition { strongSelf.enqueuedReadyForTransition = nil enqueuedReadyForTransition() } } } } self.pop_add(animation, forKey: "progress") } override func enqueueReadyForTransition(_ f: @escaping () -> Void) { if self.isAnimatingProgress { self.enqueuedReadyForTransition = f } else { f() } } override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { return RadialCheckContentNodeParameters(color: self.color, progress: self.effectiveProgress) } @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? RadialCheckContentNodeParameters { let diameter = bounds.size.width let progress = parameters.progress var pathLineWidth: CGFloat = 2.0 if (abs(diameter - 37.0) < 0.1) { pathLineWidth = 2.5 } else if (abs(diameter - 32.0) < 0.1) { pathLineWidth = 2.0 } else { pathLineWidth = 2.5 } let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0) let factor: CGFloat = max(0.3, diameter / 50.0) context.setStrokeColor(parameters.color.cgColor) context.setLineWidth(max(1.7, pathLineWidth * factor)) context.setLineCap(.round) context.setLineJoin(.round) context.setMiterLimit(10.0) let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0)) var s = CGPoint(x: center.x - 10.0 * factor, y: center.y + 1.0 * factor) var p1 = CGPoint(x: 7.0 * factor, y: 7.0 * factor) var p2 = CGPoint(x: 13.0 * factor, y: -15.0 * factor) if diameter < 36.0 { s = CGPoint(x: center.x - 7.0 * factor, y: center.y + 1.0 * factor) p1 = CGPoint(x: 4.5 * factor, y: 4.5 * factor) p2 = CGPoint(x: 10.0 * factor, y: -11.0 * factor) } if !firstSegment.isZero { if firstSegment < 1.0 { context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment)) context.addLine(to: s) } else { let secondSegment = (progress - 0.33) * 1.5 context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment)) context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y)) context.addLine(to: s) } } context.strokePath() } } private let duration: Double = 0.2 override func animateOut(to: RadialStatusNodeState, completion: @escaping () -> Void) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { _ in completion() }) self.layer.animateScale(from: 1.0, to: 0.6, duration: duration, removeOnCompletion: false) } override func animateIn(from: RadialStatusNodeState, delay: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, delay: delay) self.layer.animateScale(from: 0.7, to: 1.0, duration: duration, delay: delay) self.animateProgress(delay: delay) } }