import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import TelegramPresentationData import TelegramUIPreferences import TelegramVoip import TelegramAudio import AccountContext import TelegramCore import AppBundle import ContextUI import PresentationDataUtils import TooltipUI import VoiceChatActionButton private let slideOffset: CGFloat = 80.0 + 44.0 public final class VoiceChatOverlayController: ViewController { private final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate { private weak var controller: VoiceChatOverlayController? private var validLayout: ContainerViewLayout? init(controller: VoiceChatOverlayController) { self.controller = controller super.init() self.clipsToBounds = true } private var isButtonHidden = false private var isSlidOffscreen = false func update(hidden: Bool, slide: Bool, animated: Bool) { guard let actionButton = self.controller?.actionButton else { return } if self.isButtonHidden == hidden { return } self.isButtonHidden = hidden var slide = slide if self.isSlidOffscreen && !hidden { slide = true } self.isSlidOffscreen = hidden && slide guard actionButton.supernode === self else { return } if animated { let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) if hidden { if slide { actionButton.isHidden = false transition.updateSublayerTransformOffset(layer: actionButton.layer, offset: CGPoint(x: slideOffset, 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] finished in if finished { actionButton?.isHidden = true } }) } } else { actionButton.isHidden = false actionButton.layer.removeAllAnimations() if slide { transition.updateSublayerTransformOffset(layer: actionButton.layer, offset: CGPoint()) } else { actionButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) } } } else { actionButton.isHidden = hidden actionButton.layer.removeAllAnimations() if hidden { if slide { actionButton.layer.sublayerTransform = CATransform3DMakeTranslation(slideOffset, 0.0, 0.0) } } else { if slide { actionButton.layer.sublayerTransform = CATransform3DIdentity } } } } private var initialLeftButtonPosition: CGPoint? private var initialRightButtonPosition: CGPoint? func animateIn(from: CGRect) { guard let controller = self.controller, let actionButton = controller.actionButton, let audioOutputNode = controller.audioOutputNode, let cameraNode = controller.cameraNode, let rightButton = controller.leaveNode else { return } let leftButton: CallControllerButtonItemNode if audioOutputNode.alpha.isZero { leftButton = cameraNode } else { leftButton = audioOutputNode } self.initialLeftButtonPosition = leftButton.position self.initialRightButtonPosition = rightButton.position actionButton.update(snap: true, animated: !self.isSlidOffscreen && !self.isButtonHidden) if self.isSlidOffscreen { leftButton.isHidden = true rightButton.isHidden = true actionButton.layer.sublayerTransform = CATransform3DMakeTranslation(slideOffset, 0.0, 0.0) return } else if self.isButtonHidden { leftButton.isHidden = true rightButton.isHidden = true actionButton.isHidden = true return } let center = CGPoint(x: actionButton.frame.width / 2.0, y: actionButton.frame.height / 2.0) leftButton.layer.animatePosition(from: leftButton.position, to: center, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak leftButton] _ in leftButton?.isHidden = true leftButton?.textNode.layer.removeAllAnimations() leftButton?.layer.removeAllAnimations() }) leftButton.layer.animateScale(from: 1.0, to: 0.5, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) rightButton.layer.animatePosition(from: rightButton.position, to: center, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak rightButton] _ in rightButton?.isHidden = true rightButton?.textNode.layer.removeAllAnimations() rightButton?.layer.removeAllAnimations() }) rightButton.layer.animateScale(from: 1.0, to: 0.5, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) leftButton.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false) rightButton.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false) 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 + 90.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 }) } private var animating = false private var dismissed = false func animateOut(reclaim: Bool, targetPosition: CGPoint, completion: @escaping (Bool) -> Void) { guard let controller = self.controller, let actionButton = controller.actionButton, let audioOutputNode = controller.audioOutputNode, let cameraNode = controller.cameraNode, let rightButton = controller.leaveNode else { return } let leftButton: CallControllerButtonItemNode if audioOutputNode.alpha.isZero { leftButton = cameraNode } else { leftButton = audioOutputNode } if reclaim { self.dismissed = true if self.isSlidOffscreen { self.isSlidOffscreen = false self.isButtonHidden = true actionButton.layer.sublayerTransform = CATransform3DIdentity actionButton.update(snap: false, animated: false) actionButton.position = CGPoint(x: targetPosition.x, y: bottomAreaHeight / 2.0) leftButton.isHidden = false rightButton.isHidden = false if let leftButtonPosition = self.initialLeftButtonPosition { leftButton.position = CGPoint(x: actionButton.position.x + leftButtonPosition.x, y: actionButton.position.y) } if let rightButtonPosition = self.initialRightButtonPosition { rightButton.position = CGPoint(x: actionButton.position.x + rightButtonPosition.x, y: actionButton.position.y) } completion(true) } else if self.isButtonHidden { actionButton.isHidden = false actionButton.layer.removeAllAnimations() actionButton.layer.sublayerTransform = CATransform3DIdentity actionButton.update(snap: false, animated: false) actionButton.position = CGPoint(x: targetPosition.x, y: bottomAreaHeight / 2.0) leftButton.isHidden = false rightButton.isHidden = false if let leftButtonPosition = self.initialLeftButtonPosition { leftButton.position = CGPoint(x: actionButton.position.x + leftButtonPosition.x, y: actionButton.position.y) } if let rightButtonPosition = self.initialRightButtonPosition { rightButton.position = CGPoint(x: actionButton.position.x + rightButtonPosition.x, y: actionButton.position.y) } completion(true) } else { self.animating = true let sourcePoint = actionButton.position let transitionNode = ASDisplayNode() transitionNode.position = sourcePoint transitionNode.addSubnode(actionButton) actionButton.position = CGPoint() self.addSubnode(transitionNode) if let leftButtonPosition = self.initialLeftButtonPosition, let rightButtonPosition = self.initialRightButtonPosition { let center = CGPoint(x: actionButton.frame.width / 2.0, y: actionButton.frame.height / 2.0) leftButton.isHidden = false rightButton.isHidden = false leftButton.layer.animatePosition(from: center, to: leftButtonPosition, duration: 0.26, delay: 0.07, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false) rightButton.layer.animatePosition(from: center, to: rightButtonPosition, duration: 0.26, delay: 0.07, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false) leftButton.layer.animateScale(from: 0.55, to: 1.0, duration: 0.26, delay: 0.06, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) rightButton.layer.animateScale(from: 0.55, to: 1.0, duration: 0.26, delay: 0.06, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) leftButton.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, delay: 0.05) rightButton.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, delay: 0.05) } actionButton.update(snap: false, animated: true) actionButton.position = CGPoint(x: targetPosition.x - sourcePoint.x, y: 80.0) let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) transition.animateView { transitionNode.position = CGPoint(x: transitionNode.position.x, y: targetPosition.y - 80.0) } actionButton.layer.animatePosition(from: CGPoint(), to: actionButton.position, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { _ in self.animating = false completion(false) }) } } 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 } private var didAnimateIn = false func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.validLayout = layout if let controller = self.controller, let actionButton = controller.actionButton, let audioOutputNode = controller.audioOutputNode, let cameraNode = controller.cameraNode, let rightButton = controller.leaveNode, !self.animating && !self.dismissed { let leftButton: CallControllerButtonItemNode if audioOutputNode.alpha.isZero { leftButton = cameraNode } else { leftButton = audioOutputNode } let convertedRect = actionButton.view.convert(actionButton.bounds, to: self.view) let insets = layout.insets(options: [.input]) if !self.didAnimateIn { let leftButtonFrame = leftButton.view.convert(leftButton.bounds, to: actionButton.bottomNode.view) actionButton.bottomNode.addSubnode(leftButton) leftButton.frame = leftButtonFrame let rightButtonFrame = rightButton.view.convert(rightButton.bounds, to: actionButton.bottomNode.view) actionButton.bottomNode.addSubnode(rightButton) rightButton.frame = rightButtonFrame } 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.didAnimateIn { self.didAnimateIn = true actionButton.ignoreHierarchyChanges = true self.addSubnode(actionButton) var hidden = false if let initiallyHidden = self.controller?.initiallyHidden, initiallyHidden { hidden = initiallyHidden } if hidden { self.update(hidden: true, slide: true, animated: false) } self.animateIn(from: convertedRect) if hidden { self.controller?.setupVisibilityUpdates() } actionButton.ignoreHierarchyChanges = false } } } } private weak var actionButton: VoiceChatActionButton? private weak var cameraNode: CallControllerButtonItemNode? private weak var audioOutputNode: CallControllerButtonItemNode? private weak var leaveNode: CallControllerButtonItemNode? private var controllerNode: Node { return self.displayNode as! Node } private var disposable: Disposable? private weak var parentNavigationController: NavigationController? private var currentParams: ([UIViewController], [UIViewController], VoiceChatActionButton.State)? fileprivate var initiallyHidden: Bool init(actionButton: VoiceChatActionButton, audioOutputNode: CallControllerButtonItemNode, cameraNode: CallControllerButtonItemNode, leaveNode: CallControllerButtonItemNode, navigationController: NavigationController?, initiallyHidden: Bool) { self.actionButton = actionButton self.audioOutputNode = audioOutputNode self.cameraNode = cameraNode self.leaveNode = leaveNode self.parentNavigationController = navigationController self.initiallyHidden = initiallyHidden super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = .Ignore if case .active(.cantSpeak) = actionButton.stateValue { } else if !initiallyHidden { self.additionalSideInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 75.0) } if !self.initiallyHidden { self.setupVisibilityUpdates() } } 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() } private func setupVisibilityUpdates() { if let navigationController = self.parentNavigationController, let actionButton = self.actionButton { 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, actionButton.state)).start(next: { [weak self] controllers, overlayControllers, state in if let strongSelf = self { strongSelf.currentParams = (controllers, overlayControllers, state) strongSelf.updateVisibility() } }) } } public override func dismiss(completion: (() -> Void)? = nil) { super.dismiss(completion: completion) self.presentingViewController?.dismiss(animated: false, completion: nil) completion?() } func animateOut(reclaim: Bool, targetPosition: CGPoint, completion: @escaping (Bool) -> Void) { self.controllerNode.animateOut(reclaim: reclaim, targetPosition: targetPosition, completion: completion) } public func updateVisibility() { guard let (controllers, overlayControllers, state) = self.currentParams else { return } var hasVoiceChatController = false var overlayControllersCount = 0 for controller in controllers { if controller is VoiceChatController { hasVoiceChatController = true } } for controller in overlayControllers { if controller is TooltipController || controller is TooltipScreen || controller is AlertController { } else { overlayControllersCount += 1 } } var slide = true var hidden = true var animated = true if controllers.count == 1 || controllers.last is ChatController { if let chatController = controllers.last as? ChatController { slide = false if !chatController.isSendButtonVisible { hidden = false } } else { hidden = false } } if let tabBarController = controllers.last as? TabBarController { if let chatListController = tabBarController.controllers[tabBarController.selectedIndex] as? ChatListController, chatListController.isSearchActive { hidden = true } } if overlayControllersCount > 0 { hidden = true } switch state { case .active(.cantSpeak), .button, .scheduled: hidden = true default: break } if hasVoiceChatController { hidden = false animated = self.initiallyHidden self.initiallyHidden = false } self.controllerNode.update(hidden: hidden, slide: slide, animated: animated) let previousInsets = self.additionalSideInsets self.additionalSideInsets = hidden ? UIEdgeInsets() : UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 75.0) if previousInsets != self.additionalSideInsets { self.parentNavigationController?.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) } } private let hiddenPromise = ValuePromise() public func update(hidden: Bool, slide: Bool, animated: Bool) { self.hiddenPromise.set(hidden) 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) } }