Swiftgram/submodules/Display/Source/PeekControllerNode.swift
2019-11-14 23:27:44 +04:00

358 lines
16 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
final class PeekControllerNode: ViewControllerTracingNode {
private let requestDismiss: () -> Void
private let theme: PeekControllerTheme
private let blurView: UIView
private let dimNode: ASDisplayNode
private let containerBackgroundNode: ASImageNode
private let containerNode: ASDisplayNode
private var validLayout: ContainerViewLayout?
private var containerOffset: CGFloat = 0.0
private var panInitialContainerOffset: CGFloat?
private var content: PeekControllerContent
private var contentNode: PeekControllerContentNode & ASDisplayNode
private var contentNodeHasValidLayout = false
private var topAccessoryNode: ASDisplayNode?
private var menuNode: PeekControllerMenuNode?
private var displayingMenu = false
private var hapticFeedback: HapticFeedback?
init(theme: PeekControllerTheme, content: PeekControllerContent, requestDismiss: @escaping () -> Void) {
self.theme = theme
self.requestDismiss = requestDismiss
self.dimNode = ASDisplayNode()
self.blurView = UIVisualEffectView(effect: UIBlurEffect(style: theme.isDark ? .dark : .light))
self.blurView.isUserInteractionEnabled = false
switch content.menuActivation() {
case .drag:
self.dimNode.backgroundColor = nil
self.blurView.alpha = 1.0
case .press:
self.dimNode.backgroundColor = UIColor(white: theme.isDark ? 0.0 : 1.0, alpha: 0.5)
self.blurView.alpha = 0.0
}
self.containerBackgroundNode = ASImageNode()
self.containerBackgroundNode.isLayerBacked = true
self.containerBackgroundNode.displaysAsynchronously = false
self.containerNode = ASDisplayNode()
self.content = content
self.contentNode = content.node()
self.topAccessoryNode = content.topAccessoryNode()
var activatedActionImpl: (() -> Void)?
let menuItems = content.menuItems()
if menuItems.isEmpty {
self.menuNode = nil
} else {
self.menuNode = PeekControllerMenuNode(theme: theme, items: menuItems, activatedAction: {
activatedActionImpl?()
})
}
super.init()
if content.presentation() == .freeform {
self.containerNode.isUserInteractionEnabled = false
} else {
self.containerNode.clipsToBounds = true
self.containerNode.cornerRadius = 16.0
}
self.addSubnode(self.dimNode)
self.view.addSubview(self.blurView)
self.containerNode.addSubnode(self.contentNode)
self.addSubnode(self.containerNode)
if let topAccessoryNode = self.topAccessoryNode {
self.addSubnode(topAccessoryNode)
}
if let menuNode = self.menuNode {
self.addSubnode(menuNode)
}
activatedActionImpl = { [weak self] in
self?.requestDismiss()
}
self.hapticFeedback = HapticFeedback()
self.hapticFeedback?.prepareTap()
}
deinit {
}
override func didLoad() {
super.didLoad()
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:))))
self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(view: self.blurView, frame: CGRect(origin: CGPoint(), size: layout.size))
var layoutInsets = layout.insets(options: [])
let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
layoutInsets.left = floor((layout.size.width - containerWidth) / 2.0)
layoutInsets.right = layoutInsets.left
if !layoutInsets.bottom.isZero {
layoutInsets.bottom -= 12.0
}
let maxContainerSize = CGSize(width: layout.size.width - 14.0 * 2.0, height: layout.size.height - layoutInsets.top - layoutInsets.bottom - 90.0)
var menuSize: CGSize?
let contentSize = self.contentNode.updateLayout(size: maxContainerSize, transition: self.contentNodeHasValidLayout ? transition : .immediate)
if self.contentNodeHasValidLayout {
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize))
} else {
self.contentNode.frame = CGRect(origin: CGPoint(), size: contentSize)
}
var containerFrame: CGRect
switch self.content.presentation() {
case .contained:
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize)
case .freeform:
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 4.0)), size: contentSize)
}
if let menuNode = self.menuNode {
let menuWidth = layout.size.width - layoutInsets.left - layoutInsets.right - 14.0 * 2.0
let menuHeight = menuNode.updateLayout(width: menuWidth, transition: transition)
menuSize = CGSize(width: menuWidth, height: menuHeight)
if self.displayingMenu {
let upperBound = layout.size.height - layoutInsets.bottom - menuHeight - 14.0 * 2.0 - containerFrame.height
if containerFrame.origin.y > upperBound {
containerFrame.origin.y = upperBound
}
transition.updateAlpha(layer: self.blurView.layer, alpha: 1.0)
}
}
if self.displayingMenu {
var offset = self.containerOffset
let delta = abs(offset)
let factor: CGFloat = 60.0
offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0)
containerFrame = containerFrame.offsetBy(dx: 0.0, dy: offset)
} else {
containerFrame = containerFrame.offsetBy(dx: 0.0, dy: self.containerOffset)
}
transition.updateFrame(node: self.containerNode, frame: containerFrame)
if let topAccessoryNode = self.topAccessoryNode {
let accessorySize = topAccessoryNode.frame.size
let accessoryFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(containerFrame.midX - accessorySize.width / 2.0), y: containerFrame.minY - accessorySize.height - 16.0), size: accessorySize)
transition.updateFrame(node: topAccessoryNode, frame: accessoryFrame)
transition.updateAlpha(node: topAccessoryNode, alpha: self.displayingMenu ? 0.0 : 1.0)
}
if let menuNode = self.menuNode, let menuSize = menuSize {
let menuY: CGFloat
if self.displayingMenu {
menuY = max(containerFrame.maxY + 14.0, layout.size.height - layoutInsets.bottom - 14.0 - menuSize.height)
} else {
menuY = layout.size.height + 14.0
}
let menuFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - menuSize.width) / 2.0), y: menuY), size: menuSize)
if self.contentNodeHasValidLayout {
transition.updateFrame(node: menuNode, frame: menuFrame)
} else {
menuNode.frame = menuFrame
}
}
self.contentNodeHasValidLayout = true
}
func animateIn(from rect: CGRect) {
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.blurView.layer.animateAlpha(from: 0.0, to: self.blurView.alpha, duration: 0.3)
let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y)
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true)
self.containerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0)
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
if let topAccessoryNode = self.topAccessoryNode {
topAccessoryNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true)
topAccessoryNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0)
topAccessoryNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
if case .press = self.content.menuActivation() {
self.hapticFeedback?.tap()
} else {
self.hapticFeedback?.impact()
}
}
func animateOut(to rect: CGRect, completion: @escaping () -> Void) {
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.blurView.layer.animateAlpha(from: self.blurView.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false)
let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y)
self.containerNode.layer.animatePosition(from: CGPoint(), to: offset, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, force: true, completion: { _ in
completion()
})
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.containerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false)
if let topAccessoryNode = self.topAccessoryNode {
topAccessoryNode.layer.animatePosition(from: CGPoint(), to: offset, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, force: true, completion: { _ in
completion()
})
topAccessoryNode.layer.animateAlpha(from: topAccessoryNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
topAccessoryNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false)
}
if let menuNode = self.menuNode {
menuNode.layer.animatePosition(from: menuNode.position, to: CGPoint(x: menuNode.position.x, y: self.bounds.size.height + menuNode.bounds.size.height / 2.0), duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false)
}
}
@objc func dimNodeTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.requestDismiss()
}
}
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
guard case .drag = self.content.menuActivation() else {
return
}
switch recognizer.state {
case .began:
self.panInitialContainerOffset = self.containerOffset
case .changed:
if let panInitialContainerOffset = self.panInitialContainerOffset {
let translation = recognizer.translation(in: self.view)
var offset = panInitialContainerOffset + translation.y
if offset < 0.0 {
let delta = abs(offset)
let factor: CGFloat = 60.0
offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0)
}
self.applyDraggingOffset(offset)
}
case .cancelled, .ended:
if let _ = self.panInitialContainerOffset {
self.panInitialContainerOffset = nil
if self.containerOffset < 0.0 {
self.activateMenu()
} else {
self.requestDismiss()
}
}
default:
break
}
}
func applyDraggingOffset(_ offset: CGFloat) {
self.containerOffset = offset
if self.containerOffset < -25.0 {
//self.displayingMenu = true
} else {
//self.displayingMenu = false
}
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .immediate)
}
}
func activateMenu() {
if case .press = self.content.menuActivation() {
self.hapticFeedback?.impact()
}
if let layout = self.validLayout {
self.displayingMenu = true
self.containerOffset = 0.0
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.18, curve: .spring))
}
}
func endDraggingWithVelocity(_ velocity: CGFloat) {
if let _ = self.menuNode, velocity < -600.0 || self.containerOffset < -38.0 {
if let layout = self.validLayout {
self.displayingMenu = true
self.containerOffset = 0.0
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.18, curve: .spring))
}
} else {
self.requestDismiss()
}
}
func updateContent(content: PeekControllerContent) {
let contentNode = self.contentNode
contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak contentNode] _ in
contentNode?.removeFromSupernode()
})
contentNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.15, removeOnCompletion: false)
self.menuNode?.removeFromSupernode()
self.menuNode = nil
self.content = content
self.contentNode = content.node()
self.containerNode.addSubnode(self.contentNode)
self.contentNodeHasValidLayout = false
var activatedActionImpl: (() -> Void)?
let menuItems = content.menuItems()
if menuItems.isEmpty {
self.menuNode = nil
} else {
self.menuNode = PeekControllerMenuNode(theme: self.theme, items: menuItems, activatedAction: {
activatedActionImpl?()
})
}
if let menuNode = self.menuNode {
self.addSubnode(menuNode)
}
activatedActionImpl = { [weak self] in
self?.requestDismiss()
}
self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
self.contentNode.layer.animateSpring(from: 0.35 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.15, curve: .easeInOut))
}
self.hapticFeedback?.tap()
}
}