mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-07-07 20:10:44 +00:00
401 lines
14 KiB
Swift
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
|
|
}
|
|
}
|