mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
732 lines
34 KiB
Swift
732 lines
34 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import SwiftSignalKit
|
|
import UIKitRuntimeUtils
|
|
|
|
private let minimizedMask: UIImage? = {
|
|
return generateImage(CGSize(width: 22.0, height: 24.0), rotatedContext: { size, context in
|
|
context.setFillColor(UIColor.black.cgColor)
|
|
context.fill(CGRect(origin: .zero, size: size))
|
|
|
|
context.setBlendMode(.clear)
|
|
context.setFillColor(UIColor.clear.cgColor)
|
|
|
|
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: -10, width: 22, height: 20), cornerRadius: 10)
|
|
context.addPath(path.cgPath)
|
|
context.fillPath()
|
|
})?.stretchableImage(withLeftCapWidth: 11, topCapHeight: 12)
|
|
}()
|
|
|
|
final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDelegate {
|
|
private var theme: NavigationControllerTheme
|
|
let isFlat: Bool
|
|
var isMinimized: Bool
|
|
var appliedIsMinimized: Bool = false
|
|
|
|
private let minimizedFrameNode: ASImageNode
|
|
private let dim: ASDisplayNode
|
|
private let scrollNode: ASScrollNode
|
|
let container: NavigationContainer
|
|
|
|
private let minimizedBackgroundNode: ASDisplayNode
|
|
private let minimizedTitleNode: ImmediateTextNode
|
|
private let minimizedCloseButton: HighlightableButtonNode
|
|
private var minimizedTitleDisposable: Disposable?
|
|
|
|
private var panRecognizer: InteractiveTransitionGestureRecognizer?
|
|
|
|
private(set) var isReady: Bool = false
|
|
private(set) var dismissProgress: CGFloat = 0.0
|
|
var isReadyUpdated: (() -> Void)?
|
|
var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
|
var interactivelyDismissed: ((Bool) -> Void)?
|
|
|
|
var minimizedRequestDismiss: ((Bool) -> Void)?
|
|
var minimizedRequestMaximize: (() -> Void)?
|
|
|
|
private var isUpdatingState = false
|
|
private var ignoreScrolling = false
|
|
private var isDismissed = false
|
|
private var isInteractiveDimissEnabled = true
|
|
|
|
private var validLayout: ContainerViewLayout?
|
|
private var horizontalDismissOffset: CGFloat?
|
|
|
|
var keyboardViewManager: KeyboardViewManager? {
|
|
didSet {
|
|
if self.keyboardViewManager !== oldValue {
|
|
self.container.keyboardViewManager = self.keyboardViewManager
|
|
}
|
|
}
|
|
}
|
|
|
|
var canHaveKeyboardFocus: Bool = false {
|
|
didSet {
|
|
self.container.canHaveKeyboardFocus = self.canHaveKeyboardFocus
|
|
}
|
|
}
|
|
|
|
init(theme: NavigationControllerTheme, isFlat: Bool, isMinimized: Bool, controllerRemoved: @escaping (ViewController) -> Void) {
|
|
self.theme = theme
|
|
self.isFlat = isFlat
|
|
self.isMinimized = isMinimized
|
|
|
|
self.minimizedFrameNode = ASImageNode()
|
|
self.minimizedFrameNode.contentMode = .scaleToFill
|
|
self.minimizedFrameNode.image = minimizedMask
|
|
|
|
self.dim = ASDisplayNode()
|
|
self.dim.alpha = 0.0
|
|
|
|
self.scrollNode = ASScrollNode()
|
|
|
|
self.container = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved)
|
|
self.container.clipsToBounds = true
|
|
|
|
self.minimizedBackgroundNode = ASDisplayNode()
|
|
self.minimizedBackgroundNode.clipsToBounds = true
|
|
self.minimizedBackgroundNode.backgroundColor = theme.navigationBar.opaqueBackgroundColor
|
|
|
|
self.minimizedTitleNode = ImmediateTextNode()
|
|
|
|
self.minimizedCloseButton = HighlightableButtonNode()
|
|
self.minimizedCloseButton.setImage(UIImage(bundleImageName: "Instant View/Close"), for: .normal)
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.minimizedFrameNode)
|
|
self.addSubnode(self.dim)
|
|
self.addSubnode(self.scrollNode)
|
|
self.scrollNode.addSubnode(self.container)
|
|
|
|
self.addSubnode(self.minimizedBackgroundNode)
|
|
self.minimizedBackgroundNode.addSubnode(self.minimizedTitleNode)
|
|
self.minimizedBackgroundNode.addSubnode(self.minimizedCloseButton)
|
|
|
|
self.minimizedCloseButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside)
|
|
|
|
self.isReady = self.container.isReady
|
|
self.container.isReadyUpdated = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if !strongSelf.isReady {
|
|
strongSelf.isReady = true
|
|
if !strongSelf.isUpdatingState {
|
|
strongSelf.isReadyUpdated?()
|
|
}
|
|
}
|
|
}
|
|
|
|
applySmoothRoundedCorners(self.container.layer)
|
|
applySmoothRoundedCorners(self.minimizedBackgroundNode.layer)
|
|
}
|
|
|
|
deinit {
|
|
self.minimizedTitleDisposable?.dispose()
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.scrollNode.view.alwaysBounceVertical = false
|
|
self.scrollNode.view.alwaysBounceHorizontal = false
|
|
self.scrollNode.view.bounces = false
|
|
self.scrollNode.view.showsVerticalScrollIndicator = false
|
|
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
|
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
|
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
|
}
|
|
self.scrollNode.view.delaysContentTouches = false
|
|
self.scrollNode.view.clipsToBounds = false
|
|
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
|
|
|
|
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
|
|
guard let strongSelf = self, !strongSelf.isDismissed else {
|
|
return []
|
|
}
|
|
return .right
|
|
})
|
|
if #available(iOS 13.4, *) {
|
|
panRecognizer.allowedScrollTypesMask = .continuous
|
|
}
|
|
self.panRecognizer = panRecognizer
|
|
if let layout = self.validLayout {
|
|
switch layout.metrics.widthClass {
|
|
case .compact:
|
|
panRecognizer.isEnabled = true
|
|
case .regular:
|
|
panRecognizer.isEnabled = false
|
|
}
|
|
}
|
|
panRecognizer.delegate = self.wrappedGestureRecognizerDelegate
|
|
panRecognizer.delaysTouchesBegan = false
|
|
panRecognizer.cancelsTouchesInView = true
|
|
if !self.isFlat {
|
|
self.view.addGestureRecognizer(panRecognizer)
|
|
self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
|
}
|
|
|
|
self.minimizedBackgroundNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.maximizeTapGesture(_:))))
|
|
}
|
|
|
|
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
if gestureRecognizer.view === self.minimizedBackgroundNode.view {
|
|
return self.isMinimized
|
|
} else if !self.isMinimized {
|
|
if gestureRecognizer == self.panRecognizer, let gestureRecognizer = self.panRecognizer, gestureRecognizer.numberOfTouches == 0 {
|
|
let translation = gestureRecognizer.velocity(in: gestureRecognizer.view)
|
|
if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
|
|
return false
|
|
}
|
|
if translation.x < 4.0 {
|
|
return false
|
|
}
|
|
if self.isDismissed {
|
|
return false
|
|
}
|
|
return true
|
|
} else {
|
|
return true
|
|
}
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
return false
|
|
}
|
|
|
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
private func checkInteractiveDismissWithControllers() -> Bool {
|
|
if let controller = self.container.controllers.last {
|
|
if !controller.attemptNavigation({
|
|
}) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .began:
|
|
self.horizontalDismissOffset = 0.0
|
|
case .changed:
|
|
let translation = max(0.0, recognizer.translation(in: self.view).x)
|
|
let progress = translation / self.bounds.width
|
|
self.horizontalDismissOffset = translation
|
|
self.dismissProgress = progress
|
|
self.applyDismissProgress(transition: .immediate, completion: {})
|
|
self.container.updateAdditionalKeyboardLeftEdgeOffset(translation, transition: .immediate)
|
|
case .ended, .cancelled:
|
|
let translation = max(0.0, recognizer.translation(in: self.view).x)
|
|
let progress = translation / self.bounds.width
|
|
let velocity = recognizer.velocity(in: self.view).x
|
|
|
|
if (velocity > 1000 || progress > 0.2) && self.checkInteractiveDismissWithControllers() {
|
|
self.isDismissed = true
|
|
self.horizontalDismissOffset = self.bounds.width
|
|
self.dismissProgress = 1.0
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
|
|
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: self.bounds.width, y: 0.0), size: self.scrollNode.bounds.size))
|
|
self.container.updateAdditionalKeyboardLeftEdgeOffset(self.bounds.width, transition: transition)
|
|
self.applyDismissProgress(transition: transition, completion: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let hadInputFocus = viewTreeContainsFirstResponder(view: strongSelf.view)
|
|
strongSelf.keyboardViewManager?.dismissEditingWithoutAnimation(view: strongSelf.view)
|
|
strongSelf.interactivelyDismissed?(hadInputFocus)
|
|
})
|
|
} else {
|
|
self.horizontalDismissOffset = nil
|
|
self.dismissProgress = 0.0
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.1, curve: .easeInOut)
|
|
self.applyDismissProgress(transition: transition, completion: {})
|
|
self.container.updateAdditionalKeyboardLeftEdgeOffset(0.0, transition: transition)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
if !self.isDismissed {
|
|
self.dismissWithAnimation()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func dismissWithAnimation() {
|
|
let scrollView = self.scrollNode.view
|
|
let targetOffset: CGFloat
|
|
let duration = 0.3
|
|
let transition: ContainedViewLayoutTransition
|
|
let dismissProgress: CGFloat
|
|
dismissProgress = 1.0
|
|
targetOffset = 0.0
|
|
transition = .animated(duration: duration, curve: .easeInOut)
|
|
self.isDismissed = true
|
|
self.ignoreScrolling = true
|
|
let deltaY = targetOffset - scrollView.contentOffset.y
|
|
scrollView.setContentOffset(scrollView.contentOffset, animated: false)
|
|
scrollView.setContentOffset(CGPoint(x: 0.0, y: targetOffset), animated: false)
|
|
transition.animateOffsetAdditive(layer: self.scrollNode.layer, offset: -deltaY, completion: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if targetOffset == 0.0 {
|
|
strongSelf.interactivelyDismissed?(false)
|
|
}
|
|
})
|
|
self.ignoreScrolling = false
|
|
self.dismissProgress = dismissProgress
|
|
|
|
self.applyDismissProgress(transition: transition, completion: {})
|
|
|
|
self.view.endEditing(true)
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
if self.ignoreScrolling || self.isDismissed {
|
|
return
|
|
}
|
|
var progress = (self.bounds.height - scrollView.bounds.origin.y) / self.bounds.height
|
|
progress = max(0.0, min(1.0, progress))
|
|
self.dismissProgress = progress
|
|
self.applyDismissProgress(transition: .immediate, completion: {})
|
|
}
|
|
|
|
private func applyDismissProgress(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
|
transition.updateAlpha(node: self.dim, alpha: 1.0 - self.dismissProgress, completion: { _ in
|
|
completion()
|
|
})
|
|
self.updateDismissProgress?(self.dismissProgress, transition)
|
|
}
|
|
|
|
private var endDraggingVelocity: CGPoint?
|
|
|
|
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
let velocity = self.endDraggingVelocity ?? CGPoint()
|
|
self.endDraggingVelocity = nil
|
|
|
|
var progress = (self.bounds.height - scrollView.bounds.origin.y) / self.bounds.height
|
|
progress = max(0.0, min(1.0, progress))
|
|
|
|
let targetOffset: CGFloat
|
|
let velocityFactor: CGFloat = 0.4 / max(1.0, abs(velocity.y))
|
|
let duration = Double(min(0.3, velocityFactor))
|
|
let transition: ContainedViewLayoutTransition
|
|
let dismissProgress: CGFloat
|
|
if (velocity.y < -0.5 || progress >= 0.5) && self.checkInteractiveDismissWithControllers() {
|
|
dismissProgress = 1.0
|
|
targetOffset = 0.0
|
|
transition = .animated(duration: duration, curve: .easeInOut)
|
|
self.isDismissed = true
|
|
} else {
|
|
dismissProgress = 0.0
|
|
targetOffset = self.bounds.height
|
|
transition = .animated(duration: 0.5, curve: .spring)
|
|
}
|
|
self.ignoreScrolling = true
|
|
let deltaY = targetOffset - scrollView.contentOffset.y
|
|
scrollView.setContentOffset(scrollView.contentOffset, animated: false)
|
|
scrollView.setContentOffset(CGPoint(x: 0.0, y: targetOffset), animated: false)
|
|
transition.animateOffsetAdditive(layer: self.scrollNode.layer, offset: -deltaY, completion: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if targetOffset == 0.0 {
|
|
strongSelf.interactivelyDismissed?(false)
|
|
}
|
|
})
|
|
self.ignoreScrolling = false
|
|
self.dismissProgress = dismissProgress
|
|
|
|
self.applyDismissProgress(transition: transition, completion: {})
|
|
}
|
|
|
|
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
|
self.endDraggingVelocity = velocity
|
|
targetContentOffset.pointee = scrollView.contentOffset
|
|
}
|
|
|
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
}
|
|
|
|
func scrollViewShouldScroll(toTop scrollView: UIScrollView) -> Bool {
|
|
return false
|
|
}
|
|
|
|
func update(layout: ContainerViewLayout, controllers: [ViewController], coveredByModalTransition: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
if self.isDismissed {
|
|
return
|
|
}
|
|
|
|
self.isUpdatingState = true
|
|
|
|
self.validLayout = layout
|
|
|
|
let lastControllerUpdated = self.container.controllers.last !== controllers.last
|
|
|
|
var isStandaloneModal = false
|
|
if let controller = controllers.first, case .standaloneModal = controller.navigationPresentation {
|
|
isStandaloneModal = true
|
|
}
|
|
|
|
transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size))
|
|
self.ignoreScrolling = true
|
|
self.scrollNode.view.isScrollEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0) && self.isInteractiveDimissEnabled && !self.isFlat
|
|
let previousBounds = self.scrollNode.bounds
|
|
let scrollNodeFrame = CGRect(origin: CGPoint(x: self.horizontalDismissOffset ?? 0.0, y: 0.0), size: layout.size)
|
|
self.scrollNode.frame = scrollNodeFrame
|
|
self.scrollNode.view.contentSize = CGSize(width: layout.size.width, height: layout.size.height * 2.0)
|
|
if !self.scrollNode.view.isDecelerating && !self.scrollNode.view.isDragging {
|
|
let defaultBounds = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: layout.size)
|
|
if self.scrollNode.bounds != defaultBounds {
|
|
self.scrollNode.bounds = defaultBounds
|
|
}
|
|
if previousBounds.minY != defaultBounds.minY {
|
|
transition.animateOffsetAdditive(node: self.scrollNode, offset: previousBounds.minY - defaultBounds.minY)
|
|
}
|
|
}
|
|
self.ignoreScrolling = false
|
|
|
|
self.scrollNode.view.isScrollEnabled = !isStandaloneModal && !self.isFlat
|
|
|
|
let isLandscape = layout.orientation == .landscape
|
|
let containerLayout: ContainerViewLayout
|
|
let containerFrame: CGRect
|
|
let containerScale: CGFloat
|
|
if layout.metrics.widthClass == .compact || self.isFlat {
|
|
self.panRecognizer?.isEnabled = true
|
|
self.container.clipsToBounds = true
|
|
if self.isFlat || self.isMinimized {
|
|
self.dim.backgroundColor = .clear
|
|
} else {
|
|
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
|
}
|
|
if isStandaloneModal || isLandscape || self.isFlat {
|
|
self.container.cornerRadius = 0.0
|
|
} else {
|
|
self.container.cornerRadius = 10.0
|
|
self.minimizedBackgroundNode.cornerRadius = self.container.cornerRadius
|
|
}
|
|
|
|
if #available(iOS 11.0, *) {
|
|
if layout.safeInsets.bottom.isZero {
|
|
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
} else {
|
|
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
|
}
|
|
|
|
self.minimizedBackgroundNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
}
|
|
|
|
var topInset: CGFloat
|
|
if isStandaloneModal || isLandscape {
|
|
topInset = 0.0
|
|
containerLayout = layout
|
|
|
|
let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerLayout.size)
|
|
containerScale = 1.0
|
|
containerFrame = unscaledFrame
|
|
} else {
|
|
topInset = 10.0
|
|
|
|
let height: CGFloat
|
|
if self.isMinimized {
|
|
height = layout.size.height - topInset
|
|
topInset = layout.size.height - 78.0
|
|
} else {
|
|
if self.isFlat {
|
|
topInset = 0.0
|
|
} else if let statusBarHeight = layout.statusBarHeight {
|
|
topInset += statusBarHeight
|
|
}
|
|
height = layout.size.height - topInset
|
|
}
|
|
|
|
let effectiveStatusBarHeight: CGFloat?
|
|
if self.isFlat {
|
|
effectiveStatusBarHeight = layout.statusBarHeight
|
|
} else {
|
|
effectiveStatusBarHeight = nil
|
|
}
|
|
|
|
containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width, height: height), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), safeInsets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.safeInsets.bottom, right: layout.safeInsets.right), additionalInsets: layout.additionalInsets, statusBarHeight: effectiveStatusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
|
|
let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - coveredByModalTransition * 10.0), size: containerLayout.size)
|
|
let maxScale: CGFloat = (containerLayout.size.width - 16.0 * 2.0) / containerLayout.size.width
|
|
containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition
|
|
let maxScaledTopInset: CGFloat = topInset - 10.0
|
|
let scaledTopInset: CGFloat = topInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition
|
|
containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
|
|
}
|
|
|
|
for controller in controllers {
|
|
controller.isMinimized = self.isMinimized
|
|
}
|
|
|
|
if self.isMinimized != self.appliedIsMinimized {
|
|
self.appliedIsMinimized = self.isMinimized
|
|
|
|
if self.isMinimized {
|
|
let modalTopEdgeOffset = (controllers.last?.modalTopEdgeOffset ?? 0.0) + 96.0
|
|
if transition.isAnimated {
|
|
self.minimizedBackgroundNode.position = self.minimizedBackgroundNode.position.offsetBy(dx: 0.0, dy: modalTopEdgeOffset)
|
|
}
|
|
}
|
|
}
|
|
|
|
transition.updateFrame(node: self.minimizedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: containerFrame.minY), size: CGSize(width: layout.size.width, height: 243.0)))
|
|
transition.updateAlpha(node: self.minimizedBackgroundNode, alpha: self.isMinimized ? 1.0 : 0.0)
|
|
self.minimizedBackgroundNode.cornerRadius = 10.0
|
|
self.minimizedBackgroundNode.isUserInteractionEnabled = self.isMinimized
|
|
|
|
let titleSideInset: CGFloat = 56.0
|
|
if self.isMinimized, let controller = controllers.last {
|
|
if lastControllerUpdated || self.minimizedTitleDisposable == nil {
|
|
var isFirstUpdate = true
|
|
self.minimizedTitleDisposable = (controller.titleSignal
|
|
|> deliverOnMainQueue).start(next: { [weak self] title in
|
|
guard let self, let layout = self.validLayout else {
|
|
return
|
|
}
|
|
|
|
self.minimizedTitleNode.attributedText = NSAttributedString(string: title ?? "", font: Font.bold(17.0), textColor: self.theme.navigationBar.primaryTextColor)
|
|
|
|
if !isFirstUpdate {
|
|
let titleSize = self.minimizedTitleNode.updateLayout(CGSize(width: layout.size.width - titleSideInset * 2.0, height: 56.0))
|
|
self.minimizedTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - titleSize.width) / 2.0), y: 18.0), size: titleSize)
|
|
} else {
|
|
isFirstUpdate = false
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
self.minimizedTitleDisposable?.dispose()
|
|
self.minimizedTitleDisposable = nil
|
|
}
|
|
|
|
let titleSize = self.minimizedTitleNode.updateLayout(CGSize(width: layout.size.width - titleSideInset * 2.0, height: 56.0))
|
|
transition.updateFrame(node: self.minimizedTitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - titleSize.width) / 2.0), y: 18.0), size: titleSize))
|
|
|
|
transition.updateFrame(node: self.minimizedCloseButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: 46.0, height: 52.0)))
|
|
|
|
transition.updateAlpha(node: self.minimizedFrameNode, alpha: self.isMinimized ? 1.0 : 0.0)
|
|
transition.updateFrame(node: self.minimizedFrameNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - 81.0 - 10.0), size: CGSize(width: layout.size.width, height: 24.0 + 81.0)))
|
|
} else {
|
|
self.panRecognizer?.isEnabled = false
|
|
if self.isFlat {
|
|
self.dim.backgroundColor = .clear
|
|
self.container.clipsToBounds = true
|
|
self.container.cornerRadius = 0.0
|
|
if #available(iOS 11.0, *) {
|
|
self.container.layer.maskedCorners = []
|
|
}
|
|
} else {
|
|
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
|
|
self.container.clipsToBounds = true
|
|
self.container.cornerRadius = 10.0
|
|
if #available(iOS 11.0, *) {
|
|
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
|
}
|
|
}
|
|
|
|
let verticalInset: CGFloat = 44.0
|
|
|
|
let maxSide = max(layout.size.width, layout.size.height)
|
|
let minSide = min(layout.size.width, layout.size.height)
|
|
var containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0)
|
|
if let preferredSize = controllers.last?.preferredContentSizeForLayout(layout) {
|
|
containerSize = preferredSize
|
|
}
|
|
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize)
|
|
containerScale = 1.0
|
|
|
|
var inputHeight: CGFloat?
|
|
if let inputHeightValue = layout.inputHeight {
|
|
inputHeight = max(0.0, inputHeightValue - (layout.size.height - containerFrame.maxY))
|
|
}
|
|
|
|
let effectiveStatusBarHeight: CGFloat?
|
|
if self.isFlat {
|
|
effectiveStatusBarHeight = layout.statusBarHeight
|
|
} else {
|
|
effectiveStatusBarHeight = nil
|
|
}
|
|
|
|
containerLayout = ContainerViewLayout(size: containerSize, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: effectiveStatusBarHeight, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
|
|
}
|
|
transition.updateFrameAsPositionAndBounds(node: self.container, frame: containerFrame.offsetBy(dx: 0.0, dy: layout.size.height))
|
|
transition.updateTransformScale(node: self.container, scale: containerScale)
|
|
self.container.update(layout: containerLayout, canBeClosed: true, controllers: controllers, transition: transition)
|
|
|
|
self.isUpdatingState = false
|
|
}
|
|
|
|
func animateIn(transition: ContainedViewLayoutTransition) {
|
|
if let controller = self.container.controllers.first, case .standaloneModal = controller.navigationPresentation {
|
|
} else if self.isFlat {
|
|
} else {
|
|
transition.updateAlpha(node: self.dim, alpha: 1.0)
|
|
transition.animatePositionAdditive(node: self.container, offset: CGPoint(x: 0.0, y: self.bounds.height + self.container.bounds.height / 2.0 - (self.container.position.y - self.bounds.height)))
|
|
}
|
|
}
|
|
|
|
func dismiss(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) -> ContainedViewLayoutTransition {
|
|
for controller in self.container.controllers {
|
|
controller.viewWillDisappear(transition.isAnimated)
|
|
}
|
|
|
|
if let firstController = self.container.controllers.first, case .standaloneModal = firstController.navigationPresentation {
|
|
for controller in self.container.controllers {
|
|
controller.setIgnoreAppearanceMethodInvocations(true)
|
|
controller.displayNode.removeFromSupernode()
|
|
controller.setIgnoreAppearanceMethodInvocations(false)
|
|
controller.viewDidDisappear(transition.isAnimated)
|
|
}
|
|
completion()
|
|
return transition
|
|
} else {
|
|
if transition.isAnimated && !self.isFlat {
|
|
let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
|
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
|
alphaTransition.updateAlpha(node: self.dim, alpha: 0.0, beginWithCurrentState: true)
|
|
|
|
let targetY: CGFloat
|
|
if self.isMinimized {
|
|
let offset: CGFloat = 81.0 + 15.0
|
|
targetY = self.container.position.y + offset
|
|
positionTransition.updatePosition(node: self.minimizedBackgroundNode, position: CGPoint(x: self.minimizedBackgroundNode.position.x, y: self.minimizedBackgroundNode.position.y + offset))
|
|
} else {
|
|
targetY = self.bounds.height + self.container.bounds.height / 2.0 + self.bounds.height
|
|
}
|
|
|
|
positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: targetY), beginWithCurrentState: true, completion: { [weak self] _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
for controller in strongSelf.container.controllers {
|
|
controller.viewDidDisappear(transition.isAnimated)
|
|
}
|
|
completion()
|
|
})
|
|
return positionTransition
|
|
} else {
|
|
for controller in self.container.controllers {
|
|
controller.setIgnoreAppearanceMethodInvocations(true)
|
|
controller.displayNode.removeFromSupernode()
|
|
controller.setIgnoreAppearanceMethodInvocations(false)
|
|
controller.viewDidDisappear(transition.isAnimated)
|
|
}
|
|
completion()
|
|
return transition
|
|
}
|
|
}
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
guard let result = super.hitTest(point, with: event) else {
|
|
return nil
|
|
}
|
|
if !self.container.bounds.contains(self.view.convert(point, to: self.container.view)) {
|
|
if self.isMinimized {
|
|
return nil
|
|
} else {
|
|
return self.dim.view
|
|
}
|
|
}
|
|
if self.isMinimized && result == self.minimizedBackgroundNode.view {
|
|
return result
|
|
}
|
|
if self.isFlat {
|
|
return result
|
|
}
|
|
var currentParent: UIView? = result
|
|
var enableScrolling = true
|
|
while true {
|
|
if currentParent == nil {
|
|
break
|
|
}
|
|
if currentParent is UIKeyInput {
|
|
if currentParent?.disablesInteractiveModalDismiss == true {
|
|
enableScrolling = false
|
|
break
|
|
}
|
|
} else if let scrollView = currentParent as? UIScrollView {
|
|
if scrollView === self.scrollNode.view {
|
|
break
|
|
}
|
|
if scrollView.disablesInteractiveModalDismiss {
|
|
enableScrolling = false
|
|
break
|
|
} else {
|
|
if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top {
|
|
return self.scrollNode.view
|
|
}
|
|
}
|
|
} else if let listView = currentParent as? ListViewBackingView, let listNode = listView.target {
|
|
if listNode.view.disablesInteractiveModalDismiss {
|
|
enableScrolling = false
|
|
break
|
|
} else if listNode.scroller.isDecelerating && listNode.scroller.contentOffset.y < listNode.scroller.contentInset.top {
|
|
return self.scrollNode.view
|
|
}
|
|
} else if let currentParent, currentParent.disablesInteractiveModalDismiss {
|
|
enableScrolling = false
|
|
break
|
|
}
|
|
currentParent = currentParent?.superview
|
|
}
|
|
if let controller = self.container.controllers.last {
|
|
if controller.view.disablesInteractiveModalDismiss {
|
|
enableScrolling = false
|
|
}
|
|
}
|
|
self.isInteractiveDimissEnabled = enableScrolling
|
|
if let layout = self.validLayout {
|
|
if layout.inputHeight != nil && layout.inputHeight != 0.0 {
|
|
enableScrolling = false
|
|
}
|
|
}
|
|
self.scrollNode.view.isScrollEnabled = enableScrolling && !self.isFlat
|
|
return result
|
|
}
|
|
|
|
@objc private func closePressed() {
|
|
if !self.isDismissed {
|
|
self.minimizedRequestDismiss?(true)
|
|
}
|
|
}
|
|
|
|
@objc private func maximizeTapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
if !self.isDismissed {
|
|
if self.container.controllers.count == 1 {
|
|
self.minimizedRequestMaximize?()
|
|
} else {
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|