mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
Initial implementation of attachment menu
This commit is contained in:
464
submodules/AttachmentUI/Sources/AttachmentContainer.swift
Normal file
464
submodules/AttachmentUI/Sources/AttachmentContainer.swift
Normal file
@@ -0,0 +1,464 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import UIKitRuntimeUtils
|
||||
import Display
|
||||
import DirectionalPanGesture
|
||||
|
||||
final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
let scrollNode: ASDisplayNode
|
||||
let container: NavigationContainer
|
||||
|
||||
private(set) var isReady: Bool = false
|
||||
private(set) var dismissProgress: CGFloat = 0.0
|
||||
var isReadyUpdated: (() -> Void)?
|
||||
var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||
var interactivelyDismissed: (() -> Void)?
|
||||
|
||||
var updateModalProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
private var isUpdatingState = false
|
||||
private var isDismissed = false
|
||||
private var isInteractiveDimissEnabled = true
|
||||
|
||||
private var validLayout: (layout: ContainerViewLayout, controllers: [ViewController], coveredByModalTransition: CGFloat)?
|
||||
|
||||
var keyboardViewManager: KeyboardViewManager? {
|
||||
didSet {
|
||||
if self.keyboardViewManager !== oldValue {
|
||||
self.container.keyboardViewManager = self.keyboardViewManager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var canHaveKeyboardFocus: Bool = false {
|
||||
didSet {
|
||||
self.container.canHaveKeyboardFocus = self.canHaveKeyboardFocus
|
||||
}
|
||||
}
|
||||
|
||||
private var panGestureRecognizer: UIPanGestureRecognizer?
|
||||
|
||||
init(controllerRemoved: @escaping (ViewController) -> Void) {
|
||||
self.scrollNode = ASDisplayNode()
|
||||
|
||||
self.container = NavigationContainer(controllerRemoved: controllerRemoved)
|
||||
self.container.clipsToBounds = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.scrollNode)
|
||||
self.scrollNode.addSubnode(self.container)
|
||||
|
||||
self.isReady = self.container.isReady
|
||||
self.container.isReadyUpdated = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !strongSelf.isReady {
|
||||
strongSelf.isReady = true
|
||||
if !strongSelf.isUpdatingState {
|
||||
strongSelf.isReadyUpdated?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applySmoothRoundedCorners(self.container.layer)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let panRecognizer = DirectionalPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
||||
panRecognizer.delegate = self
|
||||
panRecognizer.delaysTouchesBegan = false
|
||||
panRecognizer.cancelsTouchesInView = true
|
||||
self.panGestureRecognizer = panRecognizer
|
||||
self.scrollNode.view.addGestureRecognizer(panRecognizer)
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)?
|
||||
|
||||
let defaultTopInset: CGFloat = 210.0
|
||||
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
guard let (layout, controllers, coveredByModalTransition) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let isLandscape = layout.orientation == .landscape
|
||||
let edgeTopInset = isLandscape ? 0.0 : defaultTopInset
|
||||
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
let topInset: CGFloat
|
||||
if self.isExpanded {
|
||||
topInset = 0.0
|
||||
} else {
|
||||
topInset = edgeTopInset
|
||||
}
|
||||
|
||||
let point = recognizer.location(in: self.view)
|
||||
let currentHitView = self.hitTest(point, with: nil)
|
||||
let scrollViewAndListNode = self.findScrollView(view: currentHitView)
|
||||
let scrollView = scrollViewAndListNode?.0
|
||||
let listNode = scrollViewAndListNode?.1
|
||||
|
||||
self.panGestureArguments = (topInset, 0.0, scrollView, listNode)
|
||||
case .changed:
|
||||
guard let (topInset, panOffset, scrollView, listNode) = self.panGestureArguments else {
|
||||
return
|
||||
}
|
||||
let visibleContentOffset = listNode?.visibleContentOffset()
|
||||
let contentOffset = scrollView?.contentOffset.y ?? 0.0
|
||||
|
||||
var translation = recognizer.translation(in: self.view).y
|
||||
|
||||
if case let .known(value) = visibleContentOffset, value <= 0.5 {
|
||||
} else if contentOffset <= 0.5 {
|
||||
} else {
|
||||
translation = panOffset
|
||||
if self.isExpanded {
|
||||
recognizer.setTranslation(CGPoint(), in: self.view)
|
||||
}
|
||||
}
|
||||
|
||||
self.panGestureArguments = (topInset, translation, scrollView, listNode)
|
||||
|
||||
let currentOffset = topInset + translation
|
||||
if !self.isExpanded {
|
||||
if currentOffset > 0.0, let scrollView = scrollView {
|
||||
scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView)
|
||||
}
|
||||
|
||||
var bounds = self.bounds
|
||||
bounds.origin.y = -translation
|
||||
bounds.origin.y = min(0.0, bounds.origin.y)
|
||||
self.bounds = bounds
|
||||
}
|
||||
|
||||
self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .immediate)
|
||||
case .ended:
|
||||
guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else {
|
||||
return
|
||||
}
|
||||
let visibleContentOffset = listNode?.visibleContentOffset()
|
||||
let contentOffset = scrollView?.contentOffset.y ?? 0.0
|
||||
|
||||
let translation = recognizer.translation(in: self.view).y
|
||||
var velocity = recognizer.velocity(in: self.view)
|
||||
|
||||
if case let .known(value) = visibleContentOffset, value > 0.0 {
|
||||
velocity = CGPoint()
|
||||
} else if case .unknown = visibleContentOffset {
|
||||
velocity = CGPoint()
|
||||
} else if contentOffset > 0.0 {
|
||||
velocity = CGPoint()
|
||||
}
|
||||
|
||||
var bounds = self.bounds
|
||||
bounds.origin.y = -translation
|
||||
bounds.origin.y = min(0.0, bounds.origin.y)
|
||||
|
||||
let offset = currentTopInset + panOffset
|
||||
let topInset: CGFloat = edgeTopInset
|
||||
if self.isExpanded {
|
||||
self.panGestureArguments = nil
|
||||
if velocity.y > 300.0 || offset > topInset / 2.0 {
|
||||
self.isExpanded = false
|
||||
if let listNode = listNode {
|
||||
listNode.scroller.setContentOffset(CGPoint(), animated: false)
|
||||
} else if let scrollView = scrollView {
|
||||
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
|
||||
}
|
||||
|
||||
let distance = topInset - offset
|
||||
let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance)
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
|
||||
self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: transition)
|
||||
} else {
|
||||
self.isExpanded = true
|
||||
|
||||
self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
} else {
|
||||
self.panGestureArguments = nil
|
||||
|
||||
var dismissing = false
|
||||
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) {
|
||||
self.interactivelyDismissed?()
|
||||
dismissing = true
|
||||
} else if (velocity.y < -300.0 || offset < topInset / 2.0) {
|
||||
if velocity.y > -2200.0, let listNode = listNode {
|
||||
DispatchQueue.main.async {
|
||||
listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
}
|
||||
}
|
||||
|
||||
let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset)
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
|
||||
self.isExpanded = true
|
||||
|
||||
self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: transition)
|
||||
} else {
|
||||
if let listNode = listNode {
|
||||
listNode.scroller.setContentOffset(CGPoint(), animated: false)
|
||||
} else if let scrollView = scrollView {
|
||||
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
|
||||
}
|
||||
|
||||
self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
|
||||
if !dismissing {
|
||||
var bounds = self.bounds
|
||||
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)
|
||||
}
|
||||
}
|
||||
case .cancelled:
|
||||
self.panGestureArguments = nil
|
||||
|
||||
self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func checkInteractiveDismissWithControllers() -> Bool {
|
||||
if let controller = self.container.controllers.last {
|
||||
if !controller.attemptNavigation({
|
||||
}) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var isExpanded = false
|
||||
func update(layout: ContainerViewLayout, controllers: [ViewController], coveredByModalTransition: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
if self.isDismissed {
|
||||
return
|
||||
}
|
||||
self.isUpdatingState = true
|
||||
|
||||
self.validLayout = (layout, controllers, coveredByModalTransition)
|
||||
|
||||
self.panGestureRecognizer?.isEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0)
|
||||
// self.scrollNode.view.isScrollEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0) && self.isInteractiveDimissEnabled
|
||||
|
||||
let isLandscape = layout.orientation == .landscape
|
||||
let edgeTopInset = isLandscape ? 0.0 : defaultTopInset
|
||||
|
||||
let topInset: CGFloat
|
||||
if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments {
|
||||
if self.isExpanded {
|
||||
topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset))
|
||||
} else {
|
||||
topInset = max(0.0, panInitialTopInset + min(0.0, panOffset))
|
||||
}
|
||||
} else {
|
||||
topInset = self.isExpanded ? 0.0 : edgeTopInset
|
||||
}
|
||||
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size))
|
||||
|
||||
let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / defaultTopInset)
|
||||
self.updateModalProgress?(modalProgress, transition)
|
||||
|
||||
let containerLayout: ContainerViewLayout
|
||||
let containerFrame: CGRect
|
||||
let containerScale: CGFloat
|
||||
if layout.metrics.widthClass == .compact {
|
||||
self.container.clipsToBounds = true
|
||||
|
||||
if isLandscape {
|
||||
self.container.cornerRadius = 0.0
|
||||
} else {
|
||||
self.container.cornerRadius = 10.0
|
||||
}
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
if layout.safeInsets.bottom.isZero {
|
||||
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
} else {
|
||||
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
}
|
||||
}
|
||||
|
||||
var containerTopInset: CGFloat
|
||||
if isLandscape {
|
||||
containerTopInset = 0.0
|
||||
containerLayout = layout
|
||||
|
||||
let unscaledFrame = CGRect(origin: CGPoint(), size: containerLayout.size)
|
||||
containerScale = 1.0
|
||||
containerFrame = unscaledFrame
|
||||
} else {
|
||||
containerTopInset = 10.0
|
||||
if let statusBarHeight = layout.statusBarHeight {
|
||||
containerTopInset += statusBarHeight
|
||||
}
|
||||
|
||||
let effectiveStatusBarHeight: CGFloat? = nil
|
||||
|
||||
containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset), 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: containerTopInset - coveredByModalTransition * 10.0), size: containerLayout.size)
|
||||
let maxScale: CGFloat = (containerLayout.size.width - 16.0 * 2.0) / containerLayout.size.width
|
||||
containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition
|
||||
let maxScaledTopInset: CGFloat = containerTopInset - 10.0
|
||||
let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition
|
||||
containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
|
||||
}
|
||||
} else {
|
||||
self.container.clipsToBounds = true
|
||||
self.container.cornerRadius = 10.0
|
||||
if #available(iOS 11.0, *) {
|
||||
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
}
|
||||
|
||||
let verticalInset: CGFloat = 44.0
|
||||
|
||||
let maxSide = max(layout.size.width, layout.size.height)
|
||||
let minSide = min(layout.size.width, layout.size.height)
|
||||
let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0)
|
||||
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize)
|
||||
containerScale = 1.0
|
||||
|
||||
var inputHeight: CGFloat?
|
||||
if let inputHeightValue = layout.inputHeight {
|
||||
inputHeight = max(0.0, inputHeightValue - (layout.size.height - containerFrame.maxY))
|
||||
}
|
||||
|
||||
let effectiveStatusBarHeight: CGFloat? = nil
|
||||
|
||||
containerLayout = ContainerViewLayout(size: containerSize, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: effectiveStatusBarHeight, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
|
||||
}
|
||||
transition.updateFrameAsPositionAndBounds(node: self.container, frame: containerFrame)
|
||||
transition.updateTransformScale(node: self.container, scale: containerScale)
|
||||
self.container.update(layout: containerLayout, canBeClosed: true, controllers: controllers, transition: transition)
|
||||
|
||||
self.isUpdatingState = false
|
||||
}
|
||||
|
||||
func dismiss(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) -> ContainedViewLayoutTransition {
|
||||
for controller in self.container.controllers {
|
||||
controller.viewWillDisappear(transition.isAnimated)
|
||||
}
|
||||
|
||||
if let firstController = self.container.controllers.first, case .standaloneModal = firstController.navigationPresentation {
|
||||
for controller in self.container.controllers {
|
||||
controller.setIgnoreAppearanceMethodInvocations(true)
|
||||
controller.displayNode.removeFromSupernode()
|
||||
controller.setIgnoreAppearanceMethodInvocations(false)
|
||||
controller.viewDidDisappear(transition.isAnimated)
|
||||
}
|
||||
completion()
|
||||
return transition
|
||||
} else {
|
||||
if transition.isAnimated {
|
||||
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
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
|
||||
}
|
||||
for controller in strongSelf.container.controllers {
|
||||
controller.viewDidDisappear(transition.isAnimated)
|
||||
}
|
||||
completion()
|
||||
})
|
||||
return positionTransition
|
||||
} else {
|
||||
for controller in self.container.controllers {
|
||||
controller.setIgnoreAppearanceMethodInvocations(true)
|
||||
controller.displayNode.removeFromSupernode()
|
||||
controller.setIgnoreAppearanceMethodInvocations(false)
|
||||
controller.viewDidDisappear(transition.isAnimated)
|
||||
}
|
||||
completion()
|
||||
return transition
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func findScrollView(view: UIView?) -> (UIScrollView, ListView?)? {
|
||||
if let view = view {
|
||||
if let view = view as? UIScrollView {
|
||||
return (view, nil)
|
||||
}
|
||||
if let node = view.asyncdisplaykit_node as? ListView {
|
||||
return (node.scroller, node)
|
||||
}
|
||||
return findScrollView(view: view.superview)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
if !self.scrollNode.frame.contains(point) {
|
||||
return false
|
||||
}
|
||||
return super.point(inside: point, with: event)
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
guard let result = super.hitTest(point, with: event) else {
|
||||
return nil
|
||||
}
|
||||
// var currentParent: UIView? = result
|
||||
// var enableScrolling = true
|
||||
// while true {
|
||||
// if currentParent == nil {
|
||||
// break
|
||||
// }
|
||||
// if currentParent is UIKeyInput {
|
||||
// if currentParent?.disablesInteractiveModalDismiss == true {
|
||||
// enableScrolling = false
|
||||
// break
|
||||
// }
|
||||
// } else if let scrollView = currentParent as? UIScrollView {
|
||||
// if scrollView === self.scrollNode.view {
|
||||
// break
|
||||
// }
|
||||
// if scrollView.disablesInteractiveModalDismiss {
|
||||
// enableScrolling = false
|
||||
// break
|
||||
// } else {
|
||||
// if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top {
|
||||
// return self.scrollNode.view
|
||||
// }
|
||||
// }
|
||||
// } else if let listView = currentParent as? ListViewBackingView, let listNode = listView.target {
|
||||
// if listNode.view.disablesInteractiveModalDismiss {
|
||||
// enableScrolling = false
|
||||
// break
|
||||
// } else if listNode.scroller.isDecelerating && listNode.scroller.contentOffset.y < listNode.scroller.contentInset.top {
|
||||
// return self.scrollNode.view
|
||||
// }
|
||||
// }
|
||||
// currentParent = currentParent?.superview
|
||||
// }
|
||||
// if let controller = self.container.controllers.last {
|
||||
// if controller.view.disablesInteractiveModalDismiss {
|
||||
// enableScrolling = false
|
||||
// }
|
||||
// }
|
||||
// self.isInteractiveDimissEnabled = enableScrolling
|
||||
// if let layout = self.validLayout {
|
||||
// if layout.inputHeight != nil && layout.inputHeight != 0.0 {
|
||||
// enableScrolling = false
|
||||
// }
|
||||
// }
|
||||
// self.scrollNode.view.isScrollEnabled = enableScrolling
|
||||
return result
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user