import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import TelegramPresentationData import TelegramUIPreferences import TelegramVoip import TelegramAudio import AccountContext import Postbox import TelegramCore import SyncCore import AppBundle import ContextUI import PresentationDataUtils public final class VoiceChatOverlayController: ViewController { private final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private weak var controller: VoiceChatOverlayController? private var validLayout: ContainerViewLayout? init(controller: VoiceChatOverlayController) { self.controller = controller } private var isButtonHidden = false private var isSlidOffscreen = false func update(hidden: Bool, slide: Bool, animated: Bool) { guard let actionButton = self.controller?.actionButton, actionButton.supernode === self else { return } if self.isButtonHidden == hidden || (!slide && self.isSlidOffscreen) { return } self.isButtonHidden = hidden if animated { let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) if hidden { if slide { self.isSlidOffscreen = true transition.updateSublayerTransformOffset(layer: actionButton.layer, offset: CGPoint(x: 70.0, y: 0.0)) } else { actionButton.layer.removeAllAnimations() actionButton.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak actionButton] _ in actionButton?.isHidden = true }) } } else { if slide { self.isSlidOffscreen = false transition.updateSublayerTransformOffset(layer: actionButton.layer, offset: CGPoint()) } else { actionButton.layer.removeAllAnimations() actionButton.isHidden = false actionButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) } } } else { } } func animateIn(from: CGRect) { guard let actionButton = self.controller?.actionButton else { return } let targetPosition = actionButton.position let sourcePoint = CGPoint(x: from.midX, y: from.midY) let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y + 120.0) let x1 = sourcePoint.x let y1 = sourcePoint.y let x2 = midPoint.x let y2 = midPoint.y let x3 = targetPosition.x let y3 = targetPosition.y let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) var keyframes: [AnyObject] = [] for i in 0 ..< 10 { let k = CGFloat(i) / CGFloat(10 - 1) let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k let y = a * x * x + b * x + c keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y))) } actionButton.layer.animateKeyframes(values: keyframes, duration: 0.2, keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { _ in }) } func animateOut(reclaim: Bool, completion: @escaping () -> Void) { guard let actionButton = self.controller?.actionButton, let layout = self.validLayout else { return } if reclaim { let targetPosition = CGPoint(x: layout.size.width / 2.0, y: layout.size.height - layout.intrinsicInsets.bottom - 268.0 / 2.0) let sourcePoint = actionButton.position let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0 - 20.0, y: sourcePoint.y + 10.0) let x1 = sourcePoint.x let y1 = sourcePoint.y let x2 = midPoint.x let y2 = midPoint.y let x3 = targetPosition.x let y3 = targetPosition.y let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) var keyframes: [AnyObject] = [] for i in 0 ..< 10 { let k = CGFloat(i) / CGFloat(10 - 1) let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k let y = a * x * x + b * x + c keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y))) } actionButton.update(snap: false) actionButton.position = targetPosition actionButton.layer.animateKeyframes(values: keyframes, duration: 0.4, keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { _ in completion() }) } else { actionButton.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self, weak actionButton] _ in actionButton?.removeFromSupernode() self?.controller?.dismiss() }) } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let actionButton = self.controller?.actionButton, actionButton.supernode === self && !self.isButtonHidden { let actionButtonSize = CGSize(width: 84.0, height: 84.0) let actionButtonFrame = CGRect(origin: CGPoint(x: actionButton.position.x - actionButtonSize.width / 2.0, y: actionButton.position.y - actionButtonSize.height / 2.0), size: actionButtonSize) if actionButtonFrame.contains(point) { return actionButton.hitTest(self.view.convert(point, to: actionButton.view), with: event) } } return nil } func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.validLayout = layout if let actionButton = self.controller?.actionButton { let convertedRect = actionButton.view.convert(actionButton.bounds, to: self.view) let insets = layout.insets(options: [.input]) transition.updatePosition(node: actionButton, position: CGPoint(x: layout.size.width - layout.safeInsets.right - 21.0, y: layout.size.height - insets.bottom - 22.0)) if actionButton.supernode !== self { self.addSubnode(actionButton) actionButton.update(snap: true) self.animateIn(from: convertedRect) } } } } private weak var actionButton: VoiceChatActionButton? private var controllerNode: Node { return self.displayNode as! Node } private var disposable: Disposable? init(actionButton: VoiceChatActionButton, navigationController: NavigationController?) { self.actionButton = actionButton super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = .Ignore self.additionalSideInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 75.0) if let navigationController = navigationController { let controllers: Signal<[UIViewController], NoError> = .single([]) |> then(navigationController.viewControllersSignal) let overlayControllers: Signal<[UIViewController], NoError> = .single([]) |> then(navigationController.overlayControllersSignal) self.disposable = (combineLatest(queue: Queue.mainQueue(), controllers, overlayControllers)).start(next: { [weak self] controllers, overlayControllers in if let strongSelf = self { var hasVoiceChatController = false for controller in controllers { if controller is VoiceChatController { hasVoiceChatController = true } } var hidden = true if controllers.count == 1 || controllers.last is ChatController { hidden = false } if overlayControllers.count > 0 { hidden = true } if hasVoiceChatController { hidden = false } strongSelf.controllerNode.update(hidden: hidden, slide: true, animated: true) } }) } } deinit { self.disposable?.dispose() } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override public func loadDisplayNode() { self.displayNode = Node(controller: self) self.displayNodeDidLoad() } public override func dismiss(completion: (() -> Void)? = nil) { super.dismiss(completion: completion) self.presentingViewController?.dismiss(animated: false, completion: nil) completion?() } func animateOut(reclaim: Bool, completion: @escaping () -> Void) { self.controllerNode.animateOut(reclaim: reclaim, completion: completion) } public func update(hidden: Bool, slide: Bool, animated: Bool) { self.controllerNode.update(hidden: hidden, slide: slide, animated: animated) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.containerLayoutUpdated(layout, transition: transition) } }