PiP improvements

This commit is contained in:
Ali 2020-06-26 19:53:15 +04:00
parent 682ae8a967
commit 3f9322d105
14 changed files with 370 additions and 99 deletions

View File

@ -29,6 +29,7 @@ public protocol MediaManager: class {
func filteredPlaylistState(accountId: AccountRecordId, playlistId: SharedMediaPlaylistId, itemId: SharedMediaPlaylistItemId, type: MediaManagerPlayerType) -> Signal<SharedMediaPlayerItemPlaybackState?, NoError>
func setOverlayVideoNode(_ node: OverlayMediaItemNode?)
func hasOverlayVideoNode(_ node: OverlayMediaItemNode) -> Bool
func audioRecorder(beginWithTone: Bool, applicationBindings: TelegramApplicationBindings, beganWithTone: @escaping (Bool) -> Void) -> Signal<ManagedAudioRecorder?, NoError>
}

View File

@ -21,6 +21,9 @@ open class OverlayMediaItemNode: ASDisplayNode {
open var unminimize: (() -> Void)?
public var manualExpandEmbed: (() -> Void)?
public var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)?
open var group: OverlayMediaItemNodeGroup? {
return nil
}

View File

@ -228,6 +228,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
let velocity = recognizer.velocity(in: self.view).x
if velocity > 1000 || navigationTransitionCoordinator.progress > 0.2 {
self.state.top?.value.viewWillLeaveNavigation()
navigationTransitionCoordinator.animateCompletion(velocity, completion: { [weak self] in
guard let strongSelf = self, let _ = strongSelf.state.layout, let _ = strongSelf.state.transition, let top = strongSelf.state.top else {
return
@ -399,6 +400,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
})
}
fromValue.value.viewWillLeaveNavigation()
fromValue.value.viewWillDisappear(true)
toValue.value.viewWillAppear(true)
toValue.value.setIgnoreAppearanceMethodInvocations(true)
@ -464,6 +466,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
})
} else {
if let fromValue = fromValue {
fromValue.value.viewWillLeaveNavigation()
fromValue.value.viewWillDisappear(false)
fromValue.value.setIgnoreAppearanceMethodInvocations(true)
fromValue.value.displayNode.removeFromSupernode()

View File

@ -117,6 +117,7 @@ open class NavigationBar: ASDisplayNode {
public var userInfo: Any?
public var makeCustomTransitionNode: ((NavigationBar, Bool) -> CustomNavigationTransitionNode?)?
public var allowsCustomTransition: (() -> Bool)?
private var collapsed: Bool {
get {

View File

@ -561,6 +561,9 @@ public enum TabBarItemContextActionType {
super.viewDidDisappear(animated)
}
open func viewWillLeaveNavigation() {
}
open override func viewDidAppear(_ animated: Bool) {
self.activeInputView = nil

View File

@ -290,6 +290,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
var playbackCompleted: (() -> Void)?
private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)?
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context
self.presentationData = presentationData
@ -423,6 +425,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
if let _ = self.customUnembedWhenPortrait, layout.size.width < layout.size.height {
self.expandIntoCustomPiP()
}
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
var dismiss = false
@ -888,6 +894,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
if let node = node.0 as? OverlayMediaItemNode {
self.customUnembedWhenPortrait = node.customUnembedWhenPortrait
node.customUnembedWhenPortrait = nil
}
if let node = node.0 as? OverlayMediaItemNode, self.context.sharedContext.mediaManager.hasOverlayVideoNode(node) {
var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view)
let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview)
@ -960,10 +971,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false)
}
videoNode.allowsGroupOpacity = true
videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak videoNode] _ in
videoNode?.allowsGroupOpacity = false
})
if surfaceCopyView.superview != nil {
videoNode.allowsGroupOpacity = true
videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak videoNode] _ in
videoNode?.allowsGroupOpacity = false
})
}
videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
transformedFrame.origin = CGPoint()
@ -1272,9 +1285,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
@objc func pictureInPictureButtonPressed() {
if let item = self.item, let videoNode = self.videoNode {
private func expandIntoCustomPiP() {
if let item = self.item, let videoNode = self.videoNode, let customUnembedWhenPortrait = customUnembedWhenPortrait {
self.customUnembedWhenPortrait = nil
videoNode.setContinuePlayingWithoutSoundOnLostAudioSession(false)
let context = self.context
@ -1306,9 +1319,77 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode {
return GalleryTransitionArguments(transitionNode: (overlayNode, overlayNode.bounds, { [weak overlayNode] in
return (overlayNode?.view.snapshotContentTree(), nil)
}), addToTransitionSurface: { [weak overlaySupernode, weak overlayNode] view in
overlaySupernode?.view.addSubview(view)
overlayNode?.canAttachContent = false
}), addToTransitionSurface: { [weak context, weak overlaySupernode, weak overlayNode] view in
guard let context = context, let overlayNode = overlayNode else {
return
}
if context.sharedContext.mediaManager.hasOverlayVideoNode(overlayNode) {
overlaySupernode?.view.addSubview(view)
}
overlayNode.canAttachContent = false
})
} else if let info = context.sharedContext.mediaManager.galleryHiddenMediaManager.findTarget(messageId: id, media: media) {
return GalleryTransitionArguments(transitionNode: (info.1, info.1.bounds, {
return info.2()
}), addToTransitionSurface: info.0)
}
return nil
}))
case .webPage:
break
}
}
if customUnembedWhenPortrait(overlayNode) {
self.beginCustomDismiss()
self.statusNode.isHidden = true
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in
self?.completeCustomDismiss()
})
}
}
}
@objc func pictureInPictureButtonPressed() {
if let item = self.item, let videoNode = self.videoNode {
videoNode.setContinuePlayingWithoutSoundOnLostAudioSession(false)
let context = self.context
let baseNavigationController = self.baseNavigationController()
let mediaManager = self.context.sharedContext.mediaManager
var expandImpl: (() -> Void)?
let overlayNode = OverlayUniversalVideoNode(postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, expand: {
expandImpl?()
}, close: { [weak mediaManager] in
mediaManager?.setOverlayVideoNode(nil)
})
expandImpl = { [weak overlayNode] in
guard let contentInfo = item.contentInfo, let overlayNode = overlayNode else {
return
}
switch contentInfo {
case let .message(message):
let gallery = GalleryController(context: context, source: .peerMessagesAtId(message.id), replaceRootController: { controller, ready in
if let baseNavigationController = baseNavigationController {
baseNavigationController.replaceTopController(controller, animated: false, ready: ready)
}
}, baseNavigationController: baseNavigationController)
gallery.temporaryDoNotWaitForReady = true
baseNavigationController?.view.endEditing(true)
(baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { [weak overlayNode] id, media in
if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode {
return GalleryTransitionArguments(transitionNode: (overlayNode, overlayNode.bounds, { [weak overlayNode] in
return (overlayNode?.view.snapshotContentTree(), nil)
}), addToTransitionSurface: { [weak context, weak overlaySupernode, weak overlayNode] view in
guard let context = context, let overlayNode = overlayNode else {
return
}
if context.sharedContext.mediaManager.hasOverlayVideoNode(overlayNode) {
overlaySupernode?.view.addSubview(view)
}
overlayNode.canAttachContent = false
})
} else if let info = context.sharedContext.mediaManager.galleryHiddenMediaManager.findTarget(messageId: id, media: media) {
return GalleryTransitionArguments(transitionNode: (info.1, info.1.bounds, {

View File

@ -45,9 +45,9 @@ public extension TabBarControllerTheme {
}
public extension NavigationBarTheme {
convenience init(rootControllerTheme: PresentationTheme, hideBackground: Bool = false) {
convenience init(rootControllerTheme: PresentationTheme, hideBackground: Bool = false, hideBadge: Bool = false) {
let theme = rootControllerTheme.rootController.navigationBar
self.init(buttonColor: theme.buttonColor, disabledButtonColor: theme.disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.backgroundColor, separatorColor: hideBackground ? .clear : theme.separatorColor, badgeBackgroundColor: theme.badgeBackgroundColor, badgeStrokeColor: hideBackground ? .clear : theme.badgeStrokeColor, badgeTextColor: theme.badgeTextColor)
self.init(buttonColor: theme.buttonColor, disabledButtonColor: theme.disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.backgroundColor, separatorColor: hideBackground ? .clear : theme.separatorColor, badgeBackgroundColor: hideBadge ? .clear : theme.badgeBackgroundColor, badgeStrokeColor: hideBadge ? .clear : theme.badgeStrokeColor, badgeTextColor: hideBadge ? .clear : theme.badgeTextColor)
}
}
@ -62,8 +62,8 @@ public extension NavigationBarPresentationData {
self.init(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme), strings: NavigationBarStrings(presentationStrings: presentationData.strings))
}
convenience init(presentationData: PresentationData, hideBackground: Bool) {
self.init(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme, hideBackground: hideBackground), strings: NavigationBarStrings(presentationStrings: presentationData.strings))
convenience init(presentationData: PresentationData, hideBackground: Bool, hideBadge: Bool) {
self.init(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme, hideBackground: hideBackground, hideBadge: hideBadge), strings: NavigationBarStrings(presentationStrings: presentationData.strings))
}
convenience init(presentationTheme: PresentationTheme, presentationStrings: PresentationStrings) {

View File

@ -322,6 +322,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private let peekTimerDisposable = MetaDisposable()
private var hasEmbeddedTitleContent = false
private var isEmbeddedTitleContentHidden = false
public override var customData: Any? {
return self.chatLocation
@ -375,7 +376,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .inline:
navigationBarPresentationData = nil
default:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: true)
navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: true, hideBadge: false)
}
super.init(context: context, navigationBarPresentationData: navigationBarPresentationData, mediaAccessoryPanelVisibility: mediaAccessoryPanelVisibility, locationBroadcastPanelSource: locationBroadcastPanelSource)
@ -2059,6 +2060,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if case let .peer(peerId) = chatLocation, peerId != context.account.peerId, subject != .scheduledMessages {
self.navigationBar?.userInfo = PeerInfoNavigationSourceTag(peerId: peerId)
}
self.navigationBar?.allowsCustomTransition = { [weak self] in
guard let strongSelf = self else {
return false
}
return !strongSelf.chatDisplayNode.hasEmbeddedTitleContent
}
self.chatTitleView = ChatTitleView(account: self.context.account, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder)
self.navigationItem.titleView = self.chatTitleView
@ -2799,9 +2806,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let navigationBarTheme: NavigationBarTheme
if self.hasEmbeddedTitleContent {
navigationBarTheme = NavigationBarTheme(rootControllerTheme: defaultDarkPresentationTheme, hideBackground: true)
navigationBarTheme = NavigationBarTheme(rootControllerTheme: defaultDarkPresentationTheme, hideBackground: true, hideBadge: true)
} else {
navigationBarTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme, hideBackground: true)
navigationBarTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme, hideBackground: true, hideBadge: false)
}
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
@ -4750,10 +4757,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}))
}
self.chatDisplayNode.updateHasEmbeddedTitleContent = { [weak self] hasEmbeddedTitleContent in
self.chatDisplayNode.updateHasEmbeddedTitleContent = { [weak self] in
guard let strongSelf = self else {
return
}
let hasEmbeddedTitleContent = strongSelf.chatDisplayNode.hasEmbeddedTitleContent
let isEmbeddedTitleContentHidden = strongSelf.chatDisplayNode.isEmbeddedTitleContentHidden
if strongSelf.hasEmbeddedTitleContent != hasEmbeddedTitleContent {
strongSelf.hasEmbeddedTitleContent = hasEmbeddedTitleContent
@ -4774,6 +4785,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
strongSelf.updateNavigationBarPresentation()
}
if strongSelf.isEmbeddedTitleContentHidden != isEmbeddedTitleContentHidden {
strongSelf.isEmbeddedTitleContentHidden = isEmbeddedTitleContentHidden
if let navigationBar = strongSelf.navigationBar {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
transition.updateAlpha(node: navigationBar, alpha: isEmbeddedTitleContentHidden ? 0.0 : 1.0)
}
}
}
self.interfaceInteraction = interfaceInteraction
@ -5168,6 +5188,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
override public func viewWillLeaveNavigation() {
self.chatDisplayNode.willNavigateAway()
}
override public func inFocusUpdated(isInFocus: Bool) {
self.chatDisplayNode.inFocusUpdated(isInFocus: isInFocus)
}

View File

@ -69,6 +69,9 @@ private final class ChatEmbeddedTitleContentNode: ASDisplayNode {
private let context: AccountContext
private let backgroundNode: ASDisplayNode
private let videoNode: OverlayUniversalVideoNode
private let disableInternalAnimationIn: Bool
private let isUIHiddenUpdated: () -> Void
private let unembedWhenPortrait: (OverlayMediaItemNode) -> Bool
private var validLayout: (CGSize, CGFloat, CGFloat)?
@ -78,9 +81,14 @@ private final class ChatEmbeddedTitleContentNode: ASDisplayNode {
private(set) var interactiveExtension: CGFloat = 0.0
private var freezeInteractiveExtension = false
init(context: AccountContext, videoNode: OverlayUniversalVideoNode, interactiveExtensionUpdated: @escaping (ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void) {
private(set) var isUIHidden: Bool = false
init(context: AccountContext, videoNode: OverlayUniversalVideoNode, disableInternalAnimationIn: Bool, interactiveExtensionUpdated: @escaping (ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void, isUIHiddenUpdated: @escaping () -> Void, unembedWhenPortrait: @escaping (OverlayMediaItemNode) -> Bool) {
self.dismissed = dismissed
self.interactiveExtensionUpdated = interactiveExtensionUpdated
self.isUIHiddenUpdated = isUIHiddenUpdated
self.unembedWhenPortrait = unembedWhenPortrait
self.disableInternalAnimationIn = disableInternalAnimationIn
self.context = context
@ -96,6 +104,14 @@ private final class ChatEmbeddedTitleContentNode: ASDisplayNode {
self.addSubnode(self.backgroundNode)
self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
self.videoNode.controlsAreShowingUpdated = { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.isUIHidden = !value
strongSelf.isUIHiddenUpdated()
}
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
@ -116,7 +132,7 @@ private final class ChatEmbeddedTitleContentNode: ASDisplayNode {
if translation.y > 80.0 {
self.freezeInteractiveExtension = true
self.videoNode.customExpand?()
self.expandIntoPiP()
} else {
self.interactiveExtension = max(0.0, offset)
self.interactiveExtensionUpdated(.immediate)
@ -146,72 +162,63 @@ private final class ChatEmbeddedTitleContentNode: ASDisplayNode {
let sourceFrame = self.videoNode.view.convert(self.videoNode.bounds, to: transitionSurface.view)
let targetFrame = self.view.convert(videoFrame, to: transitionSurface.view)
self.context.sharedContext.mediaManager.setOverlayVideoNode(nil)
transitionSurface.addSubnode(self.videoNode)
let navigationBarCopy = navigationBar?.view.snapshotView(afterScreenUpdates: true)
let navigationBarContainer = UIView()
navigationBarContainer.frame = targetFrame
navigationBarContainer.clipsToBounds = true
transitionSurface.view.addSubview(navigationBarContainer)
navigationBarContainer.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
if let navigationBar = navigationBar, let navigationBarCopy = navigationBarCopy {
let navigationFrame = navigationBar.view.convert(navigationBar.bounds, to: transitionSurface.view)
let navigationSourceFrame = navigationFrame.offsetBy(dx: -sourceFrame.minX, dy: -sourceFrame.minY)
let navigationTargetFrame = navigationFrame.offsetBy(dx: -targetFrame.minX, dy: -targetFrame.minY)
navigationBarCopy.frame = navigationTargetFrame
navigationBarContainer.addSubview(navigationBarCopy)
var navigationBarCopy: UIView?
var navigationBarContainer: UIView?
var nodeTransition = transition
if self.disableInternalAnimationIn {
nodeTransition = .immediate
} else {
self.context.sharedContext.mediaManager.setOverlayVideoNode(nil)
transitionSurface.addSubnode(self.videoNode)
navigationBarCopy.layer.animateFrame(from: navigationSourceFrame, to: navigationTargetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
navigationBarCopy.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
navigationBarCopy = navigationBar?.view.snapshotView(afterScreenUpdates: true)
let navigationBarContainerValue = UIView()
navigationBarContainer = navigationBarContainerValue
navigationBarContainerValue.frame = targetFrame
navigationBarContainerValue.clipsToBounds = true
transitionSurface.view.addSubview(navigationBarContainerValue)
}
self.videoNode.updateRoundCorners(false, transition: .animated(duration: 0.25, curve: .spring))
self.videoNode.showControls()
if !self.disableInternalAnimationIn {
navigationBarContainer?.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
}
self.videoNode.updateLayout(targetFrame.size, transition: .animated(duration: 0.25, curve: .spring))
self.videoNode.frame = targetFrame
self.videoNode.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
navigationBarContainer.removeFromSuperview()
strongSelf.addSubnode(strongSelf.videoNode)
if let (size, topInset, interactiveExtension) = strongSelf.validLayout {
strongSelf.updateLayout(size: size, topInset: topInset, interactiveExtension: interactiveExtension, transition: .immediate, transitionSurface: nil, navigationBar: nil)
}
})
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.videoNode.customExpand = { [weak self] in
guard let strongSelf = self else {
return
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
strongSelf.videoNode.customExpand = nil
strongSelf.videoNode.customClose = nil
let previousFrame = strongSelf.videoNode.frame
strongSelf.context.sharedContext.mediaManager.setOverlayVideoNode(strongSelf.videoNode)
strongSelf.videoNode.updateRoundCorners(true, transition: transition)
if let targetSuperview = strongSelf.videoNode.view.superview {
let sourceFrame = strongSelf.view.convert(previousFrame, to: targetSuperview)
let targetFrame = strongSelf.videoNode.frame
strongSelf.videoNode.frame = sourceFrame
strongSelf.videoNode.updateLayout(sourceFrame.size, transition: .immediate)
if !self.disableInternalAnimationIn {
if let navigationBar = navigationBar, let navigationBarCopy = navigationBarCopy {
let navigationFrame = navigationBar.view.convert(navigationBar.bounds, to: transitionSurface.view)
let navigationSourceFrame = navigationFrame.offsetBy(dx: -sourceFrame.minX, dy: -sourceFrame.minY)
let navigationTargetFrame = navigationFrame.offsetBy(dx: -targetFrame.minX, dy: -targetFrame.minY)
navigationBarCopy.frame = navigationTargetFrame
navigationBarContainer?.addSubview(navigationBarCopy)
transition.updateFrame(node: strongSelf.videoNode, frame: targetFrame)
strongSelf.videoNode.updateLayout(targetFrame.size, transition: transition)
navigationBarCopy.layer.animateFrame(from: navigationSourceFrame, to: navigationTargetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
navigationBarCopy.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
}
strongSelf.dismissed()
}
self.videoNode.updateRoundCorners(false, transition: nodeTransition)
if !self.disableInternalAnimationIn {
self.videoNode.showControls()
}
self.videoNode.updateLayout(targetFrame.size, transition: nodeTransition)
self.videoNode.frame = targetFrame
if self.disableInternalAnimationIn {
self.addSubnode(self.videoNode)
} else {
self.videoNode.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
navigationBarContainer?.removeFromSuperview()
strongSelf.addSubnode(strongSelf.videoNode)
if let (size, topInset, interactiveExtension) = strongSelf.validLayout {
strongSelf.updateLayout(size: size, topInset: topInset, interactiveExtension: interactiveExtension, transition: .immediate, transitionSurface: nil, navigationBar: nil)
}
})
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
self.videoNode.customClose = { [weak self] in
guard let strongSelf = self else {
return
@ -227,6 +234,39 @@ private final class ChatEmbeddedTitleContentNode: ASDisplayNode {
transition.updateFrame(node: self.videoNode, frame: videoFrame)
}
}
func expand(intoLandscape: Bool) {
if intoLandscape {
let unembedWhenPortrait = self.unembedWhenPortrait
self.videoNode.customUnembedWhenPortrait = { videoNode in
unembedWhenPortrait(videoNode)
}
}
self.videoNode.expand()
}
func expandIntoPiP() {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
self.videoNode.customExpand = nil
self.videoNode.customClose = nil
let previousFrame = self.videoNode.frame
self.context.sharedContext.mediaManager.setOverlayVideoNode(self.videoNode)
self.videoNode.updateRoundCorners(true, transition: transition)
if let targetSuperview = self.videoNode.view.superview {
let sourceFrame = self.view.convert(previousFrame, to: targetSuperview)
let targetFrame = self.videoNode.frame
self.videoNode.frame = sourceFrame
self.videoNode.updateLayout(sourceFrame.size, transition: .immediate)
transition.updateFrame(node: self.videoNode, frame: targetFrame)
self.videoNode.updateLayout(targetFrame.size, transition: transition)
}
self.dismissed()
}
}
enum ChatEmbeddedTitlePeekContent: Equatable {
@ -387,6 +427,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
private var embeddedTitlePeekContent: ChatEmbeddedTitlePeekContent = .none
private var embeddedTitleContentNode: ChatEmbeddedTitleContentNode?
private var dismissedEmbeddedTitleContentNode: ChatEmbeddedTitleContentNode?
var hasEmbeddedTitleContent: Bool {
return self.embeddedTitleContentNode != nil
}
init(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: MediaAutoDownloadSettings, navigationBar: NavigationBar?, controller: ChatControllerImpl?) {
self.context = context
@ -892,6 +935,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
let statusBarHeight = layout.insets(options: [.statusBar]).top
if let embeddedTitleContentNode = self.embeddedTitleContentNode, embeddedTitleContentNode.supernode != nil {
if layout.size.width > layout.size.height {
self.embeddedTitleContentNode = nil
self.dismissedEmbeddedTitleContentNode = embeddedTitleContentNode
embeddedTitleContentNode.expand(intoLandscape: true)
self.updateHasEmbeddedTitleContent?()
}
}
if let embeddedTitleContentNode = self.embeddedTitleContentNode {
let embeddedSize = CGSize(width: layout.size.width, height: min(400.0, embeddedTitleContentNode.calculateHeight(width: layout.size.width)) + statusBarHeight + embeddedTitleContentNode.interactiveExtension)
if embeddedTitleContentNode.supernode == nil {
@ -2648,7 +2700,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
}
}
var updateHasEmbeddedTitleContent: ((Bool) -> Void)?
var isEmbeddedTitleContentHidden: Bool {
if let embeddedTitleContentNode = self.embeddedTitleContentNode {
return embeddedTitleContentNode.isUIHidden
} else {
return false
}
}
var updateHasEmbeddedTitleContent: (() -> Void)?
func acceptEmbeddedTitlePeekContent(content: NavigationControllerDropContent) -> Bool {
guard let (_, navigationHeight) = self.validLayout else {
@ -2658,7 +2718,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
return false
}
if let item = content.item as? VideoNavigationControllerDropContentItem, let itemNode = item.itemNode as? OverlayUniversalVideoNode {
let embeddedTitleContentNode = ChatEmbeddedTitleContentNode(context: self.context, videoNode: itemNode, interactiveExtensionUpdated: { [weak self] transition in
let embeddedTitleContentNode = ChatEmbeddedTitleContentNode(context: self.context, videoNode: itemNode, disableInternalAnimationIn: false, interactiveExtensionUpdated: { [weak self] transition in
guard let strongSelf = self else {
return
}
@ -2671,12 +2731,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
strongSelf.embeddedTitleContentNode = nil
strongSelf.dismissedEmbeddedTitleContentNode = embeddedTitleContentNode
strongSelf.requestLayout(.animated(duration: 0.25, curve: .spring))
strongSelf.updateHasEmbeddedTitleContent?(false)
strongSelf.updateHasEmbeddedTitleContent?()
}
}, isUIHiddenUpdated: { [weak self] in
self?.updateHasEmbeddedTitleContent?()
}, unembedWhenPortrait: { [weak self] itemNode in
guard let strongSelf = self, let itemNode = itemNode as? OverlayUniversalVideoNode else {
return false
}
strongSelf.unembedWhenPortrait(contentNode: itemNode)
return true
})
self.embeddedTitleContentNode = embeddedTitleContentNode
self.embeddedTitlePeekContent = .none
self.updateHasEmbeddedTitleContent?(true)
self.updateHasEmbeddedTitleContent?()
DispatchQueue.main.async {
self.requestLayout(.animated(duration: 0.25, curve: .spring))
}
@ -2685,4 +2753,46 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
}
return false
}
private func unembedWhenPortrait(contentNode: OverlayUniversalVideoNode) {
let embeddedTitleContentNode = ChatEmbeddedTitleContentNode(context: self.context, videoNode: contentNode, disableInternalAnimationIn: true, interactiveExtensionUpdated: { [weak self] transition in
guard let strongSelf = self else {
return
}
strongSelf.requestLayout(transition)
}, dismissed: { [weak self] in
guard let strongSelf = self else {
return
}
if let embeddedTitleContentNode = strongSelf.embeddedTitleContentNode {
strongSelf.embeddedTitleContentNode = nil
strongSelf.dismissedEmbeddedTitleContentNode = embeddedTitleContentNode
strongSelf.requestLayout(.animated(duration: 0.25, curve: .spring))
strongSelf.updateHasEmbeddedTitleContent?()
}
}, isUIHiddenUpdated: { [weak self] in
self?.updateHasEmbeddedTitleContent?()
}, unembedWhenPortrait: { [weak self] itemNode in
guard let strongSelf = self, let itemNode = itemNode as? OverlayUniversalVideoNode else {
return false
}
strongSelf.unembedWhenPortrait(contentNode: itemNode)
return true
})
self.embeddedTitleContentNode = embeddedTitleContentNode
self.embeddedTitlePeekContent = .none
self.updateHasEmbeddedTitleContent?()
self.requestLayout(.immediate)
}
func willNavigateAway() {
if let embeddedTitleContentNode = self.embeddedTitleContentNode {
self.embeddedTitleContentNode = nil
self.dismissedEmbeddedTitleContentNode = embeddedTitleContentNode
embeddedTitleContentNode.expandIntoPiP()
self.requestLayout(.animated(duration: 0.25, curve: .spring))
self.updateHasEmbeddedTitleContent?()
}
}
}

View File

@ -616,4 +616,8 @@ public final class MediaManagerImpl: NSObject, MediaManager {
self.overlayMediaManager.controller?.addNode(node, customTransition: true)
}
}
public func hasOverlayVideoNode(_ node: OverlayMediaItemNode) -> Bool {
return self.currentOverlayVideoNode === node
}
}

View File

@ -4345,6 +4345,9 @@ public final class PeerInfoScreen: ViewController {
if other.contentNode != nil {
return nil
}
if let allowsCustomTransition = other.allowsCustomTransition, !allowsCustomTransition() {
return nil
}
if let tag = other.userInfo as? PeerInfoNavigationSourceTag, tag.peerId == peerId {
return PeerInfoNavigationTransitionNode(screenNode: strongSelf.controllerNode, presentationData: strongSelf.presentationData, headerNode: strongSelf.controllerNode.headerNode)
}

View File

@ -287,10 +287,20 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
applyLayout()
}
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
self.playerNode.frame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0)
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size))
let fromFrame = self.playerNode.frame
let toFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0)
if case let .animated(duration, curve) = transition, fromFrame != toFrame, !fromFrame.width.isZero, !fromFrame.height.isZero, !toFrame.width.isZero, !toFrame.height.isZero {
self.playerNode.frame = toFrame
transition.animatePosition(node: self.playerNode, from: CGPoint(x: fromFrame.center.x - toFrame.center.x, y: fromFrame.center.y - toFrame.center.y))
let transform = CATransform3DScale(CATransform3DIdentity, fromFrame.width / toFrame.width, fromFrame.height / toFrame.height, 1.0)
self.playerNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration)
} else {
transition.updateFrame(node: self.playerNode, frame: toFrame)
}
if let thumbnailNode = self.thumbnailNode {
thumbnailNode.frame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0)
transition.updateFrame(node: thumbnailNode, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0))
}
}

View File

@ -33,17 +33,19 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
private let defaultExpand: () -> Void
public var customExpand: (() -> Void)?
public var customClose: (() -> Void)?
public var controlsAreShowingUpdated: ((Bool) -> Void)?
public init(postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, expand: @escaping () -> Void, close: @escaping () -> Void) {
self.content = content
self.defaultExpand = expand
var expandImpl: (() -> Void)?
var controlsAreShowingUpdatedImpl: ((Bool) -> Void)?
var unminimizeImpl: (() -> Void)?
var togglePlayPauseImpl: (() -> Void)?
var closeImpl: (() -> Void)?
let decoration = OverlayVideoDecoration(unminimize: {
let decoration = OverlayVideoDecoration(contentDimensions: content.dimensions, unminimize: {
unminimizeImpl?()
}, togglePlayPause: {
togglePlayPauseImpl?()
@ -51,6 +53,8 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
expandImpl?()
}, close: {
closeImpl?()
}, controlsAreShowingUpdated: { value in
controlsAreShowingUpdatedImpl?(value)
})
self.videoNode = UniversalVideoNode(postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .overlay)
self.decoration = decoration
@ -58,14 +62,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
super.init()
expandImpl = { [weak self] in
guard let strongSelf = self else {
return
}
if let customExpand = strongSelf.customExpand {
customExpand()
} else {
strongSelf.defaultExpand()
}
self?.expand()
}
unminimizeImpl = { [weak self] in
@ -90,6 +87,10 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
strongSelf.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
}
}
controlsAreShowingUpdatedImpl = { [weak self] value in
self?.controlsAreShowingUpdated?(value)
}
self.clipsToBounds = true
self.cornerRadius = 4.0
@ -104,7 +105,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
if previous != value {
if !value {
strongSelf.dismiss()
close()
closeImpl?()
}
}
}
@ -155,4 +156,12 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
public func showControls() {
self.decoration.showControls()
}
public func expand() {
if let customExpand = self.customExpand {
customExpand()
} else {
self.defaultExpand()
}
}
}

View File

@ -26,11 +26,14 @@ private func setupArrowFrame(size: CGSize, edge: OverlayMediaItemMinimizationEdg
private let backgroundImage = UIImage(bundleImageName: "Chat/Message/OverlayPlainVideoShadow")?.precomposed().resizableImage(withCapInsets: UIEdgeInsets(top: 22.0, left: 25.0, bottom: 26.0, right: 25.0), resizingMode: .stretch)
final class OverlayVideoDecoration: UniversalVideoDecoration {
private let contentDimensions: CGSize
let backgroundNode: ASDisplayNode?
let contentContainerNode: ASDisplayNode
let foregroundNode: ASDisplayNode?
private let unminimize: () -> Void
private let controlsAreShowingUpdated: (Bool) -> Void
private let shadowNode: ASImageNode
private let foregroundContainerNode: ASDisplayNode
@ -46,8 +49,11 @@ final class OverlayVideoDecoration: UniversalVideoDecoration {
private var validLayoutSize: CGSize?
init(unminimize: @escaping () -> Void, togglePlayPause: @escaping () -> Void, expand: @escaping () -> Void, close: @escaping () -> Void) {
init(contentDimensions: CGSize, unminimize: @escaping () -> Void, togglePlayPause: @escaping () -> Void, expand: @escaping () -> Void, close: @escaping () -> Void, controlsAreShowingUpdated: @escaping (Bool) -> Void) {
self.contentDimensions = contentDimensions
self.unminimize = unminimize
self.controlsAreShowingUpdated = controlsAreShowingUpdated
self.shadowNode = ASImageNode()
self.shadowNode.image = backgroundImage
@ -78,6 +84,14 @@ final class OverlayVideoDecoration: UniversalVideoDecoration {
self.statusDisposable.dispose()
}
private func frameForContent(size: CGSize) -> CGRect {
if !self.contentDimensions.width.isZero && !self.contentDimensions.height.isZero {
let fittedSize = self.contentDimensions.aspectFittedWithOverflow(size, leeway: 10.0)
return CGRect(origin: CGPoint(x: floor(size.width - fittedSize.width) / 2.0, y: floor(size.height - fittedSize.height) / 2.0), size: fittedSize)
}
return CGRect(origin: CGPoint(), size: size)
}
func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) {
if self.contentNode !== contentNode {
let previous = self.contentNode
@ -93,7 +107,7 @@ final class OverlayVideoDecoration: UniversalVideoDecoration {
if contentNode.supernode !== self.contentContainerNode {
self.contentContainerNode.addSubnode(contentNode)
if let validLayoutSize = self.validLayoutSize {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize)
contentNode.frame = self.frameForContent(size: validLayoutSize)
contentNode.updateLayout(size: validLayoutSize, transition: .immediate)
}
}
@ -107,6 +121,8 @@ final class OverlayVideoDecoration: UniversalVideoDecoration {
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayoutSize = size
let contentFrame = self.frameForContent(size: size)
let shadowInsets = UIEdgeInsets(top: 2.0, left: 3.0, bottom: 4.0, right: 3.0)
transition.updateFrame(node: self.shadowNode, frame: CGRect(origin: CGPoint(x: -shadowInsets.left, y: -shadowInsets.top), size: CGSize(width: size.width + shadowInsets.left + shadowInsets.right, height: size.height + shadowInsets.top + shadowInsets.bottom)))
@ -129,8 +145,8 @@ final class OverlayVideoDecoration: UniversalVideoDecoration {
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - progressSize.width) / 2.0), y: floorToScreenPixels((size.height - progressSize.height) / 2.0)), size: progressSize))
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size))
contentNode.updateLayout(size: size, transition: transition)
transition.updateFrame(node: contentNode, frame: contentFrame)
contentNode.updateLayout(size: contentFrame.size, transition: transition)
}
}
@ -141,9 +157,11 @@ final class OverlayVideoDecoration: UniversalVideoDecoration {
if self.controlsNode.alpha.isZero {
self.controlsNode.alpha = 1.0
self.controlsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.controlsAreShowingUpdated(true)
} else {
self.controlsNode.alpha = 0.0
self.controlsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
self.controlsAreShowingUpdated(false)
}
}
}
@ -152,6 +170,7 @@ final class OverlayVideoDecoration: UniversalVideoDecoration {
if self.controlsNode.alpha.isZero {
self.controlsNode.alpha = 1.0
self.controlsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.controlsAreShowingUpdated(true)
}
}