Swiftgram/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift

315 lines
12 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SyncCore
import SwiftSignalKit
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
private let detailsInset: CGFloat = 17.0
private let titleInset: CGFloat = 22.0
final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode {
private let context: AccountContext
private let strings: PresentationStrings
private let nameDisplayOrder: PresentationPersonNameOrder
private let theme: InstantPageTheme
let item: InstantPageDetailsItem
private let titleTile: InstantPageTile
private let titleTileNode: InstantPageTileNode
private let highlightedBackgroundNode: ASDisplayNode
private let buttonNode: HighlightableButtonNode
private let arrowNode: InstantPageDetailsArrowNode
let separatorNode: ASDisplayNode
let contentNode: InstantPageContentNode
private let updateExpanded: (Bool) -> Void
var expanded: Bool
var previousNode: InstantPageDetailsNode?
var requestLayoutUpdate: ((Bool) -> Void)?
init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, item: InstantPageDetailsItem, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, currentlyExpanded: Bool?, updateDetailsExpanded: @escaping (Bool) -> Void) {
self.context = context
self.strings = strings
self.nameDisplayOrder = nameDisplayOrder
self.theme = theme
self.item = item
self.updateExpanded = updateDetailsExpanded
let frame = item.frame
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.highlightedBackgroundNode.alpha = 0.0
self.buttonNode = HighlightableButtonNode()
self.titleTile = InstantPageTile(frame: CGRect(x: 0.0, y: 0.0, width: frame.width, height: item.titleHeight))
self.titleTile.items.append(contentsOf: item.titleItems)
self.titleTileNode = InstantPageTileNode(tile: self.titleTile, backgroundColor: .clear)
if let expanded = currentlyExpanded {
self.expanded = expanded
} else {
self.expanded = item.initiallyExpanded
}
self.arrowNode = InstantPageDetailsArrowNode(color: theme.controlColor, open: self.expanded)
self.separatorNode = ASDisplayNode()
self.contentNode = InstantPageContentNode(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, sourcePeerType: sourcePeerType, theme: theme, items: item.items, contentSize: CGSize(width: item.frame.width, height: item.frame.height - item.titleHeight), openMedia: openMedia, longPressMedia: longPressMedia, openPeer: openPeer, openUrl: openUrl)
super.init()
self.clipsToBounds = true
self.addSubnode(self.contentNode)
self.addSubnode(self.highlightedBackgroundNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.titleTileNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.separatorNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.highlightedBackgroundNode.alpha = 1.0
strongSelf.separatorNode.alpha = 0.0
if let previousSeparator = strongSelf.previousNode?.separatorNode {
previousSeparator.alpha = 0.0
}
} else {
strongSelf.highlightedBackgroundNode.alpha = 0.0
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
strongSelf.separatorNode.alpha = 1.0
strongSelf.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if let previousSeparator = strongSelf.previousNode?.separatorNode {
previousSeparator.alpha = 1.0
previousSeparator.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
}
self.contentNode.requestLayoutUpdate = { [weak self] animated in
self?.requestLayoutUpdate?(animated)
}
self.update(strings: strings, theme: theme)
}
@objc func buttonPressed() {
self.setExpanded(!self.expanded, animated: true)
self.updateExpanded(expanded)
}
func setExpanded(_ expanded: Bool, animated: Bool) {
self.expanded = expanded
self.arrowNode.setOpen(expanded, animated: animated)
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
}
override func layout() {
super.layout()
let size = self.bounds.size
let inset = detailsInset + self.item.safeInset
self.titleTileNode.frame = self.titleTile.frame
self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: self.item.titleHeight + UIScreenPixel))
self.buttonNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: self.item.titleHeight))
self.arrowNode.frame = CGRect(x: inset, y: floorToScreenPixels((self.item.titleHeight - 8.0) / 2.0) + 1.0, width: 13.0, height: 8.0)
self.contentNode.frame = CGRect(x: 0.0, y: self.item.titleHeight, width: size.width, height: self.item.frame.height - self.item.titleHeight)
let lineSize = CGSize(width: self.frame.width - inset, height: UIScreenPixel)
self.separatorNode.frame = CGRect(origin: CGPoint(x: self.item.rtl ? 0.0 : inset, y: self.item.titleHeight - lineSize.height), size: lineSize)
}
func updateIsVisible(_ isVisible: Bool) {
}
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return self.contentNode.transitionNode(media: media)
}
func updateHiddenMedia(media: InstantPageMedia?) {
self.contentNode.updateHiddenMedia(media: media)
}
func update(strings: PresentationStrings, theme: InstantPageTheme) {
self.arrowNode.color = theme.controlColor
self.separatorNode.backgroundColor = theme.controlColor
self.highlightedBackgroundNode.backgroundColor = theme.panelHighlightedBackgroundColor
}
func updateVisibleItems(visibleBounds: CGRect, animated: Bool) {
if self.bounds.height > self.item.titleHeight {
self.contentNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -self.contentNode.frame.minX, dy: -self.contentNode.frame.minY), animated: animated)
}
}
func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? {
if self.titleTileNode.frame.contains(location) {
for case let item as InstantPageTextItem in self.item.titleItems {
if item.frame.contains(location) {
return (item, self.titleTileNode.frame.origin)
}
}
}
else if let (textItem, parentOffset) = self.contentNode.textItemAtLocation(location.offsetBy(dx: -self.contentNode.frame.minX, dy: -self.contentNode.frame.minY)) {
return (textItem, self.contentNode.frame.origin.offsetBy(dx: parentOffset.x, dy: parentOffset.y))
}
return nil
}
func tapActionAtPoint(_ point: CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction {
if self.titleTileNode.frame.contains(point) {
if self.item.linkSelectionRects(at: point).isEmpty {
return .fail
}
} else if self.contentNode.frame.contains(point) {
return self.contentNode.tapActionAtPoint(_: point.offsetBy(dx: -self.contentNode.frame.minX, dy: -self.contentNode.frame.minY))
}
return .waitForSingleTap
}
var effectiveContentSize: CGSize {
return self.contentNode.effectiveContentSize
}
func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect {
return self.contentNode.effectiveFrameForItem(item).offsetBy(dx: 0.0, dy: self.item.titleHeight)
}
}
private final class InstantPageDetailsArrowNodeParameters: NSObject {
let color: UIColor
let progress: CGFloat
init(color: UIColor, progress: CGFloat) {
self.color = color
self.progress = progress
}
}
final class InstantPageDetailsArrowNode : ASDisplayNode {
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
private (set) var open: Bool
private var progress: CGFloat = 0.0
private var targetProgress: CGFloat?
private var displayLink: CADisplayLink?
init(color: UIColor, open: Bool) {
self.color = color
self.open = open
self.progress = open ? 1.0 : 0.0
super.init()
self.isOpaque = false
self.isLayerBacked = true
class DisplayLinkProxy: NSObject {
weak var target: InstantPageDetailsArrowNode?
init(target: InstantPageDetailsArrowNode) {
self.target = target
}
@objc func displayLinkEvent() {
self.target?.displayLinkEvent()
}
}
self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent))
self.displayLink?.isPaused = true
self.displayLink?.add(to: RunLoop.main, forMode: .common)
}
deinit {
self.displayLink?.invalidate()
}
func setOpen(_ open: Bool, animated: Bool) {
self.open = open
let openProgress: CGFloat = open ? 1.0 : 0.0
if animated {
self.targetProgress = openProgress
self.displayLink?.isPaused = false
} else {
self.progress = openProgress
self.targetProgress = nil
self.displayLink?.isPaused = true
}
}
override func willEnterHierarchy() {
super.willEnterHierarchy()
if self.targetProgress != nil {
self.displayLink?.isPaused = false
}
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.displayLink?.isPaused = true
}
private func displayLinkEvent() {
if let targetProgress = self.targetProgress {
let sign = CGFloat(targetProgress - self.progress > 0 ? 1 : -1)
self.progress += 0.14 * sign
if sign > 0 && self.progress > targetProgress {
self.progress = 1.0
self.targetProgress = nil
self.displayLink?.isPaused = true
} else if sign < 0 && self.progress < targetProgress {
self.progress = 0.0
self.targetProgress = nil
self.displayLink?.isPaused = true
}
}
self.setNeedsDisplay()
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return InstantPageDetailsArrowNodeParameters(color: self.color, progress: self.progress)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if let parameters = parameters as? InstantPageDetailsArrowNodeParameters {
context.setStrokeColor(parameters.color.cgColor)
context.setLineCap(.round)
context.setLineWidth(2.0)
context.move(to: CGPoint(x: 1.0, y: 1.0 + 5.0 * parameters.progress))
context.addLine(to: CGPoint(x: 6.0, y: 6.0 - 5.0 * parameters.progress))
context.addLine(to: CGPoint(x: 11.0, y: 1.0 + 5.0 * parameters.progress))
context.strokePath()
}
}
}