Swiftgram/submodules/TelegramCallsUI/Sources/CallControllerButton.swift
2021-08-05 01:42:59 +02:00

404 lines
20 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import AppBundle
import SemanticStatusNode
import AnimationUI
private let labelFont = Font.regular(13.0)
final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
struct Content: Equatable {
enum Appearance: Equatable {
enum Color: Equatable {
case red
case green
case custom(UInt32, CGFloat)
}
case blurred(isFilled: Bool)
case color(Color)
var isFilled: Bool {
if case let .blurred(isFilled) = self {
return isFilled
} else {
return false
}
}
}
enum Image {
case cameraOff
case cameraOn
case camera
case mute
case flipCamera
case bluetooth
case speaker
case airpods
case airpodsPro
case airpodsMax
case headphones
case accept
case end
case cancel
case share
case screencast
}
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 wrapperNode: ASDisplayNode
private let contentContainer: ASDisplayNode
private let effectView: UIVisualEffectView
private let contentBackgroundNode: ASImageNode
private let contentNode: ASImageNode
private var animationNode: AnimationNode?
private let overlayHighlightNode: ASImageNode
private var statusNode: SemanticStatusNode?
let textNode: ImmediateTextNode
private let largeButtonSize: CGFloat
private var size: CGSize?
private(set) var currentContent: Content?
private(set) var currentText: String = ""
init(largeButtonSize: CGFloat = 72.0) {
self.largeButtonSize = largeButtonSize
self.wrapperNode = ASDisplayNode()
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.wrapperNode)
self.wrapperNode.addSubnode(self.contentContainer)
self.contentContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
self.wrapperNode.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)
}
}
}
override func layout() {
super.layout()
self.wrapperNode.frame = self.bounds
}
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 || self.size != size {
let previousContent = self.currentContent
self.currentContent = content
self.size = size
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.wrapperNode, alpha: content.isEnabled ? 1.0 : 0.4)
self.wrapperNode.isUserInteractionEnabled = content.isEnabled
let contentBackgroundImage: UIImage? = nil
var animationName: String?
switch content.image {
case .cameraOff:
animationName = "anim_cameraoff"
case .cameraOn:
animationName = "anim_cameraon"
default:
break
}
if let animationName = animationName {
let animationFrame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
if self.animationNode == nil {
let animationNode = AnimationNode(animation: animationName, colors: nil, scale: 1.0)
self.animationNode = animationNode
self.contentContainer.insertSubnode(animationNode, aboveSubnode: self.contentNode)
}
if let animationNode = self.animationNode {
animationNode.bounds = animationFrame
animationNode.position = CGPoint(x: self.largeButtonSize / 2.0, y: self.largeButtonSize / 2.0)
if previousContent == nil {
animationNode.seekToEnd()
} else if previousContent?.image != content.image {
animationNode.setAnimation(name: animationName)
animationNode.play()
}
}
}
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 let .custom(color, alpha):
fillColor = UIColor(rgb: color, alpha: alpha)
}
}
context.setFillColor(fillColor.cgColor)
context.fillEllipse(in: ellipseRect)
var image: UIImage?
switch content.image {
case .cameraOff, .cameraOn:
image = nil
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 .airpodsMax:
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallAirpodsMaxButton"), color: imageColor)
case .headphones:
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallHeadphonesButton"), color: imageColor)
case .accept:
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallAcceptButton"), color: imageColor)
case .end:
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallDeclineButton"), color: imageColor)
case .cancel:
image = generateImage(CGSize(width: 28.0, height: 28.0), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setLineWidth(4.0 - UIScreenPixel)
context.setLineCap(.round)
context.setStrokeColor(imageColor.cgColor)
context.move(to: CGPoint(x: 2.0 + UIScreenPixel, y: 2.0 + UIScreenPixel))
context.addLine(to: CGPoint(x: 26.0 - UIScreenPixel, y: 26.0 - UIScreenPixel))
context.strokePath()
context.move(to: CGPoint(x: 26.0 - UIScreenPixel, y: 2.0 + UIScreenPixel))
context.addLine(to: CGPoint(x: 2.0 + UIScreenPixel, y: 26.0 - UIScreenPixel))
context.strokePath()
})
case .share:
image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallShareButton"), color: imageColor)
case .screencast:
if let iconImage = generateTintedImage(image: UIImage(bundleImageName: "Call/ScreenSharePhone"), color: imageColor) {
image = generateScaledImage(image: iconImage, size: iconImage.size.aspectFitted(CGSize(width: 38.0, height: 38.0)))
}
}
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 let .custom(color, _):
fillColor = UIColor(rgb: color).withMultipliedBrightnessBy(0.2).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 let animationNode = self.animationNode {
transition.updateTransformScale(node: animationNode, scale: isSmall ? 1.35 : 1.12)
}
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
}
}