import Foundation import UIKit import Display import AsyncDisplayKit import AppBundle private let innerCircleDiameter: CGFloat = 110.0 private let outerCircleDiameter = innerCircleDiameter + 50.0 private let outerCircleMinScale = innerCircleDiameter / outerCircleDiameter private let innerCircleImage = generateFilledCircleImage(diameter: innerCircleDiameter, color: UIColor(rgb: 0x007ee5)) private let outerCircleImage = generateFilledCircleImage(diameter: outerCircleDiameter, color: UIColor(rgb: 0x007ee5, alpha: 0.2)) private let micIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: .white)! private final class ChatTextInputAudioRecordingOverlayDisplayLinkTarget: NSObject { private let f: () -> Void init(_ f: @escaping () -> Void) { self.f = f super.init() } @objc func displayLinkEvent() { self.f() } } final class ChatTextInputAudioRecordingOverlay { private weak var anchorView: UIView? private let containerNode: ASDisplayNode private let circleContainerNode: ASDisplayNode private let innerCircleNode: ASImageNode private let outerCircleNode: ASImageNode private let iconNode: ASImageNode var animationStartTime: Double? var displayLink: CADisplayLink? var currentLevel: CGFloat = 0.0 var inputLevel: CGFloat = 0.0 var animatedIn = false var dismissFactor: CGFloat = 1.0 { didSet { let scale = max(0.3, min(self.dismissFactor, 1.0)) self.circleContainerNode.transform = CATransform3DMakeScale(scale, scale, 1.0) } } init(anchorView: UIView) { self.anchorView = anchorView self.containerNode = ASDisplayNode() self.containerNode.isLayerBacked = true self.circleContainerNode = ASDisplayNode() self.circleContainerNode.isLayerBacked = true self.outerCircleNode = ASImageNode() self.outerCircleNode.displayWithoutProcessing = true self.outerCircleNode.displaysAsynchronously = false self.outerCircleNode.isLayerBacked = true self.outerCircleNode.image = outerCircleImage self.outerCircleNode.frame = CGRect(origin: CGPoint(x: -outerCircleDiameter / 2.0, y: -outerCircleDiameter / 2.0), size: CGSize(width: outerCircleDiameter, height: outerCircleDiameter)) self.innerCircleNode = ASImageNode() self.innerCircleNode.displayWithoutProcessing = true self.innerCircleNode.displaysAsynchronously = false self.innerCircleNode.isLayerBacked = true self.innerCircleNode.image = innerCircleImage self.innerCircleNode.frame = CGRect(origin: CGPoint(x: -innerCircleDiameter / 2.0, y: -innerCircleDiameter / 2.0), size: CGSize(width: innerCircleDiameter, height: innerCircleDiameter)) self.iconNode = ASImageNode() self.iconNode.displayWithoutProcessing = true self.iconNode.displaysAsynchronously = false self.iconNode.isLayerBacked = true self.iconNode.image = micIcon self.iconNode.frame = CGRect(origin: CGPoint(x: -micIcon.size.width / 2.0, y: -micIcon.size.height / 2.0), size: micIcon.size) self.circleContainerNode.addSubnode(self.outerCircleNode) self.circleContainerNode.addSubnode(self.innerCircleNode) self.containerNode.addSubnode(self.circleContainerNode) self.containerNode.addSubnode(self.iconNode) } deinit { self.displayLink?.invalidate() } func present(in window: UIWindow) { if let anchorView = self.anchorView, let anchorSuperview = anchorView.superview { if let displayLink = self.displayLink { displayLink.invalidate() } self.displayLink = CADisplayLink(target: ChatTextInputAudioRecordingOverlayDisplayLinkTarget({ [weak self] in self?.displayLinkEvent() }), selector: #selector(ChatTextInputAudioRecordingOverlayDisplayLinkTarget.displayLinkEvent)) let convertedCenter = anchorSuperview.convert(anchorView.center, to: window) self.containerNode.position = CGPoint(x: convertedCenter.x, y: convertedCenter.y) window.addSubnode(self.containerNode) self.innerCircleNode.layer.animateSpring(from: 0.2 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) self.outerCircleNode.layer.transform = CATransform3DMakeScale(outerCircleMinScale, outerCircleMinScale, 1.0) self.outerCircleNode.layer.animateSpring(from: 0.2 as NSNumber, to: outerCircleMinScale as NSNumber, keyPath: "transform.scale", duration: 0.5) self.innerCircleNode.layer.animateAlpha(from: 0.2, to: 1.0, duration: 0.15) self.outerCircleNode.layer.animateAlpha(from: 0.2, to: 1.0, duration: 0.15) self.iconNode.layer.animateAlpha(from: 0.2, to: 1.0, duration: 0.15) self.animatedIn = true self.animationStartTime = CACurrentMediaTime() self.displayLink?.add(to: RunLoop.main, forMode: .common) self.displayLink?.isPaused = false } } func dismiss() { self.displayLink?.invalidate() self.displayLink = nil var innerCompleted = false var outerCompleted = false var iconCompleted = false var containerNodeRef: ASDisplayNode? = self.containerNode let completion: () -> Void = { if let containerNode = containerNodeRef, innerCompleted, outerCompleted, iconCompleted { containerNode.removeFromSupernode() containerNodeRef = nil } } self.innerCircleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false) self.innerCircleNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.18, removeOnCompletion: false, completion: { _ in innerCompleted = true completion() }) var currentScaleValue: CGFloat = outerCircleMinScale if let currentScale = self.outerCircleNode.layer.value(forKeyPath: "transform.scale") as? AnyObject, currentScale.responds(to: Selector("floatValue")) { currentScaleValue = CGFloat(currentScale.floatValue) } self.outerCircleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false) self.outerCircleNode.layer.animateScale(from: currentScaleValue, to: 0.2, duration: 0.18, removeOnCompletion: false, completion: { _ in outerCompleted = true completion() }) self.iconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { _ in iconCompleted = true completion() }) } private func displayLinkEvent() { let t = CACurrentMediaTime() if let animationStartTime = self.animationStartTime { if t > animationStartTime + 0.5 { self.currentLevel = self.currentLevel * 0.8 + self.inputLevel * 0.2 let scale = outerCircleMinScale + self.currentLevel * (1.0 - outerCircleMinScale) self.outerCircleNode.transform = CATransform3DMakeScale(scale, scale, 1.0) } } } func addImmediateMicLevel(_ level: CGFloat) { self.inputLevel = level } }