2024-06-12 23:04:04 +04:00

401 lines
14 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
private func addRoundedRectPath(context: CGContext, rect: CGRect, radius: CGFloat) {
context.saveGState()
context.translateBy(x: rect.minX, y: rect.minY)
context.scaleBy(x: radius, y: radius)
let fw = rect.width / radius
let fh = rect.height / radius
context.move(to: CGPoint(x: fw, y: fh / 2.0))
context.addArc(tangent1End: CGPoint(x: fw, y: fh), tangent2End: CGPoint(x: fw/2, y: fh), radius: 1.0)
context.addArc(tangent1End: CGPoint(x: 0, y: fh), tangent2End: CGPoint(x: 0, y: fh/2), radius: 1)
context.addArc(tangent1End: CGPoint(x: 0, y: 0), tangent2End: CGPoint(x: fw/2, y: 0), radius: 1)
context.addArc(tangent1End: CGPoint(x: fw, y: 0), tangent2End: CGPoint(x: fw, y: fh/2), radius: 1)
context.closePath()
context.restoreGState()
}
private func stringForDuration(_ duration: Int) -> String {
let hours = duration / 3600
let minutes = duration / 60 % 60
let seconds = duration % 60
let durationString: String
if hours > 0 {
durationString = String(format: "%d:%02d:%02d", hours, minutes, seconds)
} else {
durationString = String(format: "%d:%02d", minutes, seconds)
}
return durationString
}
private final class AnimatedDotsLayer: SimpleLayer {
private let dotLayers: [SimpleLayer]
let size: CGSize
override init() {
self.dotLayers = (0 ..< 3).map { _ in
SimpleLayer()
}
let dotSpacing: CGFloat = 1.0
let dotSize = CGSize(width: 5.0, height: 5.0)
self.size = CGSize(width: CGFloat(self.dotLayers.count) * dotSize.width + CGFloat(self.dotLayers.count - 1) * dotSpacing, height: dotSize.height)
super.init()
let dotImage = UIGraphicsImageRenderer(size: dotSize).image(actions: { context in
context.cgContext.setFillColor(UIColor.white.cgColor)
context.cgContext.fillEllipse(in: CGRect(origin: CGPoint(), size: dotSize))
})
var nextX: CGFloat = 0.0
for dotLayer in self.dotLayers {
dotLayer.contents = dotImage.cgImage
dotLayer.frame = CGRect(origin: CGPoint(x: nextX, y: 0.0), size: dotSize)
nextX += dotSpacing + dotSize.width
self.addSublayer(dotLayer)
}
self.didEnterHierarchy = { [weak self] in
self?.updateAnimations()
}
}
override init(layer: Any) {
self.dotLayers = []
self.size = CGSize()
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateAnimations() {
if self.dotLayers[0].animation(forKey: "dotAnimation") != nil {
return
}
let animationDuration: Double = 0.6
for i in 0 ..< self.dotLayers.count {
let dotLayer = self.dotLayers[i]
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.duration = animationDuration
animation.fromValue = 0.3
animation.toValue = 1.0
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.autoreverses = true
animation.repeatCount = .infinity
animation.timeOffset = CGFloat(self.dotLayers.count - 1 - i) * animationDuration * 0.33
dotLayer.add(animation, forKey: "dotAnimation")
}
}
}
private final class SignalStrengthView: UIView {
let barViews: [UIImageView]
let size: CGSize
override init(frame: CGRect) {
self.barViews = (0 ..< 4).map { _ in
return UIImageView()
}
let itemWidth: CGFloat = 3.0
let itemHeight: CGFloat = 12.0
let itemSpacing: CGFloat = 2.0
self.size = CGSize(width: CGFloat(self.barViews.count) * itemWidth + CGFloat(self.barViews.count - 1) * itemSpacing, height: itemHeight)
super.init(frame: frame)
let itemImage = UIGraphicsImageRenderer(size: CGSize(width: itemWidth, height: itemWidth)).image(actions: { context in
context.cgContext.setFillColor(UIColor.white.cgColor)
addRoundedRectPath(context: context.cgContext, rect: CGRect(origin: CGPoint(), size: CGSize(width: itemWidth, height: itemWidth)), radius: 1.0)
context.cgContext.fillPath()
}).stretchableImage(withLeftCapWidth: Int(itemWidth * 0.5), topCapHeight: Int(itemWidth * 0.5))
var nextX: CGFloat = 0.0
for i in 0 ..< self.barViews.count {
let barView = self.barViews[i]
barView.image = itemImage
let barHeight = floor(CGFloat(i + 1) * itemHeight / CGFloat(self.barViews.count))
barView.frame = CGRect(origin: CGPoint(x: nextX, y: itemHeight - barHeight), size: CGSize(width: itemWidth, height: barHeight))
nextX += itemSpacing + itemWidth
self.addSubview(barView)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(value: Double) {
for i in 0 ..< self.barViews.count {
if value >= Double(i + 1) / Double(self.barViews.count) {
self.barViews[i].alpha = 1.0
} else {
self.barViews[i].alpha = 0.5
}
}
}
}
final class StatusView: UIView {
private struct LayoutState: Equatable {
var strings: PresentationStrings
var state: State
var size: CGSize
init(strings: PresentationStrings, state: State, size: CGSize) {
self.strings = strings
self.state = state
self.size = size
}
static func ==(lhs: LayoutState, rhs: LayoutState) -> Bool {
if lhs.strings !== rhs.strings {
return false
}
if lhs.state != rhs.state {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
}
enum WaitingState {
case requesting
case ringing
case connecting
case reconnecting
}
struct ActiveState: Equatable {
var startTimestamp: Double
var signalStrength: Double
init(startTimestamp: Double, signalStrength: Double) {
self.startTimestamp = startTimestamp
self.signalStrength = signalStrength
}
}
struct TerminatedState: Equatable {
var duration: Double
init(duration: Double) {
self.duration = duration
}
}
enum State: Equatable {
enum Key: Equatable {
case waiting(WaitingState)
case active
case terminated
}
case waiting(WaitingState)
case active(ActiveState)
case terminated(TerminatedState)
var key: Key {
switch self {
case let .waiting(waitingState):
return .waiting(waitingState)
case .active:
return .active
case .terminated:
return .terminated
}
}
}
private let textView: TextView
private var dotsLayer: AnimatedDotsLayer?
private var signalStrengthView: SignalStrengthView?
private var activeDurationTimer: Foundation.Timer?
private var layoutState: LayoutState?
var state: State? {
return self.layoutState?.state
}
var requestLayout: (() -> Void)?
override init(frame: CGRect) {
self.textView = TextView()
super.init(frame: frame)
self.addSubview(self.textView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.activeDurationTimer?.invalidate()
}
func update(strings: PresentationStrings, state: State, transition: ComponentTransition) -> CGSize {
if let layoutState = self.layoutState, layoutState.strings === strings, layoutState.state == state {
return layoutState.size
}
let size = self.updateInternal(strings: strings, state: state, transition: transition)
self.layoutState = LayoutState(strings: strings, state: state, size: size)
self.updateActiveDurationTimer()
return size
}
private func updateActiveDurationTimer() {
if let layoutState = self.layoutState, case let .active(activeState) = layoutState.state {
if self.activeDurationTimer == nil {
let timestamp = Date().timeIntervalSince1970
let duration = timestamp - activeState.startTimestamp
let nextTickDelay = ceil(duration) - duration + 0.05
self.activeDurationTimer = Foundation.Timer.scheduledTimer(withTimeInterval: nextTickDelay, repeats: false, block: { [weak self] _ in
guard let self else {
return
}
self.activeDurationTimer?.invalidate()
self.activeDurationTimer = nil
if let layoutState = self.layoutState {
let size = self.updateInternal(strings: layoutState.strings, state: layoutState.state, transition: .immediate)
if layoutState.size != size {
self.layoutState = nil
self.requestLayout?()
}
}
self.updateActiveDurationTimer()
})
}
} else {
if let activeDurationTimer = self.activeDurationTimer {
self.activeDurationTimer = nil
activeDurationTimer.invalidate()
}
}
}
private func updateInternal(strings: PresentationStrings, state: State, transition: ComponentTransition) -> CGSize {
let textString: String
var needsDots = false
var monospacedDigits = false
var signalStrength: Double?
switch state {
case let .waiting(waitingState):
needsDots = true
switch waitingState {
case .requesting:
textString = strings.Call_WaitingStatusRequesting
case .ringing:
textString = strings.Call_WaitingStatusRinging
case .connecting:
textString = strings.Call_WaitingStatusConnecting
case .reconnecting:
textString = strings.Call_WaitingStatusReconnecting
}
case let .active(activeState):
monospacedDigits = true
let timestamp = Date().timeIntervalSince1970
let duration = timestamp - activeState.startTimestamp
textString = stringForDuration(Int(duration))
signalStrength = activeState.signalStrength
case let .terminated(terminatedState):
if Int(terminatedState.duration) == 0 {
textString = " "
} else {
textString = stringForDuration(Int(terminatedState.duration))
}
}
var contentSize = CGSize()
if let signalStrength {
let signalStrengthView: SignalStrengthView
if let current = self.signalStrengthView {
signalStrengthView = current
} else {
signalStrengthView = SignalStrengthView(frame: CGRect())
self.signalStrengthView = signalStrengthView
self.addSubview(signalStrengthView)
}
signalStrengthView.update(value: signalStrength)
contentSize.width += signalStrengthView.size.width + 7.0
} else {
if let signalStrengthView = self.signalStrengthView {
self.signalStrengthView = nil
signalStrengthView.removeFromSuperview()
}
}
let textSize = self.textView.update(string: textString, fontSize: 16.0, fontWeight: 0.0, monospacedDigits: monospacedDigits, color: .white, constrainedWidth: 250.0, transition: .immediate)
let textFrame = CGRect(origin: CGPoint(x: contentSize.width, y: 0.0), size: textSize)
if self.textView.bounds.isEmpty {
self.textView.frame = textFrame
} else {
transition.setPosition(view: self.textView, position: textFrame.center)
transition.setBounds(view: self.textView, bounds: CGRect(origin: CGPoint(), size: textFrame.size))
}
contentSize.width += textSize.width
contentSize.height = textSize.height
if let signalStrengthView = self.signalStrengthView {
transition.setFrame(view: signalStrengthView, frame: CGRect(origin: CGPoint(x: 0.0, y: floor((textSize.height - signalStrengthView.size.height) * 0.5)), size: signalStrengthView.size))
}
if needsDots {
let dotsLayer: AnimatedDotsLayer
if let current = self.dotsLayer {
dotsLayer = current
} else {
dotsLayer = AnimatedDotsLayer()
self.dotsLayer = dotsLayer
self.layer.addSublayer(dotsLayer)
transition.animateAlpha(layer: dotsLayer, from: 0.0, to: 1.0)
}
let dotsSpacing: CGFloat = 6.0
let dotsFrame = CGRect(origin: CGPoint(x: textSize.width + dotsSpacing, y: 1.0 + floor((textSize.height - dotsLayer.size.height) * 0.5)), size: dotsLayer.size)
transition.setFrame(layer: dotsLayer, frame: dotsFrame)
contentSize.width += dotsSpacing + dotsFrame.width
} else if let dotsLayer = self.dotsLayer {
self.dotsLayer = nil
transition.setAlpha(layer: dotsLayer, alpha: 0.0, completion: { [weak dotsLayer] _ in
dotsLayer?.removeFromSuperlayer()
})
}
return contentSize
}
}