Swiftgram/submodules/TelegramCallsUI/Sources/VoiceChatSpeakerNode.swift
2020-12-29 22:10:33 +04:00

383 lines
14 KiB
Swift

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()
}
}
}