mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 14:20:20 +00:00
Video call screen improvements
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user