mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Web app minimization
This commit is contained in:
parent
7d5d9ba7ba
commit
2b1d09fb5a
@ -12284,6 +12284,7 @@ Sorry for the inconvenience.";
|
||||
"Stars.Purchase.StarsNeeded_1" = "%@ Star Needed";
|
||||
"Stars.Purchase.StarsNeeded_any" = "%@ Stars Needed";
|
||||
"Stars.Purchase.StarsNeededInfo" = "Buy Stars to use them on **%@** and other miniapps.";
|
||||
"Stars.Purchase.StarsNeededUnlockInfo" = "Buy Stars to unlock media and use them on miniapps.";
|
||||
|
||||
"Stars.Purchase.Stars_1" = "%@ Star";
|
||||
"Stars.Purchase.Stars_any" = "%@ Stars";
|
||||
@ -12453,10 +12454,15 @@ Sorry for the inconvenience.";
|
||||
"Premium.MessageEffects" = "Message Effects";
|
||||
"Premium.MessageEffectsInfo" = "Add over 500 animated effects to private messages.";
|
||||
|
||||
"Chat.UnlockMedia" = "Unlock for %@";
|
||||
"Chat.PaidMedia.UnlockMedia" = "Unlock for %@";
|
||||
"Chat.PaidMedia.Purchased" = "Purchased";
|
||||
|
||||
"Attachment.SendWithoutGrouping" = "Send Without Grouping";
|
||||
"Attachment.Paid.EditPrice" = "Edit Price";
|
||||
"Attachment.Paid.EditPrice.Stars_1" = "%@ Star";
|
||||
"Attachment.Paid.EditPrice.Stars_any" = "%@ Stars";
|
||||
"Attachment.Paid.Create" = "Make This Content Paid";
|
||||
|
||||
"WebApp.MinimizedTitleFormat" = "%1$@ & %2$@";
|
||||
"WebApp.MinimizedTitle.Others_1" = "%@ Other";
|
||||
"WebApp.MinimizedTitle.Others_any" = "%@ Others";
|
||||
|
@ -41,6 +41,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView",
|
||||
"//submodules/ReactionSelectionNode",
|
||||
"//submodules/TelegramUI/Components/Chat/TopMessageReactions",
|
||||
"//submodules/TelegramUI/Components/MinimizedContainer",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -34,13 +34,13 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate {
|
||||
private(set) var dismissProgress: CGFloat = 0.0
|
||||
var isReadyUpdated: (() -> Void)?
|
||||
var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||
var interactivelyDismissed: (() -> Bool)?
|
||||
var interactivelyDismissed: ((CGFloat) -> Bool)?
|
||||
var controllerRemoved: ((ViewController) -> Void)?
|
||||
|
||||
var shouldCancelPanGesture: (() -> Bool)?
|
||||
var requestDismiss: (() -> Void)?
|
||||
|
||||
var updateModalProgress: ((CGFloat, CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||
var updateModalProgress: ((CGFloat, CGFloat, CGRect, ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
private var isUpdatingState = false
|
||||
private var isDismissed = false
|
||||
@ -306,10 +306,13 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate {
|
||||
ignoreDismiss = true
|
||||
}
|
||||
|
||||
var minimizing = false
|
||||
var dismissing = false
|
||||
if (bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0)) && !ignoreDismiss {
|
||||
if self.interactivelyDismissed?() == true {
|
||||
if self.interactivelyDismissed?(velocity.y) == true {
|
||||
dismissing = true
|
||||
} else {
|
||||
minimizing = true
|
||||
}
|
||||
} else if self.isExpanded {
|
||||
if velocity.y > 300.0 || offset > topInset / 2.0 {
|
||||
@ -363,7 +366,9 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate {
|
||||
let previousBounds = bounds
|
||||
bounds.origin.y = 0.0
|
||||
self.bounds = bounds
|
||||
self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
if !minimizing {
|
||||
self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
}
|
||||
}
|
||||
case .cancelled:
|
||||
self.panGestureArguments = nil
|
||||
@ -391,8 +396,8 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate {
|
||||
return true
|
||||
}
|
||||
|
||||
func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) {
|
||||
guard isExpanded != self.isExpanded else {
|
||||
func update(isExpanded: Bool, force: Bool = false, transition: ContainedViewLayoutTransition) {
|
||||
guard isExpanded != self.isExpanded || force else {
|
||||
return
|
||||
}
|
||||
self.isExpanded = isExpanded
|
||||
@ -437,7 +442,7 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate {
|
||||
})
|
||||
|
||||
let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / defaultTopInset)
|
||||
self.updateModalProgress?(modalProgress, topInset, transition)
|
||||
self.updateModalProgress?(modalProgress, topInset, self.bounds, transition)
|
||||
|
||||
let containerLayout: ContainerViewLayout
|
||||
let containerFrame: CGRect
|
||||
|
@ -15,6 +15,7 @@ import LegacyMessageInputPanel
|
||||
import LegacyMessageInputPanelInputView
|
||||
import AttachmentTextInputPanelNode
|
||||
import ChatSendMessageActionUI
|
||||
import MinimizedContainer
|
||||
|
||||
public enum AttachmentButtonType: Equatable {
|
||||
case gallery
|
||||
@ -342,7 +343,7 @@ public class AttachmentController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
self.container.updateModalProgress = { [weak self] progress, topInset, transition in
|
||||
self.container.updateModalProgress = { [weak self] progress, topInset, bounds, transition in
|
||||
if let strongSelf = self, let layout = strongSelf.validLayout, !strongSelf.isDismissing {
|
||||
var transition = transition
|
||||
if strongSelf.container.supernode == nil {
|
||||
@ -350,7 +351,8 @@ public class AttachmentController: ViewController {
|
||||
}
|
||||
|
||||
strongSelf.modalProgress = progress
|
||||
strongSelf.controller?.modalTopEdgeOffset = topInset
|
||||
strongSelf.controller?.minimizedTopEdgeOffset = topInset
|
||||
strongSelf.controller?.minimizedBounds = bounds
|
||||
|
||||
if !strongSelf.isMinimizing {
|
||||
strongSelf.controller?.updateModalStyleOverlayTransitionFactor(progress, transition: transition)
|
||||
@ -364,16 +366,37 @@ public class AttachmentController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
self.container.interactivelyDismissed = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
self.container.interactivelyDismissed = { [weak self] velocity in
|
||||
if let strongSelf = self, let layout = strongSelf.validLayout {
|
||||
if let controller = strongSelf.controller, controller.shouldMinimizeOnSwipe?() == true, let navigationController = controller.navigationController as? NavigationController {
|
||||
navigationController.minimizeViewController(controller, animated: true)
|
||||
|
||||
Queue.mainQueue().after(0.5) {
|
||||
strongSelf.isMinimizing = true
|
||||
strongSelf.container.update(isExpanded: true, transition: .immediate)
|
||||
strongSelf.isMinimizing = false
|
||||
}
|
||||
let delta = layout.size.height - controller.minimizedTopEdgeOffset
|
||||
let damping: CGFloat = 180
|
||||
let initialVelocity: CGFloat = delta > 0.0 ? velocity / delta : 0.0
|
||||
|
||||
navigationController.minimizeViewController(controller, damping: damping, velocity: initialVelocity, setupContainer: { [weak self] current in
|
||||
let minimizedContainer: MinimizedContainerImpl?
|
||||
if let current = current as? MinimizedContainerImpl {
|
||||
minimizedContainer = current
|
||||
} else if let context = self?.controller?.context {
|
||||
minimizedContainer = MinimizedContainerImpl(context: context, navigationController: navigationController)
|
||||
} else {
|
||||
minimizedContainer = nil
|
||||
}
|
||||
return minimizedContainer
|
||||
}, animated: true)
|
||||
|
||||
strongSelf.dim.isHidden = true
|
||||
|
||||
strongSelf.isMinimizing = true
|
||||
strongSelf.container.update(isExpanded: true, force: true, transition: .immediate)
|
||||
// strongSelf.container.update(isExpanded: true, force: true, transition: .animated(duration: 0.4, curve: .customSpring(damping: 180.0, initialVelocity: initialVelocity)))
|
||||
strongSelf.isMinimizing = false
|
||||
|
||||
Queue.mainQueue().after(0.45, {
|
||||
strongSelf.dim.isHidden = false
|
||||
})
|
||||
|
||||
return false
|
||||
} else {
|
||||
strongSelf.controller?.dismiss(animated: true)
|
||||
@ -1038,13 +1061,31 @@ public class AttachmentController: ViewController {
|
||||
return self.buttons.contains(.standalone)
|
||||
}
|
||||
|
||||
private var snapshotView: UIView?
|
||||
public override var isMinimized: Bool {
|
||||
didSet {
|
||||
guard self.isMinimized != oldValue else {
|
||||
return
|
||||
}
|
||||
if self.isMinimized {
|
||||
if self.snapshotView == nil, let lastController = self.node.container.container.controllers.last, let snapshotView = lastController.view.snapshotView(afterScreenUpdates: false) {
|
||||
snapshotView.isUserInteractionEnabled = false
|
||||
self.snapshotView = snapshotView
|
||||
lastController.view.addSubview(snapshotView)
|
||||
}
|
||||
} else {
|
||||
if let snapshotView = self.snapshotView {
|
||||
self.snapshotView = nil
|
||||
Queue.mainQueue().after(0.5) {
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
snapshotView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.node.isDismissing {
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
let transition: ContainedViewLayoutTransition = self.isMinimized ? .immediate : .animated(duration: 0.2, curve: .easeInOut)
|
||||
transition.updateAlpha(node: self.node.dim, alpha: self.isMinimized ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
|
@ -1,445 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import AppBundle
|
||||
|
||||
let maxInteritemSpacing: CGFloat = 240.0
|
||||
let sectionInsetTop: CGFloat = 40.0
|
||||
let sectionInsetBottom: CGFloat = 0.0
|
||||
let zOffset: CGFloat = -60.0
|
||||
|
||||
let perspectiveCorrection: CGFloat = -1.0 / 1000.0
|
||||
let maxRotationAngle: CGFloat = -CGFloat.pi / 2.2
|
||||
|
||||
extension CATransform3D {
|
||||
func interpolate(other: CATransform3D, progress: CGFloat) -> CATransform3D {
|
||||
var vectors = Array<CGFloat>(repeating: 0.0, count: 16)
|
||||
vectors[0] = self.m11 + (other.m11 - self.m11) * progress
|
||||
vectors[1] = self.m12 + (other.m12 - self.m12) * progress
|
||||
vectors[2] = self.m13 + (other.m13 - self.m13) * progress
|
||||
vectors[3] = self.m14 + (other.m14 - self.m14) * progress
|
||||
vectors[4] = self.m21 + (other.m21 - self.m21) * progress
|
||||
vectors[5] = self.m22 + (other.m22 - self.m22) * progress
|
||||
vectors[6] = self.m23 + (other.m23 - self.m23) * progress
|
||||
vectors[7] = self.m24 + (other.m24 - self.m24) * progress
|
||||
vectors[8] = self.m31 + (other.m31 - self.m31) * progress
|
||||
vectors[9] = self.m32 + (other.m32 - self.m32) * progress
|
||||
vectors[10] = self.m33 + (other.m33 - self.m33) * progress
|
||||
vectors[11] = self.m34 + (other.m34 - self.m34) * progress
|
||||
vectors[12] = self.m41 + (other.m41 - self.m41) * progress
|
||||
vectors[13] = self.m42 + (other.m42 - self.m42) * progress
|
||||
vectors[14] = self.m43 + (other.m43 - self.m43) * progress
|
||||
vectors[15] = self.m44 + (other.m44 - self.m44) * progress
|
||||
|
||||
return CATransform3D(m11: vectors[0], m12: vectors[1], m13: vectors[2], m14: vectors[3], m21: vectors[4], m22: vectors[5], m23: vectors[6], m24: vectors[7], m31: vectors[8], m32: vectors[9], m33: vectors[10], m34: vectors[11], m41: vectors[12], m42: vectors[13], m43: vectors[14], m44: vectors[15])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func angle(for origin: CGFloat, itemCount: Int, bounds: CGRect, contentHeight: CGFloat?) -> CGFloat {
|
||||
var rotationAngle = rotationAngleAt0(itemCount: itemCount)
|
||||
|
||||
var contentOffset = bounds.origin.y
|
||||
if contentOffset < 0.0 {
|
||||
contentOffset *= 2.0
|
||||
}
|
||||
// } else if let contentHeight = contentHeight, bounds.maxY > contentHeight {
|
||||
//// let maxContentOffset = contentHeight - bounds.height
|
||||
//// let delta = contentOffset - maxContentOffset
|
||||
//// contentOffset = maxContentOffset + delta / 2.0
|
||||
// }
|
||||
|
||||
var yOnScreen = origin - contentOffset - sectionInsetTop
|
||||
if yOnScreen < 0 {
|
||||
yOnScreen = 0
|
||||
} else if yOnScreen > bounds.height {
|
||||
yOnScreen = bounds.height
|
||||
}
|
||||
|
||||
let maxRotationVariance = maxRotationAngle - rotationAngleAt0(itemCount: itemCount)
|
||||
rotationAngle += (maxRotationVariance / bounds.height) * yOnScreen
|
||||
|
||||
return rotationAngle
|
||||
}
|
||||
|
||||
private func final3dTransform(for origin: CGFloat, size: CGSize, contentHeight: CGFloat?, itemCount: Int, forcedAngle: CGFloat? = nil, additionalAngle: CGFloat? = nil, bounds: CGRect) -> CATransform3D {
|
||||
var transform = CATransform3DIdentity
|
||||
transform.m34 = perspectiveCorrection
|
||||
|
||||
let rotationAngle = forcedAngle ?? angle(for: origin, itemCount: itemCount, bounds: bounds, contentHeight: contentHeight)
|
||||
var effectiveRotationAngle = rotationAngle
|
||||
if let additionalAngle = additionalAngle {
|
||||
effectiveRotationAngle += additionalAngle
|
||||
}
|
||||
|
||||
let r = size.height / 2.0 + abs(zOffset / sin(rotationAngle))
|
||||
|
||||
let zTranslation = r * sin(rotationAngle)
|
||||
let yTranslation: CGFloat = r * (1 - cos(rotationAngle))
|
||||
|
||||
let zTranslateTransform = CATransform3DTranslate(transform, 0.0, -yTranslation, zTranslation)
|
||||
|
||||
let rotateTransform = CATransform3DRotate(zTranslateTransform, effectiveRotationAngle, 1.0, 0.0, 0.0)
|
||||
|
||||
return rotateTransform
|
||||
}
|
||||
|
||||
private func interitemSpacing(itemCount: Int, bounds: CGRect) -> CGFloat {
|
||||
var interitemSpacing = maxInteritemSpacing
|
||||
if itemCount > 0 {
|
||||
interitemSpacing = (bounds.height - sectionInsetTop - sectionInsetBottom) / CGFloat(min(itemCount, 5))
|
||||
}
|
||||
return interitemSpacing
|
||||
}
|
||||
|
||||
private func frameForIndex(index: Int, size: CGSize, itemCount: Int, bounds: CGRect) -> CGRect {
|
||||
let spacing = interitemSpacing(itemCount: itemCount, bounds: bounds)
|
||||
let y = sectionInsetTop + spacing * CGFloat(index)
|
||||
let origin = CGPoint(x: 0, y: y)
|
||||
|
||||
return CGRect(origin: origin, size: size)
|
||||
}
|
||||
|
||||
private func rotationAngleAt0(itemCount: Int) -> CGFloat {
|
||||
let multiplier: CGFloat = min(CGFloat(itemCount), 5.0) - 1.0
|
||||
return -CGFloat.pi / 7.0 - CGFloat.pi / 7.0 * multiplier / 4.0
|
||||
}
|
||||
|
||||
private let shadowImage: UIImage? = {
|
||||
return generateImage(CGSize(width: 1.0, height: 640.0), rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor] as CFArray
|
||||
|
||||
var locations: [CGFloat] = [0.0, 0.65, 1.0]
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: bounds.height), options: [])
|
||||
})
|
||||
}()
|
||||
|
||||
class StackItemContainerNode: ASDisplayNode {
|
||||
private let node: ASDisplayNode
|
||||
private let shadowNode: ASImageNode
|
||||
|
||||
var tapped: (() -> Void)?
|
||||
var highlighted: ((Bool) -> Void)?
|
||||
|
||||
init(node: ASDisplayNode) {
|
||||
self.node = node
|
||||
self.shadowNode = ASImageNode()
|
||||
self.shadowNode.displaysAsynchronously = false
|
||||
self.shadowNode.displayWithoutProcessing = true
|
||||
self.shadowNode.contentMode = .scaleToFill
|
||||
|
||||
super.init()
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.cornerRadius = 10.0
|
||||
applySmoothRoundedCorners(self.layer)
|
||||
|
||||
self.shadowNode.image = shadowImage
|
||||
|
||||
self.addSubnode(self.node)
|
||||
self.addSubnode(self.shadowNode)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
||||
recognizer.tapActionAtPoint = { point in
|
||||
return .waitForSingleTap
|
||||
}
|
||||
recognizer.highlight = { [weak self] point in
|
||||
if let point = point, point.x > 280.0 {
|
||||
self?.highlighted?(true)
|
||||
} else {
|
||||
self?.highlighted?(false)
|
||||
}
|
||||
}
|
||||
self.view.addGestureRecognizer(recognizer)
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.shadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
|
||||
switch gesture {
|
||||
case .tap:
|
||||
self.tapped?()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
self.node.frame = self.bounds
|
||||
self.shadowNode.frame = self.bounds
|
||||
}
|
||||
}
|
||||
|
||||
public class StackContainerNode: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDelegate {
|
||||
private let scrollNode: ASScrollNode
|
||||
private var nodes: [StackItemContainerNode]
|
||||
|
||||
private var deleteGestureRecognizer: UIPanGestureRecognizer?
|
||||
private var offsetsForDeletingItems: [Int: CGPoint]?
|
||||
private var currentDeletingIndexPath: Int?
|
||||
private var deletingOffset: CGFloat?
|
||||
|
||||
private var animatingIn = false
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
override public init() {
|
||||
self.scrollNode = ASScrollNode()
|
||||
self.nodes = []
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = .black
|
||||
|
||||
self.addSubnode(self.scrollNode)
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
|
||||
self.scrollNode.view.alwaysBounceVertical = true
|
||||
|
||||
let deleteGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPanToDelete(gestureRecognizer:)))
|
||||
deleteGestureRecognizer.delegate = self.wrappedGestureRecognizerDelegate
|
||||
deleteGestureRecognizer.delaysTouchesBegan = true
|
||||
self.scrollNode.view.addGestureRecognizer(deleteGestureRecognizer)
|
||||
self.deleteGestureRecognizer = deleteGestureRecognizer
|
||||
}
|
||||
|
||||
func item(forYPosition y: CGFloat) -> Int? {
|
||||
let itemCount = self.nodes.count
|
||||
let bounds = self.scrollNode.bounds
|
||||
|
||||
let spacing = interitemSpacing(itemCount: itemCount, bounds: bounds)
|
||||
return max(0, min(Int(floor((y - sectionInsetTop) / spacing)), itemCount - 1))
|
||||
}
|
||||
|
||||
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else {
|
||||
return false
|
||||
}
|
||||
|
||||
let touch = panGesture.location(in: gestureRecognizer.view)
|
||||
let velocity = panGesture.velocity(in: gestureRecognizer.view)
|
||||
|
||||
if abs(velocity.x) > abs(velocity.y), let item = self.item(forYPosition: touch.y) {
|
||||
return item > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@objc func didPanToDelete(gestureRecognizer: UIPanGestureRecognizer) {
|
||||
let scrollView = self.scrollNode.view
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
let touch = gestureRecognizer.location(in: scrollView)
|
||||
guard let item = self.item(forYPosition: touch.y) else { return }
|
||||
|
||||
self.currentDeletingIndexPath = item
|
||||
case .changed:
|
||||
guard let _ = self.currentDeletingIndexPath else { return }
|
||||
|
||||
var delta = gestureRecognizer.translation(in: scrollView)
|
||||
delta.y = 0
|
||||
|
||||
if let offset = self.deletingOffset {
|
||||
self.deletingOffset = offset + delta.x
|
||||
} else {
|
||||
self.deletingOffset = delta.x
|
||||
}
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: scrollView)
|
||||
|
||||
self.updateLayout()
|
||||
case .ended:
|
||||
if let _ = self.currentDeletingIndexPath {
|
||||
if let offset = self.deletingOffset {
|
||||
if offset < -self.frame.width / 2.0 {
|
||||
self.deletingOffset = -self.frame.width
|
||||
} else {
|
||||
self.deletingOffset = nil
|
||||
self.currentDeletingIndexPath = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.updateLayout()
|
||||
}
|
||||
case .cancelled, .failed:
|
||||
self.currentDeletingIndexPath = nil
|
||||
self.deletingOffset = nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func setup() {
|
||||
let images: [UIImage] = [UIImage(bundleImageName: "Settings/test1")!, UIImage(bundleImageName: "Settings/test5")!, UIImage(bundleImageName: "Settings/test4")!, UIImage(bundleImageName: "Settings/test3")!, UIImage(bundleImageName: "Settings/test2")!]
|
||||
for i in 0 ..< 5 {
|
||||
let node = ASImageNode()
|
||||
node.image = images[i]
|
||||
|
||||
let containerNode = StackItemContainerNode(node: node)
|
||||
containerNode.tapped = { [weak self] in
|
||||
self?.animateIn(index: i)
|
||||
}
|
||||
containerNode.highlighted = { [weak self] highlighted in
|
||||
self?.highlight(index: i, value: highlighted)
|
||||
}
|
||||
self.nodes.append(containerNode)
|
||||
}
|
||||
|
||||
var index: Int = 0
|
||||
let bounds = self.scrollNode.view.bounds
|
||||
let itemCount = self.nodes.count
|
||||
|
||||
for node in self.nodes {
|
||||
self.scrollNode.addSubnode(node)
|
||||
|
||||
let size = CGSize(width: self.frame.width, height: self.frame.height)
|
||||
let frame = frameForIndex(index: index, size: size, itemCount: itemCount, bounds: bounds)
|
||||
node.frame = frame
|
||||
let transform = final3dTransform(for: frame.minY, size: frame.size, contentHeight: nil, itemCount: itemCount, bounds: bounds)
|
||||
node.transform = transform
|
||||
index += 1
|
||||
}
|
||||
|
||||
if let lastFrame = self.nodes.last?.frame {
|
||||
self.scrollNode.view.contentSize = CGSize(width: self.frame.width, height: lastFrame.minY)
|
||||
}
|
||||
}
|
||||
|
||||
public func animateIn(index: Int) {
|
||||
let node = self.nodes[index]
|
||||
|
||||
self.animatingIn = true
|
||||
self.scrollNode.view.isUserInteractionEnabled = false
|
||||
node.animateIn()
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
node.transform = CATransform3DIdentity
|
||||
node.position = CGPoint(x: self.scrollNode.frame.width / 2.0, y: self.scrollNode.frame.height / 2.0)
|
||||
}
|
||||
|
||||
for i in 0 ..< index {
|
||||
let node = self.nodes[i]
|
||||
node.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -550.0), duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, mediaTimingFunction: nil, removeOnCompletion: false, additive: true, force: false, completion: nil)
|
||||
}
|
||||
|
||||
for i in (index + 1) ..< self.nodes.count {
|
||||
let node = self.nodes[i]
|
||||
node.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 550.0), duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, mediaTimingFunction: nil, removeOnCompletion: false, additive: true, force: false, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
public func highlight(index: Int, value: Bool) {
|
||||
let node = self.nodes[index]
|
||||
|
||||
let bounds = self.scrollNode.view.bounds
|
||||
let contentHeight = self.scrollNode.view.contentSize.height
|
||||
let itemCount = self.nodes.count
|
||||
|
||||
UIView.animate(withDuration: 0.4) {
|
||||
let transform = final3dTransform(for: node.frame.minY, size: node.frame.size, contentHeight: contentHeight, itemCount: itemCount, additionalAngle: value ? 0.04 : nil, bounds: bounds)
|
||||
node.transform = transform
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard !self.animatingIn else {
|
||||
return
|
||||
}
|
||||
self.updateLayout()
|
||||
}
|
||||
|
||||
func updateLayout() {
|
||||
let bounds = self.scrollNode.view.bounds
|
||||
let contentHeight = self.scrollNode.view.contentSize.height
|
||||
let itemCount = self.nodes.count
|
||||
|
||||
var index: Int = 0
|
||||
for node in self.nodes {
|
||||
let initialTransform = final3dTransform(for: node.frame.minY, size: node.frame.size, contentHeight: contentHeight, itemCount: itemCount, bounds: bounds)
|
||||
let initialFrame = frameForIndex(index: index, size: node.frame.size, itemCount: itemCount, bounds: bounds)
|
||||
|
||||
var targetTransform: CATransform3D?
|
||||
var targetPosition: CGPoint?
|
||||
|
||||
var finalPosition = initialFrame.center
|
||||
|
||||
if let deletingIndex = self.currentDeletingIndexPath, let offset = self.deletingOffset {
|
||||
if deletingIndex == index {
|
||||
finalPosition = CGPoint(x: self.frame.width / 2.0 + min(offset, 0.0), y: node.position.y)
|
||||
} else if index < deletingIndex {
|
||||
let frame = frameForIndex(index: index, size: node.frame.size, itemCount: itemCount - 1, bounds: bounds)
|
||||
targetPosition = frame.center
|
||||
|
||||
let spacing = interitemSpacing(itemCount: itemCount - 1, bounds: bounds)
|
||||
targetTransform = final3dTransform(for: frame.minY, size: node.frame.size, contentHeight: contentHeight - node.frame.height - spacing, itemCount: itemCount - 1, bounds: bounds)
|
||||
} else {
|
||||
let frame = frameForIndex(index: index - 1, size: node.frame.size, itemCount: itemCount - 1, bounds: bounds)
|
||||
targetPosition = frame.center
|
||||
|
||||
let spacing = interitemSpacing(itemCount: itemCount - 1, bounds: bounds)
|
||||
targetTransform = final3dTransform(for: frame.minY, size: node.frame.size, contentHeight: contentHeight - node.frame.height - spacing, itemCount: itemCount - 1, bounds: bounds)
|
||||
}
|
||||
} else {
|
||||
node.position = initialFrame.center
|
||||
}
|
||||
|
||||
var finalTransform = initialTransform
|
||||
if let targetTransform = targetTransform, let offset = self.deletingOffset {
|
||||
let progress = min(1.0, abs(offset / (self.frame.width)))
|
||||
finalTransform = initialTransform.interpolate(other: targetTransform, progress: progress)
|
||||
}
|
||||
|
||||
if let targetPosition = targetPosition, let offset = self.deletingOffset {
|
||||
let progress = min(1.0, abs(offset / (self.frame.width)))
|
||||
finalPosition = CGPoint(x: finalPosition.x + (targetPosition.x - finalPosition.x) * progress, y: finalPosition.y + (targetPosition.y - finalPosition.y) * progress)
|
||||
}
|
||||
|
||||
node.transform = finalTransform
|
||||
node.position = finalPosition
|
||||
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
public func update(size: CGSize) {
|
||||
let hadValidLayout = self.validLayout != nil
|
||||
self.validLayout = size
|
||||
|
||||
self.scrollNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
if !hadValidLayout {
|
||||
self.setup()
|
||||
}
|
||||
}
|
||||
}
|
@ -1177,9 +1177,11 @@ public extension ContainedViewLayoutTransition {
|
||||
self.updateTransform(layer: node.layer, transform: transform, beginWithCurrentState: beginWithCurrentState, delay: delay, completion: completion)
|
||||
}
|
||||
|
||||
func updateTransform(layer: CALayer, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||
let transform = CATransform3DMakeAffineTransform(transform)
|
||||
func updateTransform(node: ASDisplayNode, transform: CATransform3D, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||
self.updateTransform(layer: node.layer, transform: transform, beginWithCurrentState: beginWithCurrentState, delay: delay, completion: completion)
|
||||
}
|
||||
|
||||
func updateTransform(layer: CALayer, transform: CATransform3D, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||
if CATransform3DEqualToTransform(layer.transform, transform) {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
@ -1207,6 +1209,11 @@ public extension ContainedViewLayoutTransition {
|
||||
}
|
||||
}
|
||||
|
||||
func updateTransform(layer: CALayer, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||
let transform = CATransform3DMakeAffineTransform(transform)
|
||||
self.updateTransform(layer: layer, transform: transform, beginWithCurrentState: beginWithCurrentState, delay: delay, completion: completion)
|
||||
}
|
||||
|
||||
func updateTransformScale(node: ASDisplayNode, scale: CGFloat, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||
let t = node.layer.transform
|
||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
|
@ -0,0 +1,13 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
|
||||
public protocol MinimizedContainer: ASDisplayNode {
|
||||
var willMaximize: (() -> Void)? { get set }
|
||||
|
||||
func addController(_ viewController: ViewController, transition: ContainedViewLayoutTransition)
|
||||
func maximizeController(_ viewController: ViewController, animated: Bool, completion: @escaping (Bool) -> Void)
|
||||
func dismissAll(completion: @escaping () -> Void)
|
||||
|
||||
func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition)
|
||||
func collapsedHeight(layout: ContainerViewLayout) -> CGFloat
|
||||
}
|
@ -150,6 +150,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
private var rootModalFrame: NavigationModalFrame?
|
||||
private var modalContainers: [NavigationModalContainer] = []
|
||||
private var overlayContainers: [NavigationOverlayContainer] = []
|
||||
private var minimizedContainer: MinimizedContainer?
|
||||
|
||||
private var globalOverlayContainers: [NavigationOverlayContainer] = []
|
||||
private var globalOverlayBelowKeyboardContainerParent: GlobalOverlayContainerParent?
|
||||
@ -180,15 +181,6 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
}
|
||||
}
|
||||
|
||||
private var _minimizedViewControllers: [ViewController] = []
|
||||
open var minimizedViewControllers: [UIViewController] {
|
||||
get {
|
||||
return self._minimizedViewControllers.map { $0 as UIViewController }
|
||||
} set(value) {
|
||||
self.setMinimizedViewControllers(value, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
private var _viewControllersPromise = ValuePromise<[UIViewController]>()
|
||||
public var viewControllersSignal: Signal<[UIViewController], NoError> {
|
||||
return _viewControllersPromise.get()
|
||||
@ -475,7 +467,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
transition.updateFrame(node: globalOverlayContainerParent, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
}
|
||||
|
||||
let navigationLayout = makeNavigationLayout(mode: self.mode, layout: layout, controllers: self._viewControllers, minimizedControllers: self._minimizedViewControllers)
|
||||
let navigationLayout = makeNavigationLayout(mode: self.mode, layout: layout, controllers: self._viewControllers)
|
||||
|
||||
var transition = transition
|
||||
var statusBarStyle: StatusBarStyle = .Ignore
|
||||
@ -497,9 +489,8 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
let modalContainer: NavigationModalContainer
|
||||
if let existingModalContainer = existingModalContainer {
|
||||
modalContainer = existingModalContainer
|
||||
modalContainer.isMinimized = navigationLayout.modal[i].isMinimized
|
||||
} else {
|
||||
modalContainer = NavigationModalContainer(theme: self.theme, isFlat: navigationLayout.modal[i].isFlat, isMinimized: navigationLayout.modal[i].isMinimized, controllerRemoved: { [weak self] controller in
|
||||
modalContainer = NavigationModalContainer(theme: self.theme, isFlat: navigationLayout.modal[i].isFlat, controllerRemoved: { [weak self] controller in
|
||||
self?.controllerRemoved(controller)
|
||||
})
|
||||
modalContainer.container.statusBarStyleUpdated = { [weak self] transition in
|
||||
@ -534,32 +525,6 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
strongSelf.setViewControllers(controllers, animated: false)
|
||||
strongSelf.ignoreInputHeight = false
|
||||
}
|
||||
modalContainer.minimizedRequestMaximize = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var controllers = self._viewControllers
|
||||
for controller in self._minimizedViewControllers {
|
||||
controllers.append(controller)
|
||||
}
|
||||
self._viewControllers = controllers
|
||||
self._minimizedViewControllers = []
|
||||
|
||||
self.updateContainersNonReentrant(transition: .animated(duration: 0.5, curve: .spring))
|
||||
}
|
||||
modalContainer.minimizedRequestDismiss = { [weak self, weak modalContainer] animated in
|
||||
guard let self, let modalContainer else {
|
||||
return
|
||||
}
|
||||
|
||||
let minimizedControllers = self.minimizedViewControllers.filter { controller in
|
||||
return !modalContainer.container.controllers.contains(where: { $0 === controller })
|
||||
}
|
||||
if minimizedControllers.count != self.minimizedViewControllers.count {
|
||||
self.setMinimizedViewControllers(minimizedControllers, animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
modalContainers.append(modalContainer)
|
||||
}
|
||||
@ -731,7 +696,6 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
var topVisibleModalContainerWithStatusBar: NavigationModalContainer?
|
||||
var visibleModalCount = 0
|
||||
var topModalIsFlat = false
|
||||
var topModalIsMinimized = false
|
||||
var topFlatModalHasProgress = false
|
||||
let isLandscape = layout.orientation == .landscape
|
||||
var hasVisibleStandaloneModal = false
|
||||
@ -791,16 +755,13 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
}
|
||||
|
||||
if modalContainer.supernode != nil {
|
||||
if !hasVisibleStandaloneModal && !isStandaloneModal && !modalContainer.isFlat && !modalContainer.isMinimized {
|
||||
if !hasVisibleStandaloneModal && !isStandaloneModal && !modalContainer.isFlat {
|
||||
visibleModalCount += 1
|
||||
}
|
||||
if isStandaloneModal {
|
||||
hasVisibleStandaloneModal = true
|
||||
visibleModalCount = 0
|
||||
}
|
||||
|
||||
topModalIsMinimized = modalContainer.isMinimized
|
||||
|
||||
if previousModalContainer == nil {
|
||||
topModalIsFlat = modalContainer.isFlat
|
||||
|
||||
@ -857,6 +818,11 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
}
|
||||
}
|
||||
|
||||
if self.isMaximizing && layout.size.width < layout.size.height {
|
||||
modalStyleOverlayTransitionFactor = 1.0
|
||||
topFlatModalHasProgress = true
|
||||
}
|
||||
|
||||
layout.additionalInsets.left = max(layout.intrinsicInsets.left, additionalSideInsets.left)
|
||||
layout.additionalInsets.right = max(layout.intrinsicInsets.right, additionalSideInsets.right)
|
||||
|
||||
@ -865,18 +831,18 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
if let rootContainer = self.rootContainer {
|
||||
switch rootContainer {
|
||||
case let .flat(flatContainer):
|
||||
if let previousModalContainer, !previousModalContainer.isMinimized {
|
||||
flatContainer.keyboardViewManager = nil
|
||||
flatContainer.canHaveKeyboardFocus = false
|
||||
} else {
|
||||
if previousModalContainer == nil {
|
||||
flatContainer.keyboardViewManager = self.keyboardViewManager
|
||||
flatContainer.canHaveKeyboardFocus = true
|
||||
} else {
|
||||
flatContainer.keyboardViewManager = nil
|
||||
flatContainer.canHaveKeyboardFocus = false
|
||||
}
|
||||
|
||||
var updatedSize = layout.size
|
||||
var updatedIntrinsicInsets = layout.intrinsicInsets
|
||||
if topModalIsMinimized && (layout.inputHeight ?? 0.0).isZero {
|
||||
updatedSize.height -= 81.0
|
||||
if let minimizedContainer = self.minimizedContainer, (layout.inputHeight ?? 0.0).isZero {
|
||||
updatedSize.height -= minimizedContainer.collapsedHeight(layout: layout)
|
||||
updatedIntrinsicInsets.bottom = 0.0
|
||||
}
|
||||
let updatedLayout = layout.withUpdatedSize(updatedSize).withUpdatedIntrinsicInsets(updatedIntrinsicInsets)
|
||||
@ -1145,6 +1111,11 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
}
|
||||
}
|
||||
|
||||
if let minimizedContainer = self.minimizedContainer {
|
||||
minimizedContainer.frame = CGRect(origin: .zero, size: layout.size)
|
||||
minimizedContainer.updateLayout(layout, transition: transition)
|
||||
}
|
||||
|
||||
if self.inCallStatusBar != nil {
|
||||
statusBarStyle = .White
|
||||
}
|
||||
@ -1432,11 +1403,6 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
self.setViewControllers(controllers, animated: animated)
|
||||
self.ignoreInputHeight = false
|
||||
}
|
||||
|
||||
let minimizedControllers = self.minimizedViewControllers.filter({ $0 !== controller })
|
||||
if minimizedControllers.count != self.minimizedViewControllers.count {
|
||||
self.setMinimizedViewControllers(minimizedControllers, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
public func replaceController(_ controller: ViewController, with other: ViewController, animated: Bool) {
|
||||
@ -1576,27 +1542,70 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
self._viewControllersPromise.set(self.viewControllers)
|
||||
}
|
||||
|
||||
private func setMinimizedViewControllers(_ viewControllers: [UIViewController], animated: Bool) {
|
||||
self._viewControllers = self._viewControllers.filter { controller in
|
||||
return !viewControllers.contains(controller)
|
||||
}
|
||||
public func minimizeViewController(_ viewController: ViewController, damping: CGFloat?, velocity: CGFloat? = nil, setupContainer: (MinimizedContainer?) -> MinimizedContainer?, animated: Bool) {
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .customSpring(damping: damping ?? 124.0, initialVelocity: velocity ?? 0.0)) : .immediate
|
||||
|
||||
self._minimizedViewControllers = viewControllers.map { controller in
|
||||
let controller = controller as! ViewController
|
||||
controller.navigation_setNavigationController(self)
|
||||
return controller
|
||||
}
|
||||
if let layout = self.validLayout {
|
||||
self.updateContainers(layout: layout, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate, completion: { [weak self] in
|
||||
self?.notifyAccessibilityScreenChanged()
|
||||
})
|
||||
let minimizedContainer = setupContainer(self.minimizedContainer)
|
||||
if self.minimizedContainer !== minimizedContainer {
|
||||
minimizedContainer?.willMaximize = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isMaximizing = true
|
||||
self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
self.minimizedContainer?.removeFromSupernode()
|
||||
self.minimizedContainer = minimizedContainer
|
||||
|
||||
if let minimizedContainer {
|
||||
if let modalContainer = self.modalContainers.first {
|
||||
self.displayNode.insertSubnode(minimizedContainer, belowSubnode: modalContainer)
|
||||
} else {
|
||||
self.displayNode.addSubnode(minimizedContainer)
|
||||
}
|
||||
}
|
||||
|
||||
self.updateContainersNonReentrant(transition: transition)
|
||||
}
|
||||
self.filterController(viewController, animated: true)
|
||||
minimizedContainer?.addController(viewController, transition: transition)
|
||||
}
|
||||
|
||||
public func minimizeViewController(_ viewController: UIViewController, animated: Bool) {
|
||||
var controllers = self.minimizedViewControllers
|
||||
controllers.append(viewController)
|
||||
self.setMinimizedViewControllers(controllers, animated: animated)
|
||||
private var isMaximizing = false
|
||||
public func maximizeViewController(_ viewController: ViewController, animated: Bool) {
|
||||
guard let minimizedContainer = self.minimizedContainer else {
|
||||
return
|
||||
}
|
||||
if animated {
|
||||
self.isMaximizing = true
|
||||
self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
minimizedContainer.maximizeController(viewController, animated: animated, completion: { [weak self] dismissed in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var viewControllers = self.viewControllers
|
||||
viewControllers.append(viewController)
|
||||
self.setViewControllers(viewControllers, animated: false)
|
||||
self.isMaximizing = false
|
||||
|
||||
if dismissed, let minimizedContainer = self.minimizedContainer {
|
||||
self.minimizedContainer = nil
|
||||
minimizedContainer.removeFromSupernode()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func dismissMinimizedControllers(animated: Bool) {
|
||||
guard let minimizedContainer = self.minimizedContainer else {
|
||||
return
|
||||
}
|
||||
self.minimizedContainer = nil
|
||||
|
||||
minimizedContainer.dismissAll(completion: { [weak minimizedContainer] in
|
||||
minimizedContainer?.removeFromSupernode()
|
||||
})
|
||||
self.updateContainersNonReentrant(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate)
|
||||
}
|
||||
|
||||
public var _keepModalDismissProgress = false
|
||||
@ -1862,8 +1871,9 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
return
|
||||
}
|
||||
transition.updateTransform(node: container, transform: CGAffineTransformMakeTranslation(offset, 0.0))
|
||||
if let minimizedModalContainer = self.modalContainers.first(where: { $0.isMinimized }) {
|
||||
transition.updateTransform(node: minimizedModalContainer, transform: CGAffineTransformMakeTranslation(offset, 0.0))
|
||||
|
||||
if let minimizedContainer = self.minimizedContainer {
|
||||
transition.updateTransform(node: minimizedContainer, transform: CGAffineTransformMakeTranslation(offset, 0.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ struct ModalContainerLayout {
|
||||
var controllers: [ViewController]
|
||||
var isFlat: Bool
|
||||
var isStandalone: Bool
|
||||
var isMinimized: Bool
|
||||
}
|
||||
|
||||
struct NavigationLayout {
|
||||
@ -20,7 +19,7 @@ struct NavigationLayout {
|
||||
var modal: [ModalContainerLayout]
|
||||
}
|
||||
|
||||
func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewLayout, controllers: [ViewController], minimizedControllers: [ViewController]) -> NavigationLayout {
|
||||
func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewLayout, controllers: [ViewController]) -> NavigationLayout {
|
||||
var rootControllers: [ViewController] = []
|
||||
var modalStack: [ModalContainerLayout] = []
|
||||
for controller in controllers {
|
||||
@ -55,7 +54,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL
|
||||
if requiresModal {
|
||||
controller._presentedInModal = true
|
||||
if beginsModal || modalStack.isEmpty || modalStack[modalStack.count - 1].isStandalone {
|
||||
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone, isMinimized: false))
|
||||
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone))
|
||||
} else {
|
||||
modalStack[modalStack.count - 1].controllers.append(controller)
|
||||
}
|
||||
@ -65,7 +64,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL
|
||||
controller._presentedInModal = true
|
||||
}
|
||||
if modalStack[modalStack.count - 1].isStandalone {
|
||||
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone, isMinimized: false))
|
||||
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone))
|
||||
} else {
|
||||
modalStack[modalStack.count - 1].controllers.append(controller)
|
||||
}
|
||||
@ -75,22 +74,6 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL
|
||||
}
|
||||
}
|
||||
|
||||
var minimizedModalContainer: ModalContainerLayout?
|
||||
for controller in minimizedControllers {
|
||||
controller._presentedInModal = false
|
||||
if var container = minimizedModalContainer {
|
||||
container.controllers.append(controller)
|
||||
minimizedModalContainer = container
|
||||
} else {
|
||||
let container = ModalContainerLayout(controllers: [controller], isFlat: false, isStandalone: false, isMinimized: true)
|
||||
minimizedModalContainer = container
|
||||
}
|
||||
}
|
||||
|
||||
if let minimizedModalContainer {
|
||||
modalStack.insert(minimizedModalContainer, at: 0)
|
||||
}
|
||||
|
||||
let rootLayout: RootNavigationLayout
|
||||
switch mode {
|
||||
case .single:
|
||||
|
@ -4,36 +4,14 @@ 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
|
||||
@ -42,9 +20,6 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
||||
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
|
||||
@ -67,14 +42,9 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
||||
}
|
||||
}
|
||||
|
||||
init(theme: NavigationControllerTheme, isFlat: Bool, isMinimized: Bool, controllerRemoved: @escaping (ViewController) -> Void) {
|
||||
init(theme: NavigationControllerTheme, isFlat: 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
|
||||
@ -84,28 +54,12 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
||||
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 {
|
||||
@ -120,11 +74,6 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
||||
}
|
||||
|
||||
applySmoothRoundedCorners(self.container.layer)
|
||||
applySmoothRoundedCorners(self.minimizedBackgroundNode.layer)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.minimizedTitleDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
@ -167,29 +116,21 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
||||
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
|
||||
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
|
||||
}
|
||||
@ -384,8 +325,6 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
||||
|
||||
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
|
||||
@ -418,7 +357,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
||||
if layout.metrics.widthClass == .compact || self.isFlat {
|
||||
self.panRecognizer?.isEnabled = true
|
||||
self.container.clipsToBounds = true
|
||||
if self.isFlat || self.isMinimized {
|
||||
if self.isFlat {
|
||||
self.dim.backgroundColor = .clear
|
||||
} else {
|
||||
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
||||
@ -427,7 +366,6 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
||||
self.container.cornerRadius = 0.0
|
||||
} else {
|
||||
self.container.cornerRadius = 10.0
|
||||
self.minimizedBackgroundNode.cornerRadius = self.container.cornerRadius
|
||||
}
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
@ -436,8 +374,6 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
||||
} else {
|
||||
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
}
|
||||
|
||||
self.minimizedBackgroundNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
}
|
||||
|
||||
var topInset: CGFloat
|
||||
@ -450,18 +386,10 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
||||
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
|
||||
if self.isFlat {
|
||||
topInset = 0.0
|
||||
} else if let statusBarHeight = layout.statusBarHeight {
|
||||
topInset += statusBarHeight
|
||||
}
|
||||
|
||||
let effectiveStatusBarHeight: CGFloat?
|
||||
@ -471,7 +399,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
||||
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)
|
||||
containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width, height: layout.size.height - topInset), 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
|
||||
@ -479,59 +407,6 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
||||
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 {
|
||||
@ -610,17 +485,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
||||
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
|
||||
positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: self.bounds.height + self.container.bounds.height / 2.0 + self.bounds.height), beginWithCurrentState: true, completion: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@ -648,14 +513,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
||||
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
|
||||
return self.dim.view
|
||||
}
|
||||
if self.isFlat {
|
||||
return result
|
||||
@ -710,22 +568,4 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
||||
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 {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -230,7 +230,8 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
|
||||
|
||||
private var navigationBarOrigin: CGFloat = 0.0
|
||||
|
||||
public var modalTopEdgeOffset: CGFloat = 0.0
|
||||
public var minimizedTopEdgeOffset: CGFloat = 0.0
|
||||
public var minimizedBounds: CGRect?
|
||||
open var isMinimized: Bool = false
|
||||
|
||||
open var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? {
|
||||
|
@ -235,9 +235,10 @@ public class ChatMessageStarsMediaInfoNode: ASDisplayNode {
|
||||
|
||||
let text: NSMutableAttributedString
|
||||
if let peer = arguments.message.peers[arguments.message.id.peerId] as? TelegramChannel, peer.flags.contains(.isCreator) || peer.adminRights != nil {
|
||||
text = NSMutableAttributedString(string: "⭐️\(arguments.media.amount)", font: textFont, textColor: .white)
|
||||
let amountString = presentationStringsFormattedNumber(Int32(arguments.media.amount), arguments.presentationData.dateTimeFormat.groupingSeparator)
|
||||
text = NSMutableAttributedString(string: "⭐️\(amountString)", font: textFont, textColor: .white)
|
||||
} else {
|
||||
text = NSMutableAttributedString(string: "Purchased", font: textFont, textColor: .white)
|
||||
text = NSMutableAttributedString(string: arguments.presentationData.strings.Chat_PaidMedia_Purchased, font: textFont, textColor: .white)
|
||||
}
|
||||
|
||||
var offset: CGFloat = 0.0
|
||||
|
@ -130,7 +130,8 @@ public class ChatMessageUnlockMediaNode: ASDisplayNode {
|
||||
let textFont = Font.medium(fontSize)
|
||||
|
||||
let padding: CGFloat = 10.0
|
||||
let text = NSMutableAttributedString(string: arguments.presentationData.strings.Chat_UnlockMedia("⭐️ \(arguments.media.amount)").string, font: textFont, textColor: .white)
|
||||
let amountString = presentationStringsFormattedNumber(Int32(arguments.media.amount), arguments.presentationData.dateTimeFormat.groupingSeparator)
|
||||
let text = NSMutableAttributedString(string: arguments.presentationData.strings.Chat_PaidMedia_UnlockMedia("⭐️ \(amountString)").string, font: textFont, textColor: .white)
|
||||
if let range = text.string.range(of: "⭐️") {
|
||||
text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: text.string))
|
||||
text.addAttribute(.baselineOffset, value: 0.5, range: NSRange(range, in: text.string))
|
||||
|
23
submodules/TelegramUI/Components/MinimizedContainer/BUILD
Normal file
23
submodules/TelegramUI/Components/MinimizedContainer/BUILD
Normal file
@ -0,0 +1,23 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "MinimizedContainer",
|
||||
module_name = "MinimizedContainer",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/AccountContext",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,142 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
|
||||
final class MinimizedHeaderNode: ASDisplayNode {
|
||||
var theme: NavigationControllerTheme
|
||||
let strings: PresentationStrings
|
||||
|
||||
private let minimizedBackgroundNode: ASDisplayNode
|
||||
private let minimizedTitleNode: ImmediateTextNode
|
||||
private let minimizedCloseButton: HighlightableButtonNode
|
||||
private var minimizedTitleDisposable: Disposable?
|
||||
|
||||
private var _controllers: [Weak<ViewController>] = []
|
||||
var controllers: [ViewController] {
|
||||
get {
|
||||
return self._controllers.compactMap { $0.value }
|
||||
}
|
||||
set {
|
||||
if !newValue.isEmpty {
|
||||
if newValue.count != self.controllers.count {
|
||||
self._controllers = newValue.map { Weak($0) }
|
||||
|
||||
self.minimizedTitleDisposable?.dispose()
|
||||
self.minimizedTitleDisposable = nil
|
||||
|
||||
var signals: [Signal<String?, NoError>] = []
|
||||
for controller in newValue {
|
||||
signals.append(controller.titleSignal)
|
||||
}
|
||||
|
||||
self.minimizedTitleDisposable = (combineLatest(signals)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] titles in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let titles = titles.compactMap { $0 }
|
||||
if titles.count == 1, let title = titles.first {
|
||||
self.title = title
|
||||
} else if let title = titles.last {
|
||||
let othersString = self.strings.WebApp_MinimizedTitle_Others(Int32(titles.count - 1))
|
||||
self.title = self.strings.WebApp_MinimizedTitleFormat(title, othersString).string
|
||||
} else {
|
||||
self.title = nil
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
self.minimizedTitleDisposable?.dispose()
|
||||
self.minimizedTitleDisposable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var title: String? {
|
||||
didSet {
|
||||
if let (size, insets) = self.validLayout {
|
||||
self.update(size: size, insets: insets, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var requestClose: () -> Void = {}
|
||||
var requestMaximize: () -> Void = {}
|
||||
|
||||
private var validLayout: (CGSize, UIEdgeInsets)?
|
||||
|
||||
init(theme: NavigationControllerTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
||||
self.minimizedBackgroundNode = ASDisplayNode()
|
||||
self.minimizedBackgroundNode.cornerRadius = 10.0
|
||||
self.minimizedBackgroundNode.clipsToBounds = true
|
||||
self.minimizedBackgroundNode.backgroundColor = theme.navigationBar.opaqueBackgroundColor
|
||||
if #available(iOS 11.0, *) {
|
||||
self.minimizedBackgroundNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
}
|
||||
|
||||
self.minimizedTitleNode = ImmediateTextNode()
|
||||
|
||||
self.minimizedCloseButton = HighlightableButtonNode()
|
||||
self.minimizedCloseButton.setImage(UIImage(bundleImageName: "Instant View/Close"), for: .normal)
|
||||
|
||||
super.init()
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.addSubnode(self.minimizedBackgroundNode)
|
||||
self.minimizedBackgroundNode.addSubnode(self.minimizedTitleNode)
|
||||
self.minimizedBackgroundNode.addSubnode(self.minimizedCloseButton)
|
||||
|
||||
self.minimizedCloseButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside)
|
||||
|
||||
applySmoothRoundedCorners(self.minimizedBackgroundNode.layer)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.minimizedTitleDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.minimizedBackgroundNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.maximizeTapGesture(_:))))
|
||||
}
|
||||
|
||||
@objc private func closePressed() {
|
||||
self.requestClose()
|
||||
}
|
||||
|
||||
@objc private func maximizeTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
let location = recognizer.location(in: self.view)
|
||||
if location.x < 48.0 {
|
||||
self.requestClose()
|
||||
} else {
|
||||
self.requestMaximize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (size, insets)
|
||||
|
||||
let headerHeight: CGFloat = 44.0
|
||||
let titleSideInset: CGFloat = 56.0 + insets.left
|
||||
|
||||
self.minimizedTitleNode.attributedText = NSAttributedString(string: title ?? "", font: Font.bold(17.0), textColor: self.theme.navigationBar.primaryTextColor)
|
||||
|
||||
let titleSize = self.minimizedTitleNode.updateLayout(CGSize(width: size.width - titleSideInset * 2.0, height: headerHeight))
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((headerHeight - titleSize.height) / 2.0)), size: titleSize)
|
||||
self.minimizedTitleNode.bounds = CGRect(origin: .zero, size: titleFrame.size)
|
||||
transition.updatePosition(node: self.minimizedTitleNode, position: titleFrame.center)
|
||||
transition.updateFrame(node: self.minimizedCloseButton, frame: CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: CGSize(width: 44.0, height: 44.0)))
|
||||
|
||||
transition.updateFrame(node: self.minimizedBackgroundNode, frame: CGRect(origin: .zero, size: CGSize(width: size.width, height: 243.0)))
|
||||
}
|
||||
}
|
@ -0,0 +1,926 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ComponentFlow
|
||||
import AccountContext
|
||||
|
||||
private let minimizedNavigationHeight: CGFloat = 44.0
|
||||
private let minimizedTopMargin: CGFloat = 3.0
|
||||
|
||||
final class ScrollViewImpl: UIScrollView {
|
||||
var passthrough = false
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
if result === self && self.passthrough {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScrollViewDelegate, ASGestureRecognizerDelegate {
|
||||
final class Item {
|
||||
let id: AnyHashable
|
||||
let controller: ViewController
|
||||
|
||||
init(id: AnyHashable, controller: ViewController) {
|
||||
self.id = id
|
||||
self.controller = controller
|
||||
}
|
||||
}
|
||||
|
||||
final class ItemNode: ASDisplayNode {
|
||||
var theme: PresentationTheme {
|
||||
didSet {
|
||||
if self.theme !== oldValue {
|
||||
self.headerNode.theme = NavigationControllerTheme(presentationTheme: self.theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let item: Item
|
||||
private let containerNode: ASDisplayNode
|
||||
private let headerNode: MinimizedHeaderNode
|
||||
private let dimCoverNode: ASDisplayNode
|
||||
private let shadowNode: ASImageNode
|
||||
|
||||
var tapped: (() -> Void)?
|
||||
var highlighted: ((Bool) -> Void)?
|
||||
var closeTapped: (() -> Void)?
|
||||
|
||||
var isCovered: Bool = false {
|
||||
didSet {
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
transition.updateAlpha(node: self.dimCoverNode, alpha: self.isCovered ? 0.25 : 0.0)
|
||||
}
|
||||
}
|
||||
var isExpanded = false
|
||||
|
||||
private var validLayout: (CGSize, UIEdgeInsets)?
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, item: Item) {
|
||||
self.theme = theme
|
||||
self.item = item
|
||||
|
||||
self.shadowNode = ASImageNode()
|
||||
self.shadowNode.clipsToBounds = true
|
||||
self.shadowNode.cornerRadius = 10.0
|
||||
self.shadowNode.displaysAsynchronously = false
|
||||
self.shadowNode.displayWithoutProcessing = true
|
||||
self.shadowNode.contentMode = .scaleToFill
|
||||
self.shadowNode.isUserInteractionEnabled = false
|
||||
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.isUserInteractionEnabled = false
|
||||
self.containerNode.cornerRadius = 10.0
|
||||
|
||||
self.headerNode = MinimizedHeaderNode(theme: NavigationControllerTheme(presentationTheme: theme), strings: strings)
|
||||
self.headerNode.layer.allowsGroupOpacity = true
|
||||
|
||||
self.dimCoverNode = ASDisplayNode()
|
||||
self.dimCoverNode.alpha = 0.0
|
||||
self.dimCoverNode.backgroundColor = UIColor.black
|
||||
self.dimCoverNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.cornerRadius = 10.0
|
||||
applySmoothRoundedCorners(self.layer)
|
||||
applySmoothRoundedCorners(self.containerNode.layer)
|
||||
|
||||
self.shadowNode.image = shadowImage
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
self.containerNode.addSubnode(self.item.controller.displayNode)
|
||||
self.addSubnode(self.headerNode)
|
||||
self.addSubnode(self.dimCoverNode)
|
||||
self.addSubnode(self.shadowNode)
|
||||
|
||||
self.headerNode.requestClose = { [weak self] in
|
||||
if let self {
|
||||
self.closeTapped?()
|
||||
}
|
||||
}
|
||||
|
||||
self.headerNode.requestMaximize = { [weak self] in
|
||||
if let self {
|
||||
self.tapped?()
|
||||
}
|
||||
}
|
||||
|
||||
self.headerNode.controllers = [item.controller]
|
||||
}
|
||||
|
||||
func setTitleControllers(_ controllers: [ViewController]?) {
|
||||
self.headerNode.controllers = controllers ?? [self.item.controller]
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.headerNode.alpha = 0.0
|
||||
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
|
||||
alphaTransition.updateAlpha(node: self.headerNode, alpha: 1.0)
|
||||
}
|
||||
|
||||
private var isDismissed = false
|
||||
func animateOut() {
|
||||
self.isDismissed = true
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
|
||||
transition.updateAlpha(node: self.headerNode, alpha: 0.0)
|
||||
transition.updateAlpha(node: self.shadowNode, alpha: 0.0)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
||||
recognizer.tapActionAtPoint = { point in
|
||||
return .waitForSingleTap
|
||||
}
|
||||
recognizer.highlight = { [weak self] point in
|
||||
if let point = point, point.x > 280.0 {
|
||||
self?.highlighted?(true)
|
||||
} else {
|
||||
self?.highlighted?(false)
|
||||
}
|
||||
}
|
||||
self.view.addGestureRecognizer(recognizer)
|
||||
}
|
||||
|
||||
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
guard let (_, insets) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
switch gesture {
|
||||
case .tap:
|
||||
if location.x < insets.left + minimizedNavigationHeight && location.y < minimizedNavigationHeight {
|
||||
self.closeTapped?()
|
||||
} else {
|
||||
self.tapped?()
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (size, insets)
|
||||
|
||||
var topInset = insets.top
|
||||
if size.width < size.height {
|
||||
topInset += 10.0
|
||||
}
|
||||
self.containerNode.frame = CGRect(origin: .zero, size: size)
|
||||
self.containerNode.subnodeTransform = CATransform3DMakeTranslation(0.0, -topInset, 0.0)
|
||||
|
||||
self.shadowNode.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: size.height - topInset))
|
||||
|
||||
var navigationHeight: CGFloat = minimizedNavigationHeight
|
||||
if !self.isExpanded {
|
||||
navigationHeight += insets.bottom
|
||||
}
|
||||
|
||||
let headerFrame = CGRect(origin: .zero, size: CGSize(width: size.width, height: navigationHeight))
|
||||
self.headerNode.update(size: size, insets: insets, transition: transition)
|
||||
transition.updateFrame(node: self.headerNode, frame: headerFrame)
|
||||
transition.updateFrame(node: self.dimCoverNode, frame: CGRect(origin: .zero, size: size))
|
||||
|
||||
if !self.isDismissed {
|
||||
transition.updateAlpha(node: self.shadowNode, alpha: self.isExpanded ? 1.0 : 0.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private weak var navigationController: NavigationController?
|
||||
private var items: [Item] = []
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
var isExpanded: Bool = false
|
||||
public var willMaximize: (() -> Void)?
|
||||
|
||||
private let bottomEdgeView: UIImageView
|
||||
private let blurView: BlurView
|
||||
private let dimView: UIView
|
||||
private let scrollView: ScrollViewImpl
|
||||
private var itemNodes: [AnyHashable: ItemNode] = [:]
|
||||
|
||||
private var highlightedItemId: AnyHashable?
|
||||
|
||||
private var dismissGestureRecognizer: UIPanGestureRecognizer?
|
||||
private var dismissingItemId: AnyHashable?
|
||||
private var dismissingItemOffset: CGFloat?
|
||||
|
||||
private var currentTransition: Transition?
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
public init(context: AccountContext, navigationController: NavigationController) {
|
||||
self.context = context
|
||||
self.navigationController = navigationController
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.bottomEdgeView = UIImageView()
|
||||
self.bottomEdgeView.contentMode = .scaleToFill
|
||||
self.bottomEdgeView.image = 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)
|
||||
|
||||
self.blurView = BlurView(effect: nil)
|
||||
self.dimView = UIView()
|
||||
self.dimView.alpha = 0.0
|
||||
self.dimView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.6)
|
||||
self.dimView.isUserInteractionEnabled = false
|
||||
|
||||
self.scrollView = ScrollViewImpl()
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.view.addSubview(self.bottomEdgeView)
|
||||
self.view.addSubview(self.blurView)
|
||||
self.view.addSubview(self.dimView)
|
||||
self.view.addSubview(self.scrollView)
|
||||
|
||||
self.presentationDataDisposable = (self.context.sharedContext.presentationData
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.presentationData = presentationData
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.presentationDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
public override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.scrollView.delegate = self.wrappedScrollViewDelegate
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
|
||||
let dismissGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.dismissPan(_:)))
|
||||
dismissGestureRecognizer.delegate = self.wrappedGestureRecognizerDelegate
|
||||
dismissGestureRecognizer.delaysTouchesBegan = true
|
||||
self.scrollView.addGestureRecognizer(dismissGestureRecognizer)
|
||||
self.dismissGestureRecognizer = dismissGestureRecognizer
|
||||
}
|
||||
|
||||
func item(at y: CGFloat) -> Int? {
|
||||
guard let layout = self.validLayout else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let insets = layout.insets(options: [.statusBar])
|
||||
let itemCount = self.items.count
|
||||
let spacing = interitemSpacing(itemCount: itemCount, boundingSize: self.scrollView.bounds.size, insets: insets)
|
||||
return max(0, min(Int(floor((y - additionalInsetTop) / spacing)), itemCount - 1))
|
||||
}
|
||||
|
||||
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else {
|
||||
return false
|
||||
}
|
||||
|
||||
let location = panGesture.location(in: gestureRecognizer.view)
|
||||
let velocity = panGesture.velocity(in: gestureRecognizer.view)
|
||||
|
||||
if abs(velocity.x) > abs(velocity.y), let _ = self.item(at: location.y) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@objc func dismissPan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
let scrollView = self.scrollView
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
let location = gestureRecognizer.location(in: scrollView)
|
||||
guard let item = self.item(at: location.y) else { return }
|
||||
|
||||
self.dismissingItemId = self.items[item].id
|
||||
case .changed:
|
||||
guard let _ = self.dismissingItemId else { return }
|
||||
|
||||
var delta = gestureRecognizer.translation(in: scrollView)
|
||||
delta.y = 0
|
||||
|
||||
if let offset = self.dismissingItemOffset {
|
||||
self.dismissingItemOffset = offset + delta.x
|
||||
} else {
|
||||
self.dismissingItemOffset = delta.x
|
||||
}
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: scrollView)
|
||||
|
||||
self.requestUpdate(transition: .immediate)
|
||||
case .ended:
|
||||
var needsLayout = true
|
||||
if let itemId = self.dismissingItemId {
|
||||
if let offset = self.dismissingItemOffset {
|
||||
if offset < -self.frame.width / 4.0 {
|
||||
self.currentTransition = .dismiss(itemId: itemId)
|
||||
|
||||
self.items.removeAll(where: { $0.id == itemId })
|
||||
if self.items.count == 1 {
|
||||
self.isExpanded = false
|
||||
self.willMaximize?()
|
||||
needsLayout = false
|
||||
}
|
||||
}
|
||||
self.dismissingItemOffset = nil
|
||||
self.dismissingItemId = nil
|
||||
}
|
||||
}
|
||||
if needsLayout {
|
||||
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
case .cancelled, .failed:
|
||||
self.dismissingItemId = nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
if result === self.view {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func addController(_ viewController: ViewController, transition: ContainedViewLayoutTransition) {
|
||||
let item = Item(
|
||||
id: AnyHashable(Int64.random(in: Int64.min ... Int64.max)),
|
||||
controller: viewController
|
||||
)
|
||||
self.items.append(item)
|
||||
|
||||
self.currentTransition = .minimize(itemId: item.id)
|
||||
self.requestUpdate(transition: transition)
|
||||
}
|
||||
|
||||
private enum Transition: Equatable {
|
||||
case minimize(itemId: AnyHashable)
|
||||
case maximize(itemId: AnyHashable)
|
||||
case dismiss(itemId: AnyHashable)
|
||||
case dismissAll
|
||||
|
||||
func matches(item: Item) -> Bool {
|
||||
switch self {
|
||||
case .minimize:
|
||||
return false
|
||||
case let .maximize(itemId), let .dismiss(itemId):
|
||||
return item.id == itemId
|
||||
case .dismissAll:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func maximizeController(_ viewController: ViewController, animated: Bool, completion: @escaping (Bool) -> Void) {
|
||||
guard let item = self.items.first(where: { $0.controller === viewController }) else {
|
||||
completion(self.items.count == 0)
|
||||
return
|
||||
}
|
||||
if !animated {
|
||||
self.items.removeAll(where: { $0.id == item.id })
|
||||
self.itemNodes[item.id]?.removeFromSupernode()
|
||||
self.itemNodes[item.id] = nil
|
||||
completion(self.items.count == 0)
|
||||
self.scrollView.contentOffset = .zero
|
||||
return
|
||||
}
|
||||
self.isExpanded = false
|
||||
self.currentTransition = .maximize(itemId: item.id)
|
||||
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
completion(self.items.count == 0)
|
||||
self.scrollView.contentOffset = .zero
|
||||
})
|
||||
self.items.removeAll(where: { $0.id == item.id })
|
||||
}
|
||||
|
||||
public func dismissAll(completion: @escaping () -> Void) {
|
||||
self.currentTransition = .dismissAll
|
||||
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring), completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard self.isExpanded else {
|
||||
return
|
||||
}
|
||||
self.requestUpdate(transition: .immediate)
|
||||
}
|
||||
|
||||
private func requestUpdate(transition: ContainedViewLayoutTransition, completion: @escaping (Transition) -> Void = { _ in }) {
|
||||
guard let layout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
self.updateLayout(layout, transition: transition, completion: completion)
|
||||
}
|
||||
|
||||
public func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
self.updateLayout(layout, transition: transition, completion: { _ in })
|
||||
}
|
||||
|
||||
private func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, completion: @escaping (Transition) -> Void = { _ in }) {
|
||||
let isFirstTime = self.validLayout == nil
|
||||
var containerTransition = transition
|
||||
if isFirstTime {
|
||||
containerTransition = .immediate
|
||||
}
|
||||
|
||||
self.validLayout = layout
|
||||
|
||||
let bounds = CGRect(origin: .zero, size: layout.size)
|
||||
|
||||
containerTransition.updateFrame(view: self.blurView, frame: bounds)
|
||||
containerTransition.updateFrame(view: self.dimView, frame: bounds)
|
||||
if self.isExpanded {
|
||||
if self.blurView.effect == nil {
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.blurView.effect = UIBlurEffect(style: self.presentationData.theme.overallDarkAppearance ? .dark : .light)
|
||||
self.dimView.alpha = 1.0
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if self.blurView.effect != nil {
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.blurView.effect = nil
|
||||
self.dimView.alpha = 0.0
|
||||
})
|
||||
}
|
||||
}
|
||||
self.blurView.isUserInteractionEnabled = self.isExpanded
|
||||
|
||||
let bottomEdgeHeight = 24.0 + 33.0 + layout.intrinsicInsets.bottom
|
||||
let bottomEdgeOrigin = layout.size.height - bottomEdgeHeight
|
||||
containerTransition.updateFrame(view: self.bottomEdgeView, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomEdgeHeight), size: CGSize(width: layout.size.width, height: bottomEdgeHeight)))
|
||||
|
||||
if isFirstTime {
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
transition.animatePosition(layer: self.bottomEdgeView.layer, from: self.bottomEdgeView.layer.position.offsetBy(dx: 0.0, dy: minimizedNavigationHeight + minimizedTopMargin), to: self.bottomEdgeView.layer.position)
|
||||
}
|
||||
|
||||
let insets = layout.insets(options: [.statusBar])
|
||||
let itemInsets = UIEdgeInsets(top: insets.top, left: layout.safeInsets.left, bottom: insets.bottom, right: layout.safeInsets.right)
|
||||
var topInset = insets.top
|
||||
if layout.size.width < layout.size.height {
|
||||
topInset += 10.0
|
||||
}
|
||||
|
||||
var index = 0
|
||||
let contentHeight = frameForIndex(index: self.items.count - 1, size: layout.size, insets: itemInsets, itemCount: self.items.count, boundingSize: layout.size).midY - 70.0
|
||||
for item in self.items {
|
||||
if let currentTransition = self.currentTransition {
|
||||
if currentTransition.matches(item: item) {
|
||||
continue
|
||||
} else if case .dismiss = currentTransition, self.items.count == 1 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var itemTransition = containerTransition
|
||||
|
||||
let itemNode: ItemNode
|
||||
if let current = self.itemNodes[item.id] {
|
||||
itemNode = current
|
||||
itemNode.theme = self.presentationData.theme
|
||||
} else {
|
||||
itemTransition = .immediate
|
||||
itemNode = ItemNode(theme: self.presentationData.theme, strings: self.presentationData.strings, item: item)
|
||||
self.scrollView.addSubnode(itemNode)
|
||||
self.itemNodes[item.id] = itemNode
|
||||
}
|
||||
itemNode.closeTapped = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.isExpanded {
|
||||
var needsLayout = true
|
||||
self.currentTransition = .dismiss(itemId: item.id)
|
||||
|
||||
self.items.removeAll(where: { $0.id == item.id })
|
||||
if self.items.count == 1 {
|
||||
self.isExpanded = false
|
||||
self.willMaximize?()
|
||||
needsLayout = false
|
||||
}
|
||||
if needsLayout {
|
||||
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
} else {
|
||||
self.navigationController?.dismissMinimizedControllers(animated: true)
|
||||
}
|
||||
}
|
||||
itemNode.tapped = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.isExpanded {
|
||||
self.navigationController?.maximizeViewController(item.controller, animated: true)
|
||||
} else {
|
||||
if self.items.count == 1 {
|
||||
self.navigationController?.maximizeViewController(item.controller, animated: true)
|
||||
} else {
|
||||
self.isExpanded = true
|
||||
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let itemFrame: CGRect
|
||||
let itemTransform: CATransform3D
|
||||
|
||||
if index == self.items.count - 1 {
|
||||
itemNode.layer.zPosition = 10000.0
|
||||
} else {
|
||||
itemNode.layer.zPosition = 0.0
|
||||
}
|
||||
itemNode.isExpanded = self.isExpanded
|
||||
|
||||
if self.isExpanded {
|
||||
let currentItemFrame = frameForIndex(index: index, size: layout.size, insets: itemInsets, itemCount: self.items.count, boundingSize: layout.size)
|
||||
let currentItemTransform = final3dTransform(for: currentItemFrame.minY, size: currentItemFrame.size, contentHeight: contentHeight, itemCount: self.items.count, additionalAngle: self.highlightedItemId == item.id ? 0.04 : nil, scrollBounds: self.scrollView.bounds, insets: itemInsets)
|
||||
|
||||
var effectiveItemFrame = currentItemFrame
|
||||
var effectiveItemTransform = currentItemTransform
|
||||
|
||||
if let dismissingItemId = self.dismissingItemId, let deletingIndex = self.items.firstIndex(where: { $0.id == dismissingItemId }), let offset = self.dismissingItemOffset {
|
||||
var targetItemFrame: CGRect?
|
||||
var targetItemTransform: CATransform3D?
|
||||
if deletingIndex == index {
|
||||
let effectiveOffset: CGFloat
|
||||
if offset <= 0.0 {
|
||||
effectiveOffset = offset
|
||||
} else {
|
||||
effectiveOffset = scrollingRubberBandingOffset(offset: offset, bandingStart: 0.0, range: 20.0)
|
||||
}
|
||||
effectiveItemFrame = effectiveItemFrame.offsetBy(dx: effectiveOffset, dy: 0.0)
|
||||
} else if index < deletingIndex {
|
||||
let frame = frameForIndex(index: index, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size)
|
||||
let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets)
|
||||
|
||||
targetItemFrame = frame
|
||||
targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets)
|
||||
} else {
|
||||
let frame = frameForIndex(index: index - 1, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size)
|
||||
let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets)
|
||||
|
||||
targetItemFrame = frame
|
||||
targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets)
|
||||
}
|
||||
|
||||
if let targetItemFrame, let targetItemTransform {
|
||||
let fraction = max(0.0, min(1.0, -1.0 * offset / (layout.size.width * 1.5)))
|
||||
effectiveItemFrame = effectiveItemFrame.interpolate(with: targetItemFrame, fraction: fraction)
|
||||
effectiveItemTransform = effectiveItemTransform.interpolate(with: targetItemTransform, fraction: fraction)
|
||||
}
|
||||
}
|
||||
itemFrame = effectiveItemFrame
|
||||
itemTransform = effectiveItemTransform
|
||||
|
||||
itemNode.isCovered = false
|
||||
} else {
|
||||
var itemOffset: CGFloat = bottomEdgeOrigin + 13.0
|
||||
var hideTransform = false
|
||||
if let currentTransition = self.currentTransition {
|
||||
if case let .maximize(itemId) = currentTransition {
|
||||
itemOffset += layout.size.height * 0.25
|
||||
if let lastItemNode = self.scrollView.subviews.last?.asyncdisplaykit_node as? ItemNode, lastItemNode.item.id == itemId {
|
||||
hideTransform = true
|
||||
}
|
||||
} else if case .dismiss = currentTransition, self.items.count == 1 {
|
||||
itemOffset += layout.size.height * 0.25
|
||||
}
|
||||
}
|
||||
|
||||
var effectiveItemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemOffset), size: layout.size)
|
||||
var effectiveItemTransform = itemNode.transform
|
||||
if hideTransform {
|
||||
effectiveItemTransform = CATransform3DMakeScale(0.7, 0.7, 1.0)
|
||||
} else if index == self.items.count - 1 {
|
||||
if self.items.count > 1 {
|
||||
effectiveItemFrame = effectiveItemFrame.offsetBy(dx: 0.0, dy: 4.0)
|
||||
}
|
||||
effectiveItemTransform = CATransform3DIdentity
|
||||
} else {
|
||||
let sideInset: CGFloat = 10.0
|
||||
let scaledWidth = layout.size.width - sideInset * 2.0
|
||||
let scale = scaledWidth / layout.size.width
|
||||
let scaledHeight = layout.size.height * scale
|
||||
let verticalOffset = layout.size.height - scaledHeight
|
||||
effectiveItemFrame = effectiveItemFrame.offsetBy(dx: 0.0, dy: -verticalOffset / 2.0)
|
||||
effectiveItemTransform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
}
|
||||
itemFrame = effectiveItemFrame
|
||||
itemTransform = effectiveItemTransform
|
||||
|
||||
itemNode.isCovered = index == self.items.count - 2
|
||||
}
|
||||
|
||||
itemNode.bounds = CGRect(origin: .zero, size: itemFrame.size)
|
||||
itemNode.updateLayout(size: layout.size, insets: itemInsets, transition: itemTransition)
|
||||
|
||||
if index == self.items.count - 1 && !self.isExpanded {
|
||||
itemNode.setTitleControllers(self.items.map { $0.controller })
|
||||
} else {
|
||||
itemNode.setTitleControllers(nil)
|
||||
}
|
||||
|
||||
itemTransition.updateTransform(node: itemNode, transform: itemTransform)
|
||||
itemTransition.updatePosition(node: itemNode, position: itemFrame.center)
|
||||
|
||||
index += 1
|
||||
}
|
||||
|
||||
let contentSize = CGSize(width: layout.size.width, height: contentHeight)
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
if self.scrollView.frame != bounds {
|
||||
self.scrollView.frame = bounds
|
||||
}
|
||||
self.scrollView.passthrough = !self.isExpanded
|
||||
self.scrollView.isScrollEnabled = self.isExpanded
|
||||
|
||||
if let currentTransition = self.currentTransition {
|
||||
switch self.currentTransition {
|
||||
case let .minimize(itemId):
|
||||
guard let itemNode = self.itemNodes[itemId] else {
|
||||
return
|
||||
}
|
||||
|
||||
let dimView = UIView()
|
||||
dimView.alpha = 1.0
|
||||
dimView.frame = CGRect(origin: .zero, size: layout.size)
|
||||
dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
||||
self.view.insertSubview(dimView, aboveSubview: self.blurView)
|
||||
dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
dimView.removeFromSuperview()
|
||||
})
|
||||
|
||||
|
||||
itemNode.animateIn()
|
||||
|
||||
var initialOffset = insets.top + itemNode.item.controller.minimizedTopEdgeOffset
|
||||
if layout.size.width < layout.size.height {
|
||||
initialOffset += 10.0
|
||||
}
|
||||
if let minimizedBounds = itemNode.item.controller.minimizedBounds {
|
||||
initialOffset += -minimizedBounds.minY
|
||||
}
|
||||
|
||||
transition.animatePosition(node: itemNode, from: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + initialOffset), completion: { _ in
|
||||
if self.currentTransition == currentTransition {
|
||||
self.currentTransition = nil
|
||||
}
|
||||
completion(currentTransition)
|
||||
})
|
||||
case let .maximize(itemId):
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
guard let itemNode = self.itemNodes[itemId] else {
|
||||
return
|
||||
}
|
||||
|
||||
let dimView = UIView()
|
||||
dimView.frame = CGRect(origin: .zero, size: layout.size)
|
||||
dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
||||
self.view.insertSubview(dimView, aboveSubview: self.blurView)
|
||||
dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
|
||||
itemNode.animateOut()
|
||||
transition.updateTransform(node: itemNode, transform: CATransform3DIdentity)
|
||||
transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in
|
||||
if self.currentTransition == currentTransition {
|
||||
self.currentTransition = nil
|
||||
}
|
||||
completion(currentTransition)
|
||||
self.itemNodes[itemId] = nil
|
||||
itemNode.removeFromSupernode()
|
||||
dimView.removeFromSuperview()
|
||||
|
||||
self.requestUpdate(transition: .immediate)
|
||||
})
|
||||
case let .dismiss(itemId):
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
guard let dismissedItemNode = self.itemNodes[itemId] else {
|
||||
return
|
||||
}
|
||||
if self.items.count == 1 {
|
||||
if let itemNode = self.itemNodes.first(where: { $0.0 != itemId })?.value {
|
||||
let dimView = UIView()
|
||||
dimView.frame = CGRect(origin: .zero, size: layout.size)
|
||||
dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
||||
self.view.insertSubview(dimView, aboveSubview: self.blurView)
|
||||
dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
|
||||
itemNode.animateOut()
|
||||
transition.updateTransform(node: itemNode, transform: CATransform3DIdentity)
|
||||
transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in
|
||||
if self.currentTransition == currentTransition {
|
||||
self.currentTransition = nil
|
||||
}
|
||||
completion(currentTransition)
|
||||
self.itemNodes[itemId] = nil
|
||||
itemNode.removeFromSupernode()
|
||||
dimView.removeFromSuperview()
|
||||
|
||||
self.navigationController?.maximizeViewController(itemNode.item.controller, animated: false)
|
||||
|
||||
self.requestUpdate(transition: .immediate)
|
||||
})
|
||||
}
|
||||
transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y))
|
||||
} else {
|
||||
transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y), completion: { _ in
|
||||
if self.currentTransition == currentTransition {
|
||||
self.currentTransition = nil
|
||||
}
|
||||
completion(currentTransition)
|
||||
|
||||
self.itemNodes[itemId] = nil
|
||||
dismissedItemNode.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
case .dismissAll:
|
||||
let dismissOffset = collapsedHeight(layout: layout)
|
||||
transition.updatePosition(layer: self.bottomEdgeView.layer, position: self.bottomEdgeView.layer.position.offsetBy(dx: 0.0, dy: dismissOffset), completion: { _ in
|
||||
if self.currentTransition == currentTransition {
|
||||
self.currentTransition = nil
|
||||
}
|
||||
completion(currentTransition)
|
||||
})
|
||||
transition.updatePosition(layer: self.scrollView.layer, position: self.scrollView.center.offsetBy(dx: 0.0, dy: dismissOffset))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func collapsedHeight(layout: ContainerViewLayout) -> CGFloat {
|
||||
return minimizedNavigationHeight + minimizedTopMargin + layout.intrinsicInsets.bottom
|
||||
}
|
||||
}
|
||||
|
||||
private let maxInteritemSpacing: CGFloat = 240.0
|
||||
private let additionalInsetTop: CGFloat = 16.0
|
||||
private let additionalInsetBottom: CGFloat = 0.0
|
||||
private let zOffset: CGFloat = -60.0
|
||||
|
||||
private let perspectiveCorrection: CGFloat = -1.0 / 1000.0
|
||||
private let maxRotationAngle: CGFloat = -CGFloat.pi / 2.2
|
||||
|
||||
private func angle(for origin: CGFloat, itemCount: Int, scrollBounds: CGRect, contentHeight: CGFloat?, insets: UIEdgeInsets) -> CGFloat {
|
||||
var rotationAngle = rotationAngleAt0(itemCount: itemCount)
|
||||
|
||||
var contentOffset = scrollBounds.origin.y
|
||||
if contentOffset < 0.0 {
|
||||
contentOffset *= 2.0
|
||||
}
|
||||
|
||||
var yOnScreen = origin - contentOffset - additionalInsetTop - insets.top
|
||||
if yOnScreen < 0 {
|
||||
yOnScreen = 0
|
||||
} else if yOnScreen > scrollBounds.height {
|
||||
yOnScreen = scrollBounds.height
|
||||
}
|
||||
|
||||
let maxRotationVariance = maxRotationAngle - rotationAngleAt0(itemCount: itemCount)
|
||||
rotationAngle += (maxRotationVariance / scrollBounds.height) * yOnScreen
|
||||
|
||||
return rotationAngle
|
||||
}
|
||||
|
||||
private func final3dTransform(for origin: CGFloat, size: CGSize, contentHeight: CGFloat?, itemCount: Int, forcedAngle: CGFloat? = nil, additionalAngle: CGFloat? = nil, scrollBounds: CGRect, insets: UIEdgeInsets) -> CATransform3D {
|
||||
var transform = CATransform3DIdentity
|
||||
transform.m34 = perspectiveCorrection
|
||||
|
||||
let rotationAngle = forcedAngle ?? angle(for: origin, itemCount: itemCount, scrollBounds: scrollBounds, contentHeight: contentHeight, insets: insets)
|
||||
var effectiveRotationAngle = rotationAngle
|
||||
if let additionalAngle = additionalAngle {
|
||||
effectiveRotationAngle += additionalAngle
|
||||
}
|
||||
|
||||
let r = size.height / 2.0 + abs(zOffset / sin(rotationAngle))
|
||||
|
||||
let zTranslation = r * sin(rotationAngle)
|
||||
let yTranslation: CGFloat = r * (1 - cos(rotationAngle))
|
||||
|
||||
let zTranslateTransform = CATransform3DTranslate(transform, 0.0, -yTranslation, zTranslation)
|
||||
|
||||
let rotateTransform = CATransform3DRotate(zTranslateTransform, effectiveRotationAngle, 1.0, 0.0, 0.0)
|
||||
|
||||
return rotateTransform
|
||||
}
|
||||
|
||||
private func interitemSpacing(itemCount: Int, boundingSize: CGSize, insets: UIEdgeInsets) -> CGFloat {
|
||||
var interitemSpacing = maxInteritemSpacing
|
||||
if itemCount > 0 {
|
||||
interitemSpacing = (boundingSize.height - additionalInsetTop - additionalInsetBottom - insets.top) / CGFloat(min(itemCount, 5))
|
||||
}
|
||||
return interitemSpacing
|
||||
}
|
||||
|
||||
private func frameForIndex(index: Int, size: CGSize, insets: UIEdgeInsets, itemCount: Int, boundingSize: CGSize) -> CGRect {
|
||||
let spacing = interitemSpacing(itemCount: itemCount, boundingSize: boundingSize, insets: insets)
|
||||
let y = additionalInsetTop + insets.top + spacing * CGFloat(index)
|
||||
let origin = CGPoint(x: insets.left, y: y)
|
||||
|
||||
return CGRect(origin: origin, size: CGSize(width: size.width - insets.left - insets.right, height: size.height))
|
||||
}
|
||||
|
||||
private func rotationAngleAt0(itemCount: Int) -> CGFloat {
|
||||
let multiplier: CGFloat = min(CGFloat(itemCount), 5.0) - 1.0
|
||||
return -CGFloat.pi / 7.0 - CGFloat.pi / 7.0 * multiplier / 4.0
|
||||
}
|
||||
|
||||
private class BlurView: UIVisualEffectView {
|
||||
private func setup() {
|
||||
for subview in self.subviews {
|
||||
if subview.description.contains("VisualEffectSubview") {
|
||||
subview.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
if let sublayer = self.layer.sublayers?[0], let filters = sublayer.filters {
|
||||
sublayer.backgroundColor = nil
|
||||
sublayer.isOpaque = false
|
||||
let allowedKeys: [String] = [
|
||||
"gaussianBlur",
|
||||
"colorSaturate"
|
||||
]
|
||||
sublayer.filters = filters.filter { filter in
|
||||
guard let filter = filter as? NSObject else {
|
||||
return true
|
||||
}
|
||||
let filterName = String(describing: filter)
|
||||
if !allowedKeys.contains(filterName) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var effect: UIVisualEffect? {
|
||||
get {
|
||||
return super.effect
|
||||
}
|
||||
set {
|
||||
super.effect = newValue
|
||||
self.setup()
|
||||
}
|
||||
}
|
||||
|
||||
override func didAddSubview(_ subview: UIView) {
|
||||
super.didAddSubview(subview)
|
||||
self.setup()
|
||||
}
|
||||
}
|
||||
|
||||
private let shadowImage: UIImage? = {
|
||||
return generateImage(CGSize(width: 1.0, height: 480.0), rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor] as CFArray
|
||||
|
||||
var locations: [CGFloat] = [0.0, 0.65, 1.0]
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: bounds.height), options: [])
|
||||
})
|
||||
}()
|
@ -0,0 +1,52 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension CATransform3D {
|
||||
func interpolate(with other: CATransform3D, fraction: CGFloat) -> CATransform3D {
|
||||
var vectors = Array<CGFloat>(repeating: 0.0, count: 16)
|
||||
vectors[0] = self.m11 + (other.m11 - self.m11) * fraction
|
||||
vectors[1] = self.m12 + (other.m12 - self.m12) * fraction
|
||||
vectors[2] = self.m13 + (other.m13 - self.m13) * fraction
|
||||
vectors[3] = self.m14 + (other.m14 - self.m14) * fraction
|
||||
vectors[4] = self.m21 + (other.m21 - self.m21) * fraction
|
||||
vectors[5] = self.m22 + (other.m22 - self.m22) * fraction
|
||||
vectors[6] = self.m23 + (other.m23 - self.m23) * fraction
|
||||
vectors[7] = self.m24 + (other.m24 - self.m24) * fraction
|
||||
vectors[8] = self.m31 + (other.m31 - self.m31) * fraction
|
||||
vectors[9] = self.m32 + (other.m32 - self.m32) * fraction
|
||||
vectors[10] = self.m33 + (other.m33 - self.m33) * fraction
|
||||
vectors[11] = self.m34 + (other.m34 - self.m34) * fraction
|
||||
vectors[12] = self.m41 + (other.m41 - self.m41) * fraction
|
||||
vectors[13] = self.m42 + (other.m42 - self.m42) * fraction
|
||||
vectors[14] = self.m43 + (other.m43 - self.m43) * fraction
|
||||
vectors[15] = self.m44 + (other.m44 - self.m44) * fraction
|
||||
|
||||
return CATransform3D(m11: vectors[0], m12: vectors[1], m13: vectors[2], m14: vectors[3], m21: vectors[4], m22: vectors[5], m23: vectors[6], m24: vectors[7], m31: vectors[8], m32: vectors[9], m33: vectors[10], m34: vectors[11], m41: vectors[12], m42: vectors[13], m43: vectors[14], m44: vectors[15])
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGFloat {
|
||||
func interpolate(with other: CGFloat, fraction: CGFloat) -> CGFloat {
|
||||
let invT = 1.0 - fraction
|
||||
let result = other * fraction + self * invT
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGPoint {
|
||||
func interpolate(with other: CGPoint, fraction: CGFloat) -> CGPoint {
|
||||
return CGPoint(x: self.x.interpolate(with: other.x, fraction: fraction), y: self.y.interpolate(with: other.y, fraction: fraction))
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGSize {
|
||||
func interpolate(with other: CGSize, fraction: CGFloat) -> CGSize {
|
||||
return CGSize(width: self.width.interpolate(with: other.width, fraction: fraction), height: self.height.interpolate(with: other.height, fraction: fraction))
|
||||
}
|
||||
}
|
||||
|
||||
extension CGRect {
|
||||
func interpolate(with other: CGRect, fraction: CGFloat) -> CGRect {
|
||||
return CGRect(origin: self.origin.interpolate(with: other.origin, fraction: fraction), size: self.size.interpolate(with: other.size, fraction: fraction))
|
||||
}
|
||||
}
|
@ -229,7 +229,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
|
||||
|
||||
let textString: String
|
||||
if let _ = context.component.requiredStars {
|
||||
textString = strings.Stars_Purchase_StarsNeededInfo(state.peer?.compactDisplayTitle ?? "").string
|
||||
textString = state.peer == nil ? strings.Stars_Purchase_StarsNeededUnlockInfo : strings.Stars_Purchase_StarsNeededInfo(state.peer?.compactDisplayTitle ?? "").string
|
||||
} else {
|
||||
textString = strings.Stars_Purchase_GetStarsInfo
|
||||
}
|
||||
@ -310,11 +310,11 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
|
||||
minimumCount = requiredStars - balance
|
||||
}
|
||||
for product in products {
|
||||
if let minimumCount, minimumCount > product.option.count {
|
||||
if let minimumCount, minimumCount > product.option.count && !(items.isEmpty && product.id == products.last?.id) {
|
||||
continue
|
||||
}
|
||||
|
||||
if let _ = minimumCount, items.isEmpty {
|
||||
if let _ = minimumCount, items.isEmpty {
|
||||
|
||||
} else if !context.component.expanded && !initialValues.contains(product.option.count) {
|
||||
continue
|
||||
@ -381,7 +381,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
if !context.component.expanded {
|
||||
if !context.component.expanded && items.count > 1 {
|
||||
let titleComponent = AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: strings.Stars_Purchase_ShowMore,
|
||||
|
@ -213,7 +213,16 @@ private final class SheetContent: CombinedComponent {
|
||||
}
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { _ in
|
||||
action()
|
||||
Queue.mainQueue().after(0.1, { [weak self] in
|
||||
if let self, let balance = self.balance, balance < self.invoice.totalAmount {
|
||||
self.inProgress = false
|
||||
self.updated()
|
||||
|
||||
self.buy(requestTopUp: requestTopUp, completion: completion)
|
||||
} else {
|
||||
action()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -437,7 +446,8 @@ private final class SheetContent: CombinedComponent {
|
||||
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, theme)
|
||||
}
|
||||
|
||||
let buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amount)", font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center)
|
||||
let amountString = presentationStringsFormattedNumber(Int32(amount), presentationData.dateTimeFormat.groupingSeparator)
|
||||
let buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amountString)", font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center)
|
||||
if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 {
|
||||
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string))
|
||||
@ -450,6 +460,7 @@ private final class SheetContent: CombinedComponent {
|
||||
let starsContext = component.starsContext
|
||||
let botTitle = state.botPeer?.compactDisplayTitle ?? ""
|
||||
let invoice = component.invoice
|
||||
let isMedia = !component.extendedMedia.isEmpty
|
||||
let button = button.update(
|
||||
component: ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
@ -472,7 +483,7 @@ private final class SheetContent: CombinedComponent {
|
||||
context: accountContext,
|
||||
starsContext: starsContext,
|
||||
options: state?.options ?? [],
|
||||
peerId: state?.botPeer?.id,
|
||||
peerId: isMedia ? nil : state?.botPeer?.id,
|
||||
requiredStars: invoice.totalAmount,
|
||||
completion: { [weak starsContext] stars in
|
||||
starsContext?.add(balance: stars)
|
||||
|
@ -3752,6 +3752,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return state.updatedShowWebView(true).updatedForceInputCommandsHidden(true)
|
||||
}
|
||||
|
||||
let context = strongSelf.context
|
||||
let params = WebAppParameters(source: .menu, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false)
|
||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
||||
@ -3798,7 +3799,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}
|
||||
}, getNavigationController: { [weak self] in
|
||||
return self?.effectiveNavigationController
|
||||
return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||
})
|
||||
controller.navigationPresentation = .flatModal
|
||||
strongSelf.push(controller)
|
||||
@ -3823,6 +3824,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let context = strongSelf.context
|
||||
let params = WebAppParameters(source: isInline ? .inline : .simple, peerId: peerId, botId: botId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false)
|
||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
||||
@ -3843,7 +3845,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}
|
||||
}, getNavigationController: { [weak self] in
|
||||
return self?.effectiveNavigationController
|
||||
return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||
})
|
||||
controller.navigationPresentation = .flatModal
|
||||
strongSelf.currentWebAppController = controller
|
||||
@ -3863,13 +3865,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let context = strongSelf.context
|
||||
let params = WebAppParameters(source: .generic, peerId: peerId, botId: peerId, botName: botName, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false)
|
||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
||||
}, completion: { [weak self] in
|
||||
self?.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
}, getNavigationController: { [weak self] in
|
||||
return self?.effectiveNavigationController
|
||||
return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||
})
|
||||
controller.navigationPresentation = .flatModal
|
||||
strongSelf.currentWebAppController = controller
|
||||
@ -8188,6 +8191,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let context = strongSelf.context
|
||||
let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, url: url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings))
|
||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
||||
@ -8210,7 +8214,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}, completion: { [weak self] in
|
||||
self?.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
}, getNavigationController: { [weak self] in
|
||||
return self?.effectiveNavigationController
|
||||
return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||
})
|
||||
controller.navigationPresentation = .flatModal
|
||||
strongSelf.currentWebAppController = controller
|
||||
|
@ -2196,7 +2196,11 @@ public func standaloneWebAppController(
|
||||
controller.getSourceRect = getSourceRect
|
||||
controller.title = params.botName
|
||||
controller.shouldMinimizeOnSwipe = {
|
||||
return false
|
||||
if params.source != .menu {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user