Swiftgram/submodules/TelegramCallsUI/Sources/CallControllerStatusNode.swift
2020-08-09 19:54:50 +03:00

263 lines
12 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
private let compactNameFont = Font.regular(28.0)
private let regularNameFont = Font.regular(36.0)
private let compactStatusFont = Font.regular(18.0)
private let regularStatusFont = Font.regular(18.0)
enum CallControllerStatusValue: Equatable {
case text(string: String, displayLogo: Bool)
case timer((String) -> String, Double)
static func ==(lhs: CallControllerStatusValue, rhs: CallControllerStatusValue) -> Bool {
switch lhs {
case let .text(text, displayLogo):
if case .text(text, displayLogo) = rhs {
return true
} else {
return false
}
case let .timer(_, referenceTime):
if case .timer(_, referenceTime) = rhs {
return true
} else {
return false
}
}
}
}
final class CallControllerStatusNode: ASDisplayNode {
private let titleNode: TextNode
private let statusContainerNode: ASDisplayNode
private let statusNode: TextNode
private let statusMeasureNode: TextNode
private let receptionNode: CallControllerReceptionNode
private let logoNode: ASImageNode
var title: String = ""
var subtitle: String = ""
var status: CallControllerStatusValue = .text(string: "", displayLogo: false) {
didSet {
if self.status != oldValue {
self.statusTimer?.invalidate()
if let snapshotView = self.statusContainerNode.view.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = self.statusContainerNode.frame
self.view.insertSubview(snapshotView, belowSubview: self.statusContainerNode.view)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animateScale(from: 1.0, to: 0.3, duration: 0.3, removeOnCompletion: false)
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: snapshotView.frame.height / 2.0), duration: 0.3, delay: 0.0, removeOnCompletion: false, additive: true)
self.statusContainerNode.layer.animateScale(from: 0.3, to: 1.0, duration: 0.3)
self.statusContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.statusContainerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -snapshotView.frame.height / 2.0), to: CGPoint(), duration: 0.3, delay: 0.0, additive: true)
}
if case .timer = self.status {
self.statusTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
if let strongSelf = self, let validLayoutWidth = strongSelf.validLayoutWidth {
let _ = strongSelf.updateLayout(constrainedWidth: validLayoutWidth, transition: .immediate)
}
}, queue: Queue.mainQueue())
self.statusTimer?.start()
} else {
if let validLayoutWidth = self.validLayoutWidth {
let _ = self.updateLayout(constrainedWidth: validLayoutWidth, transition: .immediate)
}
}
}
}
}
var reception: Int32? {
didSet {
if self.reception != oldValue {
if let reception = self.reception {
self.receptionNode.reception = reception
if oldValue == nil {
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring)
transition.updateAlpha(node: self.receptionNode, alpha: 1.0)
}
} else if self.reception == nil, oldValue != nil {
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring)
transition.updateAlpha(node: self.receptionNode, alpha: 0.0)
}
if (oldValue == nil) != (self.reception != nil) {
if let validLayoutWidth = self.validLayoutWidth {
let _ = self.updateLayout(constrainedWidth: validLayoutWidth, transition: .immediate)
}
}
}
}
}
private var statusTimer: SwiftSignalKit.Timer?
private var validLayoutWidth: CGFloat?
override init() {
self.titleNode = TextNode()
self.statusContainerNode = ASDisplayNode()
self.statusNode = TextNode()
self.statusNode.displaysAsynchronously = false
self.statusMeasureNode = TextNode()
self.receptionNode = CallControllerReceptionNode()
self.receptionNode.alpha = 0.0
self.logoNode = ASImageNode()
self.logoNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallTitleLogo"), color: .white)
self.logoNode.isHidden = true
super.init()
self.isUserInteractionEnabled = false
self.addSubnode(self.titleNode)
self.addSubnode(self.statusContainerNode)
self.statusContainerNode.addSubnode(self.statusNode)
self.statusContainerNode.addSubnode(self.receptionNode)
self.statusContainerNode.addSubnode(self.logoNode)
}
deinit {
self.statusTimer?.invalidate()
}
func setVisible(_ visible: Bool, transition: ContainedViewLayoutTransition) {
let alpha: CGFloat = visible ? 1.0 : 0.0
transition.updateAlpha(node: self.titleNode, alpha: alpha)
transition.updateAlpha(node: self.statusContainerNode, alpha: alpha)
}
func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayoutWidth = constrainedWidth
let nameFont: UIFont
let statusFont: UIFont
if constrainedWidth < 330.0 {
nameFont = compactNameFont
statusFont = compactStatusFont
} else {
nameFont = regularNameFont
statusFont = regularStatusFont
}
var statusOffset: CGFloat = 0.0
let statusText: String
let statusMeasureText: String
var statusDisplayLogo: Bool = false
switch self.status {
case let .text(text, displayLogo):
statusText = text
statusMeasureText = text
statusDisplayLogo = displayLogo
if displayLogo {
statusOffset += 10.0
}
case let .timer(format, referenceTime):
let duration = Int32(CFAbsoluteTimeGetCurrent() - referenceTime)
let durationString: String
let measureDurationString: String
if duration > 60 * 60 {
durationString = String(format: "%02d:%02d:%02d", arguments: [duration / 3600, (duration / 60) % 60, duration % 60])
measureDurationString = "00:00:00"
} else {
durationString = String(format: "%02d:%02d", arguments: [(duration / 60) % 60, duration % 60])
measureDurationString = "00:00"
}
statusText = format(durationString)
statusMeasureText = format(measureDurationString)
if self.reception != nil {
statusOffset += 8.0
}
}
let spacing: CGFloat = 1.0
let (titleLayout, titleApply) = TextNode.asyncLayout(self.titleNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.title, font: nameFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)))
let (statusMeasureLayout, statusMeasureApply) = TextNode.asyncLayout(self.statusMeasureNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: statusMeasureText, font: statusFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)))
let (statusLayout, statusApply) = TextNode.asyncLayout(self.statusNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: statusText, font: statusFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)))
let _ = titleApply()
let _ = statusApply()
let _ = statusMeasureApply()
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - titleLayout.size.width) / 2.0), y: 0.0), size: titleLayout.size)
self.statusContainerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: titleLayout.size.height + spacing), size: CGSize(width: constrainedWidth, height: statusLayout.size.height))
self.statusNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - statusMeasureLayout.size.width) / 2.0) + statusOffset, y: 0.0), size: statusLayout.size)
self.receptionNode.frame = CGRect(origin: CGPoint(x: self.statusNode.frame.minX - receptionNodeSize.width, y: 9.0), size: receptionNodeSize)
self.logoNode.isHidden = !statusDisplayLogo
if let image = self.logoNode.image, let firstLineRect = statusMeasureLayout.linesRects().first {
let firstLineOffset = floor((statusMeasureLayout.size.width - firstLineRect.width) / 2.0)
self.logoNode.frame = CGRect(origin: CGPoint(x: self.statusNode.frame.minX + firstLineOffset - image.size.width - 7.0, y: 5.0), size: image.size)
}
return titleLayout.size.height + spacing + statusLayout.size.height
}
}
private final class CallControllerReceptionNodeParameters: NSObject {
let reception: Int32
init(reception: Int32) {
self.reception = reception
}
}
private let receptionNodeSize = CGSize(width: 24.0, height: 10.0)
final class CallControllerReceptionNode : ASDisplayNode {
var reception: Int32 = 4 {
didSet {
self.setNeedsDisplay()
}
}
override init() {
super.init()
self.isOpaque = false
self.isLayerBacked = true
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return CallControllerReceptionNodeParameters(reception: self.reception)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
context.setFillColor(UIColor.white.cgColor)
if let parameters = parameters as? CallControllerReceptionNodeParameters{
let width: CGFloat = 3.0
var spacing: CGFloat = 1.5
if UIScreenScale > 2 {
spacing = 4.0 / 3.0
}
for i in 0 ..< 4 {
let height = 4.0 + 2.0 * CGFloat(i)
let rect = CGRect(x: bounds.minX + CGFloat(i) * (width + spacing), y: receptionNodeSize.height - height, width: width, height: height)
if i >= parameters.reception {
context.setAlpha(0.4)
}
let path = UIBezierPath(roundedRect: rect, cornerRadius: 0.5)
context.addPath(path.cgPath)
context.fillPath()
}
}
}
}