Video call screen improvements

This commit is contained in:
Ali
2020-07-07 20:14:02 +04:00
parent 781355b477
commit b311b5cf74
18 changed files with 4882 additions and 4392 deletions

View File

@@ -14,21 +14,63 @@ import LocalizedPeerData
import PhotoResources
import CallsEmoji
private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect {
return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t)))
}
private func interpolate(from: CGFloat, to: CGFloat, value: CGFloat) -> CGFloat {
return (1.0 - value) * from + value * to
}
private final class IncomingVideoNode: ASDisplayNode {
private let videoView: UIView
private let videoView: PresentationCallVideoView
private var effectView: UIVisualEffectView?
private var isBlurred: Bool = false
init(videoView: UIView) {
private let isReadyUpdated: () -> Void
private(set) var isReady: Bool = false
private var isReadyTimer: SwiftSignalKit.Timer?
init(videoView: PresentationCallVideoView, isReadyUpdated: @escaping () -> Void) {
self.videoView = videoView
self.isReadyUpdated = isReadyUpdated
super.init()
self.view.addSubview(self.videoView)
self.view.addSubview(self.videoView.view)
self.isReadyTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in
guard let strongSelf = self else {
return
}
if !strongSelf.isReady {
strongSelf.isReady = true
strongSelf.isReadyUpdated()
}
}, queue: .mainQueue())
self.isReadyTimer?.start()
videoView.setOnFirstFrameReceived { [weak self] in
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
if !strongSelf.isReady {
strongSelf.isReady = true
strongSelf.isReadyTimer?.invalidate()
strongSelf.isReadyUpdated()
}
}
}
}
deinit {
self.isReadyTimer?.invalidate()
}
func updateLayout(size: CGSize) {
self.videoView.frame = CGRect(origin: CGPoint(), size: size)
self.videoView.view.frame = CGRect(origin: CGPoint(), size: size)
}
func updateIsBlurred(isBlurred: Bool) {
@@ -41,7 +83,7 @@ private final class IncomingVideoNode: ASDisplayNode {
if self.effectView == nil {
let effectView = UIVisualEffectView()
self.effectView = effectView
effectView.frame = self.videoView.frame
effectView.frame = self.videoView.view.frame
self.view.addSubview(effectView)
}
UIView.animate(withDuration: 0.3, animations: {
@@ -57,26 +99,26 @@ private final class IncomingVideoNode: ASDisplayNode {
private final class OutgoingVideoNode: ASDisplayNode {
private let videoTransformContainer: ASDisplayNode
private let videoView: UIView
private let videoView: PresentationCallVideoView
private let buttonNode: HighlightTrackingButtonNode
private var effectView: UIVisualEffectView?
private var isBlurred: Bool = false
private var isExpanded: Bool = false
private var currentCornerRadius: CGFloat = 0.0
var tapped: (() -> Void)?
init(videoView: UIView) {
init(videoView: PresentationCallVideoView) {
self.videoTransformContainer = ASDisplayNode()
self.videoTransformContainer.clipsToBounds = true
self.videoView = videoView
self.videoView.layer.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
self.videoView.view.layer.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
self.buttonNode = HighlightTrackingButtonNode()
super.init()
self.videoTransformContainer.view.addSubview(self.videoView)
self.videoTransformContainer.view.addSubview(self.videoView.view)
self.addSubnode(self.videoTransformContainer)
//self.addSubnode(self.buttonNode)
@@ -87,10 +129,10 @@ private final class OutgoingVideoNode: ASDisplayNode {
self.tapped?()
}
func updateLayout(size: CGSize, isExpanded: Bool, transition: ContainedViewLayoutTransition) {
func updateLayout(size: CGSize, cornerRadius: CGFloat, transition: ContainedViewLayoutTransition) {
let videoFrame = CGRect(origin: CGPoint(), size: size)
self.buttonNode.frame = videoFrame
self.isExpanded = isExpanded
self.currentCornerRadius = cornerRadius
let previousVideoFrame = self.videoTransformContainer.frame
self.videoTransformContainer.frame = videoFrame
@@ -99,11 +141,11 @@ private final class OutgoingVideoNode: ASDisplayNode {
transition.animateTransformScale(node: self.videoTransformContainer, from: previousVideoFrame.height / videoFrame.height)
}
self.videoView.frame = videoFrame
self.videoView.view.frame = videoFrame
transition.updateCornerRadius(layer: self.videoTransformContainer.layer, cornerRadius: isExpanded ? 0.0 : 16.0)
transition.updateCornerRadius(layer: self.videoTransformContainer.layer, cornerRadius: self.currentCornerRadius)
if let effectView = self.effectView {
transition.updateCornerRadius(layer: effectView.layer, cornerRadius: isExpanded ? 0.0 : 16.0)
transition.updateCornerRadius(layer: effectView.layer, cornerRadius: self.currentCornerRadius)
}
}
@@ -117,9 +159,9 @@ private final class OutgoingVideoNode: ASDisplayNode {
if self.effectView == nil {
let effectView = UIVisualEffectView()
effectView.clipsToBounds = true
effectView.layer.cornerRadius = self.isExpanded ? 0.0 : 16.0
effectView.layer.cornerRadius = self.currentCornerRadius
self.effectView = effectView
effectView.frame = self.videoView.frame
effectView.frame = self.videoView.view.frame
self.view.addSubview(effectView)
}
UIView.animate(withDuration: 0.3, animations: {
@@ -133,7 +175,7 @@ private final class OutgoingVideoNode: ASDisplayNode {
}
}
final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
final class CallControllerNode: ViewControllerTracingNode, CallControllerNodeProtocol {
private enum VideoNodeCorner {
case topLeft
case topRight
@@ -153,6 +195,7 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
private let easyDebugAccess: Bool
private let call: PresentationCall
private let containerTransformationNode: ASDisplayNode
private let containerNode: ASDisplayNode
private let imageNode: TransformImageNode
@@ -202,9 +245,21 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
var callEnded: ((Bool) -> Void)?
var dismissedInteractively: (() -> Void)?
private var buttonsMode: CallControllerButtonsMode?
private var isUIHidden: Bool = false
private var isVideoPaused: Bool = false
private enum PictureInPictureGestureState {
case none
case collapsing(didSelectCorner: Bool)
case dragging(initialPosition: CGPoint, draggingPosition: CGPoint)
}
private var pictureInPictureGestureState: PictureInPictureGestureState = .none
private var pictureInPictureCorner: VideoNodeCorner = .topRight
private var pictureInPictureTransitionFraction: CGFloat = 0.0
init(sharedContext: SharedAccountContext, account: Account, presentationData: PresentationData, statusBar: StatusBar, debugInfo: Signal<(String, String), NoError>, shouldStayHiddenUntilConnection: Bool = false, easyDebugAccess: Bool, call: PresentationCall) {
self.sharedContext = sharedContext
self.account = account
@@ -215,6 +270,9 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
self.easyDebugAccess = easyDebugAccess
self.call = call
self.containerTransformationNode = ASDisplayNode()
self.containerTransformationNode.clipsToBounds = true
self.containerNode = ASDisplayNode()
if self.shouldStayHiddenUntilConnection {
self.containerNode.alpha = 0.0
@@ -242,13 +300,10 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.containerNode.backgroundColor = .black
self.addSubnode(self.containerNode)
self.addSubnode(self.containerTransformationNode)
self.containerTransformationNode.addSubnode(self.containerNode)
self.backButtonNode.setTitle(presentationData.strings.Common_Back, with: Font.regular(17.0), with: .white, for: [])
self.backButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -20.0, bottom: -8.0, right: -8.0)
@@ -377,7 +432,14 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
return
}
if let incomingVideoView = incomingVideoView {
let incomingVideoNode = IncomingVideoNode(videoView: incomingVideoView)
let incomingVideoNode = IncomingVideoNode(videoView: incomingVideoView, isReadyUpdated: {
guard let strongSelf = self else {
return
}
if let (layout, navigationBarHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.5, curve: .spring))
}
})
strongSelf.incomingVideoNode = incomingVideoNode
strongSelf.containerNode.insertSubnode(incomingVideoNode, aboveSubnode: strongSelf.dimNode)
if let (layout, navigationBarHeight) = strongSelf.validLayout {
@@ -399,8 +461,8 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
return
}
if let outgoingVideoView = outgoingVideoView {
outgoingVideoView.backgroundColor = .black
outgoingVideoView.clipsToBounds = true
outgoingVideoView.view.backgroundColor = .black
outgoingVideoView.view.clipsToBounds = true
if let audioOutputState = strongSelf.audioOutputState, let currentOutput = audioOutputState.currentOutput {
switch currentOutput {
case .speaker, .builtin:
@@ -436,6 +498,7 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
}
if let incomingVideoNode = self.incomingVideoNode {
incomingVideoNode.isHidden = !incomingVideoNode.isReady
let isActive: Bool
switch callState.remoteVideoState {
case .inactive:
@@ -457,37 +520,42 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
switch callState.state {
case .waiting, .connecting:
statusValue = .text(self.presentationData.strings.Call_StatusConnecting)
statusValue = .text(string: self.presentationData.strings.Call_StatusConnecting, displayLogo: false)
case let .requesting(ringing):
if ringing {
statusValue = .text(self.presentationData.strings.Call_StatusRinging)
statusValue = .text(string: self.presentationData.strings.Call_StatusRinging, displayLogo: false)
} else {
statusValue = .text(self.presentationData.strings.Call_StatusRequesting)
statusValue = .text(string: self.presentationData.strings.Call_StatusRequesting, displayLogo: false)
}
case .terminating:
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
statusValue = .text(string: self.presentationData.strings.Call_StatusEnded, displayLogo: false)
case let .terminated(_, reason, _):
if let reason = reason {
switch reason {
case let .ended(type):
switch type {
case .busy:
statusValue = .text(self.presentationData.strings.Call_StatusBusy)
statusValue = .text(string: self.presentationData.strings.Call_StatusBusy, displayLogo: false)
case .hungUp, .missed:
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
statusValue = .text(string: self.presentationData.strings.Call_StatusEnded, displayLogo: false)
}
case .error:
statusValue = .text(self.presentationData.strings.Call_StatusFailed)
statusValue = .text(string: self.presentationData.strings.Call_StatusFailed, displayLogo: false)
}
} else {
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
statusValue = .text(string: self.presentationData.strings.Call_StatusEnded, displayLogo: false)
}
case .ringing:
var text = self.presentationData.strings.Call_StatusIncoming
var text: String
if self.call.isVideo {
text = self.presentationData.strings.Call_IncomingVideoCall
} else {
text = self.presentationData.strings.Call_IncomingVoiceCall
}
if !self.statusNode.subtitle.isEmpty {
text += "\n\(self.statusNode.subtitle)"
}
statusValue = .text(text)
statusValue = .text(string: text, displayLogo: true)
case .active(let timestamp, let reception, let keyVisualHash), .reconnecting(let timestamp, let reception, let keyVisualHash):
let strings = self.presentationData.strings
var isReconnecting = false
@@ -517,28 +585,6 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
}
statusReception = reception
}
switch callState.state {
case .terminated, .terminating:
if !self.statusNode.alpha.isEqual(to: 0.5) {
self.statusNode.alpha = 0.5
self.buttonsNode.alpha = 0.5
self.keyButtonNode.alpha = 0.5
self.backButtonArrowNode.alpha = 0.5
self.backButtonNode.alpha = 0.5
self.statusNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
self.buttonsNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
self.keyButtonNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
}
default:
if !self.statusNode.alpha.isEqual(to: 1.0) {
self.statusNode.alpha = 1.0
self.buttonsNode.alpha = 1.0
self.keyButtonNode.alpha = 1.0
self.backButtonArrowNode.alpha = 1.0
self.backButtonNode.alpha = 1.0
}
}
if self.shouldStayHiddenUntilConnection {
switch callState.state {
case .connecting, .active:
@@ -550,6 +596,15 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
self.statusNode.status = statusValue
self.statusNode.reception = statusReception
if let callState = self.callState {
switch callState.state {
case .active, .connecting, .reconnecting:
break
default:
self.isUIHidden = false
}
}
self.updateButtonsMode()
if case let .terminated(id, _, reportRating) = callState.state, let callId = id {
@@ -598,24 +653,27 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
switch callState.state {
case .ringing:
let buttonsMode: CallControllerButtonsMode = .incoming(speakerMode: mode, videoState: mappedVideoState)
self.buttonsNode.updateMode(strings: self.presentationData.strings, mode: buttonsMode)
self.buttonsMode = .incoming(speakerMode: mode, videoState: mappedVideoState)
self.buttonsTerminationMode = buttonsMode
case .waiting, .requesting:
let buttonsMode: CallControllerButtonsMode = .outgoingRinging(speakerMode: mode, videoState: mappedVideoState)
self.buttonsNode.updateMode(strings: self.presentationData.strings, mode: buttonsMode)
self.buttonsMode = .outgoingRinging(speakerMode: mode, videoState: mappedVideoState)
self.buttonsTerminationMode = buttonsMode
case .active, .connecting, .reconnecting:
let buttonsMode: CallControllerButtonsMode = .active(speakerMode: mode, videoState: mappedVideoState)
self.buttonsNode.updateMode(strings: self.presentationData.strings, mode: buttonsMode)
self.buttonsMode = .active(speakerMode: mode, videoState: mappedVideoState)
self.buttonsTerminationMode = buttonsMode
case .terminating, .terminated:
if let buttonsTerminationMode = self.buttonsTerminationMode {
self.buttonsNode.updateMode(strings: self.presentationData.strings, mode: buttonsTerminationMode)
self.buttonsMode = buttonsTerminationMode
} else {
self.buttonsNode.updateMode(strings: self.presentationData.strings, mode: .active(speakerMode: mode, videoState: mappedVideoState))
self.buttonsMode = .active(speakerMode: mode, videoState: mappedVideoState)
}
}
if let (layout, navigationHeight) = self.validLayout {
self.pictureInPictureTransitionFraction = 0.0
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
}
}
func animateIn() {
@@ -645,70 +703,110 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
}
}
func expandFromPipIfPossible() {
if self.pictureInPictureTransitionFraction.isEqual(to: 1.0), let (layout, navigationHeight) = self.validLayout {
self.pictureInPictureTransitionFraction = 0.0
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
}
}
private func calculatePreviewVideoRect(layout: ContainerViewLayout, navigationHeight: CGFloat) -> CGRect {
let buttonsHeight: CGFloat = 190.0
let buttonsOffset: CGFloat
if layout.size.width.isEqual(to: 320.0) {
if layout.size.height.isEqual(to: 480.0) {
buttonsOffset = 60.0
} else {
buttonsOffset = 73.0
}
} else {
buttonsOffset = 83.0
}
var uiDisplayTransition: CGFloat = self.isUIHidden ? 0.0 : 1.0
uiDisplayTransition *= 1.0 - self.pictureInPictureTransitionFraction
let buttonsOriginY: CGFloat
if self.isUIHidden {
buttonsOriginY = layout.size.height + 40.0 - 80.0
} else {
buttonsOriginY = layout.size.height - (buttonsOffset - 40.0) - buttonsHeight - layout.intrinsicInsets.bottom
}
let buttonsHeight: CGFloat = self.buttonsNode.bounds.height
let previewVideoSize = layout.size.aspectFitted(CGSize(width: 200.0, height: 200.0))
var insets = layout.insets(options: .statusBar)
insets.top += 44.0 + 8.0
insets.bottom = buttonsHeight + 27.0
insets.left = 20.0
insets.right = 20.0
let expandedInset: CGFloat = 16.0
insets.top = interpolate(from: expandedInset, to: insets.top, value: uiDisplayTransition)
insets.bottom = interpolate(from: expandedInset, to: insets.bottom, value: uiDisplayTransition)
insets.left = interpolate(from: expandedInset, to: insets.left, value: 1.0 - self.pictureInPictureTransitionFraction)
insets.right = interpolate(from: expandedInset, to: insets.right, value: 1.0 - self.pictureInPictureTransitionFraction)
let previewVideoSide = interpolate(from: 350.0, to: 200.0, value: 1.0 - self.pictureInPictureTransitionFraction)
let previewVideoSize = layout.size.aspectFitted(CGSize(width: previewVideoSide, height: previewVideoSide))
let previewVideoY: CGFloat
let previewVideoX: CGFloat
switch self.outgoingVideoNodeCorner {
case .topLeft:
previewVideoX = 20.0
if self.isUIHidden {
previewVideoY = layout.insets(options: .statusBar).top + 8.0
} else {
previewVideoY = layout.insets(options: .statusBar).top + 44.0 + 8.0
}
previewVideoX = insets.left
previewVideoY = insets.top
case .topRight:
previewVideoX = layout.size.width - previewVideoSize.width - 20.0
if self.isUIHidden {
previewVideoY = layout.insets(options: .statusBar).top + 8.0
} else {
previewVideoY = layout.insets(options: .statusBar).top + 44.0 + 8.0
}
previewVideoX = layout.size.width - previewVideoSize.width - insets.right
previewVideoY = insets.top
case .bottomLeft:
previewVideoX = 20.0
if self.isUIHidden {
previewVideoY = layout.size.height - layout.intrinsicInsets.bottom - 8.0 - previewVideoSize.height
} else {
previewVideoY = buttonsOriginY + 100.0 - previewVideoSize.height
}
previewVideoX = insets.left
previewVideoY = layout.size.height - insets.bottom - previewVideoSize.height
case .bottomRight:
previewVideoX = layout.size.width - previewVideoSize.width - 20.0
if self.isUIHidden {
previewVideoY = layout.size.height - layout.intrinsicInsets.bottom - 8.0 - previewVideoSize.height
} else {
previewVideoY = buttonsOriginY + 100.0 - previewVideoSize.height
}
previewVideoX = layout.size.width - previewVideoSize.width - insets.right
previewVideoY = layout.size.height - insets.bottom - previewVideoSize.height
}
return CGRect(origin: CGPoint(x: previewVideoX, y: previewVideoY), size: previewVideoSize)
}
private func calculatePictureInPictureContainerRect(layout: ContainerViewLayout, navigationHeight: CGFloat) -> CGRect {
let pictureInPictureTopInset: CGFloat = layout.insets(options: .statusBar).top + 44.0 + 8.0
let pictureInPictureSideInset: CGFloat = 8.0
let pictureInPictureSize = layout.size.fitted(CGSize(width: 240.0, height: 240.0))
let pictureInPictureBottomInset: CGFloat = layout.insets(options: .input).bottom + 44.0 + 8.0
let containerPictureInPictureFrame: CGRect
switch self.pictureInPictureCorner {
case .topLeft:
containerPictureInPictureFrame = CGRect(origin: CGPoint(x: pictureInPictureSideInset, y: pictureInPictureTopInset), size: pictureInPictureSize)
case .topRight:
containerPictureInPictureFrame = CGRect(origin: CGPoint(x: layout.size.width - pictureInPictureSideInset - pictureInPictureSize.width, y: pictureInPictureTopInset), size: pictureInPictureSize)
case .bottomLeft:
containerPictureInPictureFrame = CGRect(origin: CGPoint(x: pictureInPictureSideInset, y: layout.size.height - pictureInPictureBottomInset - pictureInPictureSize.height), size: pictureInPictureSize)
case .bottomRight:
containerPictureInPictureFrame = CGRect(origin: CGPoint(x: layout.size.width - pictureInPictureSideInset - pictureInPictureSize.width, y: layout.size.height - pictureInPictureBottomInset - pictureInPictureSize.height), size: pictureInPictureSize)
}
return containerPictureInPictureFrame
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
let overlayAlpha: CGFloat = self.isUIHidden ? 0.0 : 1.0
var uiDisplayTransition: CGFloat = self.isUIHidden ? 0.0 : 1.0
uiDisplayTransition *= 1.0 - self.pictureInPictureTransitionFraction
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let buttonsHeight: CGFloat
if let buttonsMode = self.buttonsMode {
buttonsHeight = self.buttonsNode.updateLayout(strings: self.presentationData.strings, mode: buttonsMode, constrainedWidth: layout.size.width, bottomInset: layout.intrinsicInsets.bottom, transition: transition)
} else {
buttonsHeight = 0.0
}
let defaultButtonsOriginY = layout.size.height - buttonsHeight
let buttonsOriginY = interpolate(from: layout.size.height + 10.0, to: defaultButtonsOriginY, value: uiDisplayTransition)
var overlayAlpha: CGFloat = uiDisplayTransition
switch self.callState?.state {
case .terminated, .terminating:
overlayAlpha *= 0.5
default:
break
}
let containerFullScreenFrame = CGRect(origin: CGPoint(), size: layout.size)
let containerPictureInPictureFrame = self.calculatePictureInPictureContainerRect(layout: layout, navigationHeight: navigationBarHeight)
let containerFrame = interpolateFrame(from: containerFullScreenFrame, to: containerPictureInPictureFrame, t: self.pictureInPictureTransitionFraction)
transition.updateFrame(node: self.containerTransformationNode, frame: containerFrame)
transition.updateSublayerTransformScale(node: self.containerTransformationNode, scale: min(1.0, containerFrame.width / layout.size.width * 1.01))
transition.updateCornerRadius(layer: self.containerTransformationNode.layer, cornerRadius: self.pictureInPictureTransitionFraction * 10.0)
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: (containerFrame.width - layout.size.width) / 2.0, y: floor(containerFrame.height - layout.size.height) / 2.0), size: layout.size))
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
if let keyPreviewNode = self.keyPreviewNode {
@@ -751,18 +849,6 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
statusOffset += layout.safeInsets.top
let buttonsHeight: CGFloat = 190.0
let buttonsOffset: CGFloat
if layout.size.width.isEqual(to: 320.0) {
if layout.size.height.isEqual(to: 480.0) {
buttonsOffset = 60.0
} else {
buttonsOffset = 73.0
}
} else {
buttonsOffset = 83.0
}
let statusHeight = self.statusNode.updateLayout(constrainedWidth: layout.size.width, transition: transition)
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: 0.0, y: statusOffset), size: CGSize(width: layout.size.width, height: statusHeight)))
transition.updateAlpha(node: self.statusNode, alpha: overlayAlpha)
@@ -770,13 +856,6 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
let videoPausedSize = self.videoPausedNode.updateLayout(CGSize(width: layout.size.width - 16.0, height: 100.0))
transition.updateFrame(node: self.videoPausedNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - videoPausedSize.width) / 2.0), y: floor((layout.size.height - videoPausedSize.height) / 2.0)), size: videoPausedSize))
self.buttonsNode.updateLayout(strings: self.presentationData.strings, constrainedWidth: layout.size.width, transition: transition)
let buttonsOriginY: CGFloat
if self.isUIHidden {
buttonsOriginY = layout.size.height + 40.0 - 80.0
} else {
buttonsOriginY = layout.size.height - (buttonsOffset - 40.0) - buttonsHeight - layout.intrinsicInsets.bottom
}
transition.updateFrame(node: self.buttonsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: buttonsOriginY), size: CGSize(width: layout.size.width, height: buttonsHeight)))
transition.updateAlpha(node: self.buttonsNode, alpha: overlayAlpha)
@@ -801,18 +880,18 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
if outgoingVideoNode.frame.isEmpty {
outgoingVideoTransition = .immediate
}
if self.incomingVideoNode == nil {
outgoingVideoNode.frame = fullscreenVideoFrame
outgoingVideoNode.updateLayout(size: layout.size, isExpanded: true, transition: outgoingVideoTransition)
} else {
if let incomingVideoNode = self.incomingVideoNode, incomingVideoNode.isReady {
if self.minimizedVideoDraggingPosition == nil {
if self.outgoingVideoExplicitelyFullscreen {
outgoingVideoTransition.updateFrame(node: outgoingVideoNode, frame: fullscreenVideoFrame)
} else {
outgoingVideoTransition.updateFrame(node: outgoingVideoNode, frame: previewVideoFrame)
}
outgoingVideoNode.updateLayout(size: outgoingVideoNode.frame.size, isExpanded: self.outgoingVideoExplicitelyFullscreen, transition: outgoingVideoTransition)
outgoingVideoNode.updateLayout(size: outgoingVideoNode.frame.size, cornerRadius: interpolate(from: self.outgoingVideoExplicitelyFullscreen ? 0.0 : 14.0, to: 24.0, value: self.pictureInPictureTransitionFraction), transition: outgoingVideoTransition)
}
} else {
outgoingVideoNode.frame = fullscreenVideoFrame
outgoingVideoNode.updateLayout(size: layout.size, cornerRadius: 0.0, transition: outgoingVideoTransition)
}
}
@@ -861,12 +940,27 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let _ = self.keyPreviewNode {
if !self.pictureInPictureTransitionFraction.isZero {
if let (layout, navigationHeight) = self.validLayout {
self.pictureInPictureTransitionFraction = 0.0
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
}
} else if let _ = self.keyPreviewNode {
self.backPressed()
} else {
if self.incomingVideoNode != nil || self.outgoingVideoNode != nil {
self.isUIHidden = !self.isUIHidden
if let (layout, navigationBarHeight) = self.validLayout {
var updated = false
if let callState = self.callState {
switch callState.state {
case .active, .connecting, .reconnecting:
self.isUIHidden = !self.isUIHidden
updated = true
default:
break
}
}
if updated, let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
}
} else {
@@ -1034,13 +1128,15 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
switch recognizer.state {
case .began:
let location = recognizer.location(in: self.view)
//let translation = recognizer.translation(in: self.view)
//location.x += translation.x
//location.y += translation.y
if let _ = self.incomingVideoNode, let outgoingVideoNode = self.outgoingVideoNode, outgoingVideoNode.frame.contains(location) {
if self.self.pictureInPictureTransitionFraction.isZero, let _ = self.incomingVideoNode, let outgoingVideoNode = self.outgoingVideoNode, outgoingVideoNode.frame.contains(location) {
self.minimizedVideoInitialPosition = outgoingVideoNode.position
} else {
self.minimizedVideoInitialPosition = nil
if !self.pictureInPictureTransitionFraction.isZero {
self.pictureInPictureGestureState = .dragging(initialPosition: self.containerTransformationNode.position, draggingPosition: self.containerTransformationNode.position)
} else {
self.pictureInPictureGestureState = .collapsing(didSelectCorner: false)
}
}
case .changed:
if let outgoingVideoNode = self.outgoingVideoNode, let minimizedVideoInitialPosition = self.minimizedVideoInitialPosition {
@@ -1049,10 +1145,43 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
self.minimizedVideoDraggingPosition = minimizedVideoDraggingPosition
outgoingVideoNode.position = minimizedVideoDraggingPosition
} else {
let offset = recognizer.translation(in: self.view).y
var bounds = self.bounds
bounds.origin.y = -offset
self.bounds = bounds
switch self.pictureInPictureGestureState {
case .none:
let offset = recognizer.translation(in: self.view).y
var bounds = self.bounds
bounds.origin.y = -offset
self.bounds = bounds
case let .collapsing(didSelectCorner):
if let (layout, navigationHeight) = self.validLayout {
let offset = recognizer.translation(in: self.view)
if !didSelectCorner {
self.pictureInPictureGestureState = .collapsing(didSelectCorner: true)
if offset.x < 0.0 {
self.pictureInPictureCorner = .topLeft
} else {
self.pictureInPictureCorner = .topRight
}
}
let maxOffset: CGFloat = min(300.0, layout.size.height / 2.0)
let offsetTransition = max(0.0, min(1.0, abs(offset.y) / maxOffset))
self.pictureInPictureTransitionFraction = offsetTransition
switch self.pictureInPictureCorner {
case .topRight, .bottomRight:
self.pictureInPictureCorner = offset.y < 0.0 ? .topRight : .bottomRight
case .topLeft, .bottomLeft:
self.pictureInPictureCorner = offset.y < 0.0 ? .topLeft : .bottomLeft
}
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
case .dragging(let initialPosition, var draggingPosition):
let translation = recognizer.translation(in: self.view)
draggingPosition.x = initialPosition.x + translation.x
draggingPosition.y = initialPosition.y + translation.y
self.pictureInPictureGestureState = .dragging(initialPosition: initialPosition, draggingPosition: draggingPosition)
self.containerTransformationNode.position = draggingPosition
}
}
case .cancelled, .ended:
if let outgoingVideoNode = self.outgoingVideoNode, let _ = self.minimizedVideoInitialPosition, let minimizedVideoDraggingPosition = self.minimizedVideoDraggingPosition {
@@ -1067,25 +1196,62 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
outgoingVideoNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: minimizedVideoDraggingPosition.x - videoFrame.midX, y: minimizedVideoDraggingPosition.y - videoFrame.midY)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, delay: 0.0, initialVelocity: 0.0, damping: 110.0, removeOnCompletion: true, additive: true, completion: nil)
}
} else {
let velocity = recognizer.velocity(in: self.view).y
if abs(velocity) < 100.0 {
var bounds = self.bounds
let previous = bounds
bounds.origin = CGPoint()
self.bounds = bounds
self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
} else {
var bounds = self.bounds
let previous = bounds
bounds.origin = CGPoint(x: 0.0, y: velocity > 0.0 ? -bounds.height: bounds.height)
self.bounds = bounds
self.layer.animateBounds(from: previous, to: bounds, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { [weak self] _ in
self?.dismissedInteractively?()
})
switch self.pictureInPictureGestureState {
case .none:
let velocity = recognizer.velocity(in: self.view).y
if abs(velocity) < 100.0 {
var bounds = self.bounds
let previous = bounds
bounds.origin = CGPoint()
self.bounds = bounds
self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
} else {
var bounds = self.bounds
let previous = bounds
bounds.origin = CGPoint(x: 0.0, y: velocity > 0.0 ? -bounds.height: bounds.height)
self.bounds = bounds
self.layer.animateBounds(from: previous, to: bounds, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { [weak self] _ in
self?.dismissedInteractively?()
})
}
case .collapsing:
self.pictureInPictureGestureState = .none
let velocity = recognizer.velocity(in: self.view).y
if abs(velocity) < 100.0 {
if let (layout, navigationHeight) = self.validLayout {
self.pictureInPictureTransitionFraction = 0.0
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
}
} else {
if let (layout, navigationHeight) = self.validLayout {
self.pictureInPictureTransitionFraction = 1.0
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
}
}
case let .dragging(initialPosition, _):
self.pictureInPictureGestureState = .none
if let (layout, navigationHeight) = self.validLayout {
let translation = recognizer.translation(in: self.view)
let draggingPosition = CGPoint(x: initialPosition.x + translation.x, y: initialPosition.y + translation.y)
self.pictureInPictureCorner = self.nodeLocationForPosition(layout: layout, position: draggingPosition, velocity: recognizer.velocity(in: self.view))
let containerFrame = self.calculatePictureInPictureContainerRect(layout: layout, navigationHeight: navigationHeight)
self.containerTransformationNode.frame = containerFrame
containerTransformationNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: draggingPosition.x - containerFrame.midX, y: draggingPosition.y - containerFrame.midY)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, delay: 0.0, initialVelocity: 0.0, damping: 110.0, removeOnCompletion: true, additive: true, completion: nil)
}
}
}
default:
break
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.containerTransformationNode.frame.contains(point) {
return self.containerTransformationNode.view.hitTest(self.view.convert(point, to: self.containerTransformationNode.view), with: event)
}
return nil
}
}