import Foundation import UIKit import AsyncDisplayKit import Display import LegacyComponents private final class PasscodeLockIconNodeParameters: NSObject { let unlockedColor: UIColor let lockedColor: UIColor let progress: CGFloat let fromScale: CGFloat let keepLockedColor: Bool init(unlockedColor: UIColor, lockedColor: UIColor, progress: CGFloat, fromScale: CGFloat, keepLockedColor: Bool) { self.unlockedColor = unlockedColor self.lockedColor = lockedColor self.progress = progress self.fromScale = fromScale self.keepLockedColor = keepLockedColor super.init() } } final class PasscodeLockIconNode: ASDisplayNode { var unlockedColor: UIColor = .black { didSet { self.setNeedsDisplay() } } private var effectiveProgress: CGFloat = 1.0 { didSet { self.setNeedsDisplay() } } private var fromScale: CGFloat = 1.0 private var keepLockedColor = false override init() { super.init() self.isOpaque = false self.backgroundColor = .clear } func animateIn(fromScale: CGFloat = 1.0) { self.fromScale = fromScale self.pop_removeAllAnimations() let animation = POPBasicAnimation() animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in property?.readBlock = { node, values in values?.pointee = (node as! PasscodeLockIconNode).effectiveProgress } property?.writeBlock = { node, values in (node as! PasscodeLockIconNode).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.55 self.pop_add(animation, forKey: "progress") } func animateUnlock() { self.fromScale = 1.0 self.keepLockedColor = true self.pop_removeAllAnimations() let animation = POPBasicAnimation() animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in property?.readBlock = { node, values in values?.pointee = (node as! PasscodeLockIconNode).effectiveProgress } property?.writeBlock = { node, values in (node as! PasscodeLockIconNode).effectiveProgress = values!.pointee } property?.threshold = 0.01 }) as! POPAnimatableProperty) animation.fromValue = 1.0 as NSNumber animation.toValue = 0.0 as NSNumber animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) animation.duration = 0.75 self.pop_add(animation, forKey: "progress") } override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { return PasscodeLockIconNodeParameters(unlockedColor: self.unlockedColor, lockedColor: .white, progress: self.effectiveProgress, fromScale: self.fromScale, keepLockedColor: self.keepLockedColor) } @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) } guard let parameters = parameters as? PasscodeLockIconNodeParameters else { return } let progress = parameters.progress let fromScale = parameters.fromScale let lockSpan: CGFloat = parameters.keepLockedColor ? 0.5 : 0.85 let lockProgress = min(1.0, progress / lockSpan) context.translateBy(x: bounds.width / 2.0, y: bounds.height / 2.0) context.scaleBy(x: fromScale + (1.0 - fromScale) * lockProgress, y: fromScale + (1.0 - fromScale) * lockProgress) context.translateBy(x: -bounds.width / 2.0, y: -bounds.height / 2.0) let color = parameters.keepLockedColor ? parameters.lockedColor : parameters.unlockedColor.mixedWith(parameters.lockedColor, alpha: progress) context.setStrokeColor(color.cgColor) let lineWidth: CGFloat = 3.0 context.setLineWidth(lineWidth) var topRect: CGRect var topRadius: CGFloat var offset: CGFloat = 0.0 if lockProgress < 0.5 { topRect = CGRect(x: 19.0, y: lineWidth / 2.0 + 1.0, width: 14.0 * (0.5 - lockProgress) / 0.5, height: 22.0) topRadius = 6.0 * (0.5 - lockProgress) * 2.0 } else { let width = 14.0 * (lockProgress - 0.5) * 2.0 topRect = CGRect(x: 19.0 - width, y: lineWidth / 2.0 + 1.0, width: width, height: 22.0) topRadius = 6.0 * (lockProgress - 0.5) * 2.0 } if progress > lockSpan { let innerProgress = (progress - lockSpan) / (1.0 - lockSpan) if !parameters.keepLockedColor { if innerProgress < 0.6 { offset = 2.0 * min(1.0, innerProgress / 0.6) } else { offset = 2.0 * min(1.0, max(0.0, (1.0 - innerProgress) / 0.4)) } } topRect.origin.y += 4.0 * min(1.0, max(0.0, innerProgress / 0.6)) + offset } let topPath = UIBezierPath(roundedRect: topRect, cornerRadius: topRadius) context.addPath(topPath.cgPath) context.strokePath() var clearRect: CGRect if lockProgress < 0.5 { clearRect = CGRect(x: topRect.minX + lineWidth, y: topRect.minY + 11.0, width: 14.0, height: 22.0) } else { clearRect = CGRect(x: topRect.maxX - 14.0 - lineWidth, y: topRect.minY + 11.0, width: 14.0, height: 22.0) } context.setBlendMode(.clear) context.clear(clearRect) context.setBlendMode(.normal) context.setFillColor(color.cgColor) let basePath = UIBezierPath(roundedRect: CGRect(x: 0.0, y: bounds.height - 21.0 + offset, width: 24.0, height: 19.0), cornerRadius: 3.5) context.addPath(basePath.cgPath) context.fillPath() } }