mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
332 lines
16 KiB
Swift
332 lines
16 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import SwiftSignalKit
|
|
import AppBundle
|
|
import SemanticStatusNode
|
|
|
|
private let labelFont = Font.regular(13.0)
|
|
|
|
final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|
struct Content: Equatable {
|
|
enum Appearance: Equatable {
|
|
enum Color {
|
|
case red
|
|
case green
|
|
case redDimmed
|
|
case greenDimmed
|
|
case grayDimmed
|
|
}
|
|
|
|
case blurred(isFilled: Bool)
|
|
case color(Color)
|
|
|
|
var isFilled: Bool {
|
|
if case let .blurred(isFilled) = self {
|
|
return isFilled
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
enum Image {
|
|
case camera
|
|
case mute
|
|
case flipCamera
|
|
case bluetooth
|
|
case speaker
|
|
case airpods
|
|
case airpodsPro
|
|
case accept
|
|
case end
|
|
}
|
|
|
|
var appearance: Appearance
|
|
var image: Image
|
|
var isEnabled: Bool
|
|
var hasProgress: Bool
|
|
|
|
init(appearance: Appearance, image: Image, isEnabled: Bool = true, hasProgress: Bool = false) {
|
|
self.appearance = appearance
|
|
self.image = image
|
|
self.isEnabled = isEnabled
|
|
self.hasProgress = hasProgress
|
|
}
|
|
}
|
|
|
|
private let contentContainer: ASDisplayNode
|
|
private let effectView: UIVisualEffectView
|
|
private let contentBackgroundNode: ASImageNode
|
|
private let contentNode: ASImageNode
|
|
private let overlayHighlightNode: ASImageNode
|
|
private var statusNode: SemanticStatusNode?
|
|
private let textNode: ImmediateTextNode
|
|
|
|
private let largeButtonSize: CGFloat = 72.0
|
|
|
|
private(set) var currentContent: Content?
|
|
private(set) var currentText: String = ""
|
|
|
|
init() {
|
|
self.contentContainer = ASDisplayNode()
|
|
|
|
self.effectView = UIVisualEffectView()
|
|
self.effectView.effect = UIBlurEffect(style: .light)
|
|
self.effectView.layer.cornerRadius = self.largeButtonSize / 2.0
|
|
self.effectView.clipsToBounds = true
|
|
self.effectView.isUserInteractionEnabled = false
|
|
|
|
self.contentBackgroundNode = ASImageNode()
|
|
self.contentBackgroundNode.isUserInteractionEnabled = false
|
|
|
|
self.contentNode = ASImageNode()
|
|
self.contentNode.isUserInteractionEnabled = false
|
|
|
|
self.overlayHighlightNode = ASImageNode()
|
|
self.overlayHighlightNode.isUserInteractionEnabled = false
|
|
self.overlayHighlightNode.alpha = 0.0
|
|
|
|
self.textNode = ImmediateTextNode()
|
|
self.textNode.displaysAsynchronously = false
|
|
self.textNode.isUserInteractionEnabled = false
|
|
|
|
super.init(pointerStyle: nil)
|
|
|
|
self.addSubnode(self.contentContainer)
|
|
self.contentContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
|
|
|
self.addSubnode(self.textNode)
|
|
|
|
self.contentContainer.view.addSubview(self.effectView)
|
|
self.contentContainer.addSubnode(self.contentBackgroundNode)
|
|
self.contentContainer.addSubnode(self.contentNode)
|
|
self.contentContainer.addSubnode(self.overlayHighlightNode)
|
|
|
|
self.highligthedChanged = { [weak self] highlighted in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if highlighted {
|
|
strongSelf.overlayHighlightNode.alpha = 1.0
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)
|
|
transition.updateSublayerTransformScale(node: strongSelf, scale: 0.9)
|
|
} else {
|
|
strongSelf.overlayHighlightNode.alpha = 0.0
|
|
strongSelf.overlayHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
|
|
transition.updateSublayerTransformScale(node: strongSelf, scale: 1.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
func update(size: CGSize, content: Content, text: String, transition: ContainedViewLayoutTransition) {
|
|
let scaleFactor = size.width / self.largeButtonSize
|
|
|
|
let isSmall = self.largeButtonSize > size.width
|
|
|
|
self.effectView.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
|
self.contentBackgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
|
self.contentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
|
self.overlayHighlightNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
|
|
|
if self.currentContent != content {
|
|
let previousContent = self.currentContent
|
|
self.currentContent = content
|
|
|
|
if content.hasProgress {
|
|
let statusFrame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
|
|
if self.statusNode == nil {
|
|
let statusNode = SemanticStatusNode(backgroundNodeColor: .white, foregroundNodeColor: .clear, cutout: statusFrame.insetBy(dx: 8.0, dy: 8.0))
|
|
self.statusNode = statusNode
|
|
self.contentContainer.insertSubnode(statusNode, belowSubnode: self.contentNode)
|
|
statusNode.transitionToState(.progress(value: nil, cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 4.0, lineWidth: 3.0)), animated: false, completion: {})
|
|
}
|
|
if let statusNode = self.statusNode {
|
|
statusNode.frame = statusFrame
|
|
if transition.isAnimated {
|
|
statusNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
|
statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
} else if let statusNode = self.statusNode {
|
|
self.statusNode = nil
|
|
transition.updateAlpha(node: statusNode, alpha: 0.0, completion: { [weak statusNode] _ in
|
|
statusNode?.removeFromSupernode()
|
|
})
|
|
}
|
|
|
|
switch content.appearance {
|
|
case .blurred:
|
|
self.effectView.isHidden = false
|
|
case .color:
|
|
self.effectView.isHidden = true
|
|
}
|
|
|
|
transition.updateAlpha(node: self, alpha: content.isEnabled ? 1.0 : 0.4)
|
|
self.isUserInteractionEnabled = content.isEnabled
|
|
|
|
let contentBackgroundImage: UIImage? = nil
|
|
|
|
let contentImage = generateImage(CGSize(width: self.largeButtonSize, height: self.largeButtonSize), contextGenerator: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
|
|
var ellipseRect = CGRect(origin: CGPoint(), size: size)
|
|
var fillColor: UIColor = .clear
|
|
let imageColor: UIColor = .white
|
|
var drawOverMask = false
|
|
context.setBlendMode(.normal)
|
|
let imageScale: CGFloat = 1.0
|
|
switch content.appearance {
|
|
case let .blurred(isFilled):
|
|
if content.hasProgress {
|
|
fillColor = .white
|
|
drawOverMask = true
|
|
context.setBlendMode(.copy)
|
|
ellipseRect = ellipseRect.insetBy(dx: 7.0, dy: 7.0)
|
|
} else {
|
|
if isFilled {
|
|
fillColor = .white
|
|
drawOverMask = true
|
|
context.setBlendMode(.copy)
|
|
}
|
|
}
|
|
case let .color(color):
|
|
switch color {
|
|
case .red:
|
|
fillColor = UIColor(rgb: 0xd92326)
|
|
case .green:
|
|
fillColor = UIColor(rgb: 0x74db58)
|
|
case .redDimmed:
|
|
fillColor = UIColor(rgb: 0xd92326).withMultipliedBrightnessBy(0.3)
|
|
case .greenDimmed:
|
|
fillColor = UIColor(rgb: 0x74db58).withMultipliedBrightnessBy(0.3)
|
|
case .grayDimmed:
|
|
fillColor = UIColor(rgb: 0x1C1C1E)
|
|
}
|
|
}
|
|
|
|
context.setFillColor(fillColor.cgColor)
|
|
context.fillEllipse(in: ellipseRect)
|
|
|
|
var image: UIImage?
|
|
|
|
switch content.image {
|
|
case .camera:
|
|
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallCameraButton"), color: imageColor)
|
|
case .mute:
|
|
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallMuteButton"), color: imageColor)
|
|
case .flipCamera:
|
|
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallSwitchCameraButton"), color: imageColor)
|
|
case .bluetooth:
|
|
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallBluetoothButton"), color: imageColor)
|
|
case .speaker:
|
|
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallSpeakerButton"), color: imageColor)
|
|
case .airpods:
|
|
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallAirpodsButton"), color: imageColor)
|
|
case .airpodsPro:
|
|
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallAirpodsProButton"), color: imageColor)
|
|
case .accept:
|
|
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallAcceptButton"), color: imageColor)
|
|
case .end:
|
|
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallDeclineButton"), color: imageColor)
|
|
}
|
|
|
|
if let image = image {
|
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
context.scaleBy(x: imageScale, y: imageScale)
|
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
|
|
|
let imageRect = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
|
|
if drawOverMask {
|
|
context.clip(to: imageRect, mask: image.cgImage!)
|
|
context.setBlendMode(.copy)
|
|
context.setFillColor(UIColor.clear.cgColor)
|
|
context.fill(CGRect(origin: CGPoint(), size: size))
|
|
} else {
|
|
context.draw(image.cgImage!, in: imageRect)
|
|
}
|
|
}
|
|
})
|
|
|
|
if transition.isAnimated, let contentBackgroundImage = contentBackgroundImage, let previousContent = self.contentBackgroundNode.image {
|
|
self.contentBackgroundNode.image = contentBackgroundImage
|
|
self.contentBackgroundNode.layer.animate(from: previousContent.cgImage!, to: contentBackgroundImage.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
|
|
} else {
|
|
self.contentBackgroundNode.image = contentBackgroundImage
|
|
}
|
|
|
|
if transition.isAnimated, let previousContent = previousContent, previousContent.image == .accept && content.image == .end {
|
|
let rotation = CGFloat.pi / 4.0 * 3.0
|
|
|
|
if let snapshotView = self.contentNode.view.snapshotContentTree() {
|
|
snapshotView.frame = self.contentNode.view.frame
|
|
self.contentContainer.view.addSubview(snapshotView)
|
|
|
|
snapshotView.layer.animateRotation(from: 0.0, to: rotation, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
|
snapshotView?.removeFromSuperview()
|
|
})
|
|
}
|
|
self.contentNode.image = contentImage
|
|
self.contentNode.layer.animateRotation(from: -rotation, to: 0.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
|
} else if transition.isAnimated, let contentImage = contentImage, let previousContent = self.contentNode.image {
|
|
self.contentNode.image = contentImage
|
|
self.contentNode.layer.animate(from: previousContent.cgImage!, to: contentImage.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
|
|
} else {
|
|
self.contentNode.image = contentImage
|
|
}
|
|
|
|
self.overlayHighlightNode.image = generateImage(CGSize(width: self.largeButtonSize, height: self.largeButtonSize), contextGenerator: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
|
|
let fillColor: UIColor
|
|
context.setBlendMode(.normal)
|
|
switch content.appearance {
|
|
case let .blurred(isFilled):
|
|
if isFilled {
|
|
fillColor = UIColor(white: 0.0, alpha: 0.1)
|
|
} else {
|
|
fillColor = UIColor(white: 1.0, alpha: 0.2)
|
|
}
|
|
case let .color(color):
|
|
switch color {
|
|
case .red:
|
|
fillColor = UIColor(rgb: 0xd92326).withMultipliedBrightnessBy(0.2).withAlphaComponent(0.2)
|
|
case .green:
|
|
fillColor = UIColor(rgb: 0x74db58).withMultipliedBrightnessBy(0.2).withAlphaComponent(0.2)
|
|
case .redDimmed:
|
|
fillColor = UIColor(rgb: 0xd92326).withMultipliedBrightnessBy(0.4).withAlphaComponent(0.2)
|
|
case .greenDimmed:
|
|
fillColor = UIColor(rgb: 0x74db58).withMultipliedBrightnessBy(0.4).withAlphaComponent(0.2)
|
|
case .grayDimmed:
|
|
fillColor = UIColor(rgb: 0x1C1C1E).withAlphaComponent(0.2)
|
|
}
|
|
}
|
|
|
|
context.setFillColor(fillColor.cgColor)
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
|
})
|
|
}
|
|
|
|
transition.updatePosition(node: self.contentContainer, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0))
|
|
transition.updateSublayerTransformScale(node: self.contentContainer, scale: scaleFactor)
|
|
|
|
if self.currentText != text {
|
|
self.textNode.attributedText = NSAttributedString(string: text, font: labelFont, textColor: .white)
|
|
}
|
|
let textSize = self.textNode.updateLayout(CGSize(width: 150.0, height: 100.0))
|
|
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: size.height + (isSmall ? 5.0 : 8.0)), size: textSize)
|
|
if self.currentText.isEmpty {
|
|
self.textNode.frame = textFrame
|
|
if transition.isAnimated {
|
|
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
|
}
|
|
} else {
|
|
transition.updateFrameAdditiveToCenter(node: self.textNode, frame: textFrame)
|
|
}
|
|
self.currentText = text
|
|
}
|
|
}
|