mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
383 lines
14 KiB
Swift
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()
|
|
}
|
|
}
|
|
}
|