mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
[WIP] Private Call UI
This commit is contained in:
@@ -1,6 +1,36 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
|
||||
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]
|
||||
@@ -71,28 +101,116 @@ private final class AnimatedDotsLayer: SimpleLayer {
|
||||
}
|
||||
}
|
||||
|
||||
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 state: State
|
||||
var size: CGSize
|
||||
|
||||
init(state: State, size: CGSize) {
|
||||
self.state = state
|
||||
self.size = size
|
||||
}
|
||||
}
|
||||
|
||||
enum WaitingState {
|
||||
case requesting
|
||||
case ringing
|
||||
case generatingKeys
|
||||
}
|
||||
|
||||
struct ActiveState {
|
||||
struct ActiveState: Equatable {
|
||||
var startTimestamp: Double
|
||||
var signalStrength: Double
|
||||
|
||||
init(signalStrength: Double) {
|
||||
init(startTimestamp: Double, signalStrength: Double) {
|
||||
self.startTimestamp = startTimestamp
|
||||
self.signalStrength = signalStrength
|
||||
}
|
||||
}
|
||||
|
||||
enum State {
|
||||
enum State: Equatable {
|
||||
enum Key: Equatable {
|
||||
case waiting(WaitingState)
|
||||
case active
|
||||
}
|
||||
|
||||
case waiting(WaitingState)
|
||||
case active(ActiveState)
|
||||
|
||||
var key: Key {
|
||||
switch self {
|
||||
case let .waiting(waitingState):
|
||||
return .waiting(waitingState)
|
||||
case .active:
|
||||
return .active
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var textView: TextView
|
||||
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()
|
||||
@@ -106,9 +224,60 @@ final class StatusView: UIView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(state: State) -> CGSize {
|
||||
deinit {
|
||||
self.activeDurationTimer?.invalidate()
|
||||
}
|
||||
|
||||
func update(state: State, transition: Transition) -> CGSize {
|
||||
if let layoutState = self.layoutState, layoutState.state == state {
|
||||
return layoutState.size
|
||||
}
|
||||
let size = self.updateInternal(state: state, transition: transition)
|
||||
self.layoutState = LayoutState(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(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(state: State, transition: Transition) -> CGSize {
|
||||
let textString: String
|
||||
var needsDots = false
|
||||
var monospacedDigits = false
|
||||
var signalStrength: Double?
|
||||
switch state {
|
||||
case let .waiting(waitingState):
|
||||
needsDots = true
|
||||
@@ -122,15 +291,49 @@ final class StatusView: UIView {
|
||||
textString = "Exchanging encryption keys"
|
||||
}
|
||||
case let .active(activeState):
|
||||
textString = "0:00"
|
||||
let _ = activeState
|
||||
monospacedDigits = true
|
||||
|
||||
let timestamp = Date().timeIntervalSince1970
|
||||
let duration = timestamp - activeState.startTimestamp
|
||||
textString = stringForDuration(Int(duration))
|
||||
signalStrength = activeState.signalStrength
|
||||
}
|
||||
let textSize = self.textView.update(string: textString, fontSize: 16.0, fontWeight: 0.0, constrainedWidth: 200.0)
|
||||
self.textView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: textSize)
|
||||
|
||||
var contentSize = textSize
|
||||
var contentSize = CGSize()
|
||||
|
||||
let dotsSpacing: CGFloat = 6.0
|
||||
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
|
||||
@@ -140,13 +343,19 @@ final class StatusView: UIView {
|
||||
dotsLayer = AnimatedDotsLayer()
|
||||
self.dotsLayer = dotsLayer
|
||||
self.layer.addSublayer(dotsLayer)
|
||||
transition.animateAlpha(layer: dotsLayer, from: 0.0, to: 1.0)
|
||||
}
|
||||
|
||||
dotsLayer.frame = CGRect(origin: CGPoint(x: textSize.width + dotsSpacing, y: 1.0 + floor((textSize.height - dotsLayer.size.height) * 0.5)), size: dotsLayer.size)
|
||||
contentSize.width += dotsSpacing + dotsLayer.size.width
|
||||
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
|
||||
dotsLayer.removeFromSuperlayer()
|
||||
transition.setAlpha(layer: dotsLayer, alpha: 0.0, completion: { [weak dotsLayer] _ in
|
||||
dotsLayer?.removeFromSuperlayer()
|
||||
})
|
||||
}
|
||||
|
||||
return contentSize
|
||||
|
||||
Reference in New Issue
Block a user