import Foundation import UIKit import AsyncDisplayKit import Display private final class VoiceChatSpeakerNodeDrawingState: NSObject { let color: UIColor let transition: CGFloat let reverse: Bool init(color: UIColor, transition: CGFloat, reverse: Bool) { self.color = color self.transition = transition self.reverse = reverse super.init() } } private func generateWaveImage(color: UIColor, num: Int) -> UIImage? { return generateImage(CGSize(width: 36.0, height: 36.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setStrokeColor(color.cgColor) context.setLineWidth(1.0 + UIScreenPixel) context.setLineCap(.round) context.translateBy(x: 6.0, y: 6.0) switch num { case 1: let _ = try? drawSvgPath(context, path: "M15,9 C15.6666667,9.95023099 16,10.9487504 16,11.9955581 C16,13.0423659 15.6666667,14.0438465 15,15 S ") case 2: let _ = try? drawSvgPath(context, path: "M17.5,6.5 C18.8724771,8.24209014 19.5587156,10.072709 19.5587156,11.9918565 C19.5587156,13.9110041 18.8724771,15.7470519 17.5,17.5 S ") case 3: let _ = try? drawSvgPath(context, path: "M20,3.5 C22,6.19232113 23,9.02145934 23,11.9874146 C23,14.9533699 22,17.7908984 20,20.5 S ") default: break } }) } final class VoiceChatSpeakerNode: ASDisplayNode { class State: Equatable { enum Value: Equatable { case muted case low case medium case high } let value: Value let color: UIColor init(value: Value, color: UIColor) { self.value = value self.color = color } static func ==(lhs: State, rhs: State) -> Bool { if lhs.value != rhs.value { return false } if lhs.color.argb != rhs.color.argb { return false } return true } } private var hasState = false private var state: State = State(value: .medium, color: .black) private let iconNode: IconNode private let waveNode1: ASImageNode private let waveNode2: ASImageNode private let waveNode3: ASImageNode override init() { self.iconNode = IconNode() self.waveNode1 = ASImageNode() self.waveNode1.displaysAsynchronously = false self.waveNode1.displayWithoutProcessing = true self.waveNode2 = ASImageNode() self.waveNode2.displaysAsynchronously = false self.waveNode2.displayWithoutProcessing = true self.waveNode3 = ASImageNode() self.waveNode3.displaysAsynchronously = false self.waveNode3.displayWithoutProcessing = true super.init() self.addSubnode(self.iconNode) self.addSubnode(self.waveNode1) self.addSubnode(self.waveNode2) self.addSubnode(self.waveNode3) } private var animating = false func update(state: State, animated: Bool, force: Bool = false) { var animated = animated if !self.hasState { self.hasState = true animated = false } if self.state != state || force { let previousState = self.state self.state = state if animated && self.animating { return } if previousState.color != state.color { self.waveNode1.image = generateWaveImage(color: state.color, num: 1) self.waveNode2.image = generateWaveImage(color: state.color, num: 2) self.waveNode3.image = generateWaveImage(color: state.color, num: 3) } self.update(transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, completion: { if self.state != state { self.update(state: self.state, animated: animated, force: true) } }) } } private func update(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) { self.animating = transition.isAnimated self.iconNode.update(state: IconNode.State(muted: self.state.value == .muted, color: self.state.color), animated: transition.isAnimated) let bounds = self.bounds let center = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0) self.iconNode.bounds = CGRect(origin: CGPoint(), size: bounds.size) self.waveNode1.bounds = CGRect(origin: CGPoint(), size: bounds.size) self.waveNode2.bounds = CGRect(origin: CGPoint(), size: bounds.size) self.waveNode3.bounds = CGRect(origin: CGPoint(), size: bounds.size) let iconPosition: CGPoint let wave1Position: CGPoint var wave1Alpha: CGFloat = 1.0 let wave2Position: CGPoint var wave2Alpha: CGFloat = 1.0 let wave3Position: CGPoint var wave3Alpha: CGFloat = 1.0 switch self.state.value { case .muted: iconPosition = CGPoint(x: center.x, y: center.y) wave1Position = CGPoint(x: center.x + 4.0, y: center.y) wave2Position = CGPoint(x: center.x + 4.0, y: center.y) wave3Position = CGPoint(x: center.x + 4.0, y: center.y) wave1Alpha = 0.0 wave2Alpha = 0.0 wave3Alpha = 0.0 case .low: iconPosition = CGPoint(x: center.x - 1.0, y: center.y) wave1Position = CGPoint(x: center.x + 3.0, y: center.y) wave2Position = CGPoint(x: center.x + 3.0, y: center.y) wave3Position = CGPoint(x: center.x + 3.0, y: center.y) wave2Alpha = 0.0 wave3Alpha = 0.0 case .medium: iconPosition = CGPoint(x: center.x - 3.0, y: center.y) wave1Position = CGPoint(x: center.x + 1.0, y: center.y) wave2Position = CGPoint(x: center.x + 1.0, y: center.y) wave3Position = CGPoint(x: center.x + 1.0, y: center.y) wave3Alpha = 0.0 case .high: iconPosition = CGPoint(x: center.x - 4.0, y: center.y) wave1Position = CGPoint(x: center.x, y: center.y) wave2Position = CGPoint(x: center.x, y: center.y) wave3Position = CGPoint(x: center.x, y: center.y) } transition.updatePosition(node: self.iconNode, position: iconPosition) { _ in self.animating = false completion() } transition.updatePosition(node: self.waveNode1, position: wave1Position) transition.updatePosition(node: self.waveNode2, position: wave2Position) transition.updatePosition(node: self.waveNode3, position: wave3Position) transition.updateAlpha(node: self.waveNode1, alpha: wave1Alpha) transition.updateAlpha(node: self.waveNode2, alpha: wave2Alpha) transition.updateAlpha(node: self.waveNode3, alpha: wave3Alpha) } } private class IconNode: ASDisplayNode { class State: Equatable { let muted: Bool let color: UIColor init(muted: Bool, color: UIColor) { self.muted = muted self.color = color } static func ==(lhs: State, rhs: State) -> Bool { if lhs.muted != rhs.muted { return false } if lhs.color.argb != rhs.color.argb { return false } return true } } private class TransitionContext { let startTime: Double let duration: Double let previousState: State init(startTime: Double, duration: Double, previousState: State) { self.startTime = startTime self.duration = duration self.previousState = previousState } } private var animator: ConstantDisplayLinkAnimator? private var hasState = false private var state: State = State(muted: false, color: .black) private var transitionContext: TransitionContext? override init() { super.init() self.isOpaque = false } func update(state: State, animated: Bool) { var animated = animated if !self.hasState { self.hasState = true animated = false } if self.state != state { let previousState = self.state self.state = state if animated { self.transitionContext = TransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousState: previousState) } self.updateAnimations() self.setNeedsDisplay() } } private func updateAnimations() { var animate = false let timestamp = CACurrentMediaTime() if let transitionContext = self.transitionContext { if transitionContext.startTime + transitionContext.duration < timestamp { self.transitionContext = nil } else { animate = true } } if animate { let animator: ConstantDisplayLinkAnimator if let current = self.animator { animator = current } else { animator = ConstantDisplayLinkAnimator(update: { [weak self] in self?.updateAnimations() }) self.animator = animator } animator.isPaused = false } else { self.animator?.isPaused = true } self.setNeedsDisplay() } override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { var transitionFraction: CGFloat = self.state.muted ? 1.0 : 0.0 var color = self.state.color var reverse = false if let transitionContext = self.transitionContext { let timestamp = CACurrentMediaTime() var t = CGFloat((timestamp - transitionContext.startTime) / transitionContext.duration) t = min(1.0, max(0.0, t)) if transitionContext.previousState.muted != self.state.muted { transitionFraction = self.state.muted ? t : 1.0 - t reverse = transitionContext.previousState.muted } if transitionContext.previousState.color.rgb != color.rgb { color = transitionContext.previousState.color.interpolateTo(color, fraction: t)! } } return VoiceChatSpeakerNodeDrawingState(color: color, transition: transitionFraction, reverse: reverse) } @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) } guard let parameters = parameters as? VoiceChatSpeakerNodeDrawingState else { return } let clearLineWidth: CGFloat = 4.0 let lineWidth: CGFloat = 1.0 + UIScreenPixel context.setFillColor(parameters.color.cgColor) context.setStrokeColor(parameters.color.cgColor) context.setLineWidth(lineWidth) context.translateBy(x: 7.0, y: 6.0) let _ = try? drawSvgPath(context, path: "M7,9 L10,9 L13.6080479,5.03114726 C13.9052535,4.70422117 14.4112121,4.6801279 14.7381382,4.97733344 C14.9049178,5.12895118 15,5.34388952 15,5.5692855 L15,18.4307145 C15,18.8725423 14.6418278,19.2307145 14.2,19.2307145 C13.974604,19.2307145 13.7596657,19.1356323 13.6080479,18.9688527 L10,15 L7,15 C6.44771525,15 6,14.5522847 6,14 L6,10 C6,9.44771525 6.44771525,9 7,9 S ") context.translateBy(x: -7.0, y: -6.0) if parameters.transition > 0.0 { let startPoint: CGPoint let endPoint: CGPoint let origin: CGPoint let length: CGFloat if bounds.width > 30.0 { origin = CGPoint(x: 9.0, y: 10.0 - UIScreenPixel) length = 17.0 } else { origin = CGPoint(x: 5.0 + UIScreenPixel, y: 4.0 + UIScreenPixel) length = 15.0 } if parameters.reverse { startPoint = CGPoint(x: origin.x + length * (1.0 - parameters.transition), y: origin.y + length * (1.0 - parameters.transition)) endPoint = CGPoint(x: origin.x + length, y: origin.y + length) } else { startPoint = origin endPoint = CGPoint(x: origin.x + length * parameters.transition, y: origin.y + length * parameters.transition) } context.setBlendMode(.clear) context.setLineWidth(clearLineWidth) context.move(to: startPoint) context.addLine(to: endPoint) context.strokePath() context.setBlendMode(.normal) context.setStrokeColor(parameters.color.cgColor) context.setLineWidth(lineWidth) context.setLineCap(.round) context.setLineJoin(.round) context.move(to: startPoint) context.addLine(to: endPoint) context.strokePath() } } }