import Foundation import UIKit import AsyncDisplayKit import Display import LegacyComponents public struct CheckNodeTheme { public let backgroundColor: UIColor public let strokeColor: UIColor public let borderColor: UIColor public let hasShadow: Bool public init(backgroundColor: UIColor, strokeColor: UIColor, borderColor: UIColor, hasShadow: Bool) { self.backgroundColor = backgroundColor self.strokeColor = strokeColor self.borderColor = borderColor self.hasShadow = hasShadow } } public enum CheckNodeContent { case check case counter(Int) } private final class CheckNodeParameters: NSObject { let theme: CheckNodeTheme let content: CheckNodeContent let animationProgress: CGFloat let selected: Bool init(theme: CheckNodeTheme, content: CheckNodeContent, animationProgress: CGFloat, selected: Bool) { self.theme = theme self.content = content self.animationProgress = animationProgress self.selected = selected } } public class ModernCheckNode: ASDisplayNode { private var animationProgress: CGFloat = 0.0 public var theme: CheckNodeTheme { didSet { self.setNeedsDisplay() } } public init(theme: CheckNodeTheme, content: CheckNodeContent = .check) { self.theme = theme self.content = content super.init() self.isOpaque = false } public var content: CheckNodeContent { didSet { self.setNeedsDisplay() } } public var selected = false public func setSelected(_ selected: Bool, animated: Bool = false) { guard self.selected != selected else { return } self.selected = selected if selected && animated { let animation = POPBasicAnimation() animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in property?.readBlock = { node, values in values?.pointee = (node as! ModernCheckNode).animationProgress } property?.writeBlock = { node, values in (node as! ModernCheckNode).animationProgress = values!.pointee (node as! ModernCheckNode).setNeedsDisplay() } 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.21 self.pop_add(animation, forKey: "progress") } else { self.pop_removeAllAnimations() self.animationProgress = selected ? 1.0 : 0.0 self.setNeedsDisplay() } } public func setHighlighted(_ highlighted: Bool, animated: Bool = false) { } override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { return CheckNodeParameters(theme: self.theme, content: self.content, animationProgress: self.animationProgress, selected: self.selected) } @objc override public 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? CheckNodeParameters { let progress = parameters.animationProgress let diameter = bounds.width let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0) var borderWidth: CGFloat = 1.5 if UIScreenScale == 3.0 { borderWidth = 5.0 / 3.0 } context.setStrokeColor(parameters.theme.borderColor.cgColor) context.setLineWidth(borderWidth) context.strokeEllipse(in: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0)) context.setFillColor(parameters.theme.backgroundColor.cgColor) context.fillEllipse(in: bounds.insetBy(dx: (diameter - borderWidth) * (1.0 - parameters.animationProgress), dy: (diameter - borderWidth) * (1.0 - parameters.animationProgress))) let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0)) let s = CGPoint(x: center.x - 4.0, y: center.y + UIScreenPixel) let p1 = CGPoint(x: 3.0, y: 3.0) let p2 = CGPoint(x: 5.0, y: -6.0) 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.setStrokeColor(parameters.theme.strokeColor.cgColor) if parameters.theme.strokeColor == .clear { context.setBlendMode(.clear) } context.setLineWidth(borderWidth) context.setLineCap(.round) context.setLineJoin(.round) context.setMiterLimit(10.0) context.strokePath() } } override public func touchesBegan(_ touches: Set, with event: UIEvent?) { super.touchesBegan(touches, with: event) } override public func touchesEnded(_ touches: Set, with event: UIEvent?) { super.touchesEnded(touches, with: event) } override public func touchesCancelled(_ touches: Set?, with event: UIEvent?) { super.touchesCancelled(touches, with: event) } }