mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 06:10:03 +00:00
447 lines
17 KiB
Swift
447 lines
17 KiB
Swift
import Foundation
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import Postbox
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
|
|
private let detailsHeaderHeight: CGFloat = 44.0
|
|
private let detailsInset: CGFloat = 17.0
|
|
private let titleInset: CGFloat = 22.0
|
|
|
|
final class InstantPageDetailsContentNode : ASDisplayNode {
|
|
private let account: Account
|
|
private let strings: PresentationStrings
|
|
private let theme: InstantPageTheme
|
|
|
|
var currentLayoutTiles: [InstantPageTile] = []
|
|
var currentLayoutItemsWithNodes: [InstantPageItem] = []
|
|
var distanceThresholdGroupCount: [Int: Int] = [:]
|
|
|
|
var visibleTiles: [Int: InstantPageTileNode] = [:]
|
|
var visibleItemsWithNodes: [Int: InstantPageNode] = [:]
|
|
|
|
var currentLayout: InstantPageLayout
|
|
let contentSize: CGSize
|
|
|
|
init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, items: [InstantPageItem], contentSize: CGSize) {
|
|
self.account = account
|
|
self.strings = strings
|
|
self.theme = theme
|
|
|
|
self.currentLayout = InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
|
self.contentSize = contentSize
|
|
|
|
super.init()
|
|
|
|
self.updateLayout()
|
|
}
|
|
|
|
private func updateLayout() {
|
|
for (_, tileNode) in self.visibleTiles {
|
|
tileNode.removeFromSupernode()
|
|
}
|
|
self.visibleTiles.removeAll()
|
|
|
|
let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: contentSize.width)
|
|
|
|
var currentLayoutItemsWithViews: [InstantPageItem] = []
|
|
var distanceThresholdGroupCount: [Int : Int] = [:]
|
|
|
|
for item in self.currentLayout.items {
|
|
if item.wantsNode {
|
|
currentLayoutItemsWithViews.append(item)
|
|
if let group = item.distanceThresholdGroup() {
|
|
let count: Int
|
|
if let currentCount = distanceThresholdGroupCount[Int(group)] {
|
|
count = currentCount
|
|
} else {
|
|
count = 0
|
|
}
|
|
distanceThresholdGroupCount[Int(group)] = count + 1
|
|
}
|
|
}
|
|
}
|
|
|
|
self.currentLayoutTiles = currentLayoutTiles
|
|
self.currentLayoutItemsWithNodes = currentLayoutItemsWithViews
|
|
self.distanceThresholdGroupCount = distanceThresholdGroupCount
|
|
}
|
|
|
|
func updateVisibleItems() {
|
|
var visibleTileIndices = Set<Int>()
|
|
var visibleItemIndices = Set<Int>()
|
|
|
|
let visibleBounds = self.bounds // self.scrollNode.view.bounds
|
|
|
|
var topNode: ASDisplayNode?
|
|
if let scrollSubnodes = self.subnodes {
|
|
for node in scrollSubnodes.reversed() {
|
|
if let node = node as? InstantPageTileNode {
|
|
topNode = node
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
var tileIndex = -1
|
|
for tile in self.currentLayoutTiles {
|
|
tileIndex += 1
|
|
var tileVisibleFrame = tile.frame
|
|
tileVisibleFrame.origin.y -= 400.0
|
|
tileVisibleFrame.size.height += 400.0 * 2.0
|
|
if tileVisibleFrame.intersects(visibleBounds) {
|
|
visibleTileIndices.insert(tileIndex)
|
|
|
|
if visibleTiles[tileIndex] == nil {
|
|
let tileNode = InstantPageTileNode(tile: tile, backgroundColor: .clear)
|
|
tileNode.frame = tile.frame
|
|
if let topNode = topNode {
|
|
self.insertSubnode(tileNode, aboveSubnode: topNode)
|
|
} else {
|
|
self.insertSubnode(tileNode, at: 0)
|
|
}
|
|
topNode = tileNode
|
|
self.visibleTiles[tileIndex] = tileNode
|
|
}
|
|
}
|
|
}
|
|
|
|
var itemIndex = -1
|
|
for item in self.currentLayoutItemsWithNodes {
|
|
itemIndex += 1
|
|
var itemThreshold: CGFloat = 0.0
|
|
if let group = item.distanceThresholdGroup() {
|
|
var count: Int = 0
|
|
if let currentCount = self.distanceThresholdGroupCount[group] {
|
|
count = currentCount
|
|
}
|
|
itemThreshold = item.distanceThresholdWithGroupCount(count)
|
|
}
|
|
var itemFrame = item.frame
|
|
itemFrame.origin.y -= itemThreshold
|
|
itemFrame.size.height += itemThreshold * 2.0
|
|
if visibleBounds.intersects(itemFrame) {
|
|
visibleItemIndices.insert(itemIndex)
|
|
|
|
var itemNode = self.visibleItemsWithNodes[itemIndex]
|
|
if let currentItemNode = itemNode {
|
|
if !item.matchesNode(currentItemNode) {
|
|
(currentItemNode as! ASDisplayNode).removeFromSupernode()
|
|
self.visibleItemsWithNodes.removeValue(forKey: itemIndex)
|
|
itemNode = nil
|
|
}
|
|
}
|
|
|
|
if itemNode == nil {
|
|
if let itemNode = item.node(account: self.account, strings: self.strings, theme: self.theme, openMedia: { [weak self] media in
|
|
//self?.openMedia(media)
|
|
}, openPeer: { [weak self] peerId in
|
|
//self?.openPeer(peerId)
|
|
}, openUrl: { [weak self] url in
|
|
//self?.openUrl(url)
|
|
}, updateWebEmbedHeight: { [weak self] key, height in
|
|
//self?.updateWebEmbedHeight(key, height)
|
|
}, updateDetailsOpened: { _, _ in
|
|
}) {
|
|
itemNode.frame = item.frame
|
|
if let topNode = topNode {
|
|
self.insertSubnode(itemNode, aboveSubnode: topNode)
|
|
} else {
|
|
self.insertSubnode(itemNode, at: 0)
|
|
}
|
|
topNode = itemNode
|
|
self.visibleItemsWithNodes[itemIndex] = itemNode
|
|
}
|
|
} else {
|
|
if (itemNode as! ASDisplayNode).frame != item.frame {
|
|
(itemNode as! ASDisplayNode).frame = item.frame
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var removeTileIndices: [Int] = []
|
|
for (index, tileNode) in self.visibleTiles {
|
|
if !visibleTileIndices.contains(index) {
|
|
removeTileIndices.append(index)
|
|
tileNode.removeFromSupernode()
|
|
}
|
|
}
|
|
for index in removeTileIndices {
|
|
self.visibleTiles.removeValue(forKey: index)
|
|
}
|
|
|
|
var removeItemIndices: [Int] = []
|
|
for (index, itemNode) in self.visibleItemsWithNodes {
|
|
if !visibleItemIndices.contains(index) {
|
|
removeItemIndices.append(index)
|
|
(itemNode as! ASDisplayNode).removeFromSupernode()
|
|
} else {
|
|
var itemFrame = (itemNode as! ASDisplayNode).frame
|
|
let itemThreshold: CGFloat = 200.0
|
|
itemFrame.origin.y -= itemThreshold
|
|
itemFrame.size.height += itemThreshold * 2.0
|
|
itemNode.updateIsVisible(visibleBounds.intersects(itemFrame))
|
|
}
|
|
}
|
|
for index in removeItemIndices {
|
|
self.visibleItemsWithNodes.removeValue(forKey: index)
|
|
}
|
|
}
|
|
}
|
|
|
|
final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode {
|
|
private let account: Account
|
|
private let strings: PresentationStrings
|
|
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
|
|
private let separatorNode: ASDisplayNode
|
|
private let contentNode: InstantPageDetailsContentNode
|
|
|
|
let updateOpened: (Int, Bool) -> Void
|
|
var opened: Bool
|
|
|
|
init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, item: InstantPageDetailsItem, updateDetailsOpened: @escaping (Int, Bool) -> Void) {
|
|
self.account = account
|
|
self.strings = strings
|
|
self.theme = theme
|
|
self.item = item
|
|
|
|
self.updateOpened = updateDetailsOpened
|
|
|
|
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: detailsHeaderHeight))
|
|
self.titleTileNode = InstantPageTileNode(tile: self.titleTile, backgroundColor: .clear)
|
|
|
|
let titleItems = layoutTextItemWithString(item.title, boundingWidth: frame.size.width - detailsInset * 2.0 - titleInset, offset: CGPoint(x: detailsInset + titleInset, y: 0.0)).0
|
|
var offset: CGFloat?
|
|
for var item in titleItems {
|
|
var itemOffset = floorToScreenPixels((detailsHeaderHeight - item.frame.height) / 2.0)
|
|
if item is InstantPageTextItem {
|
|
offset = itemOffset
|
|
} else if let offset = offset {
|
|
itemOffset = offset
|
|
}
|
|
item.frame = item.frame.offsetBy(dx: 0.0, dy: itemOffset)
|
|
}
|
|
self.titleTile.items.append(contentsOf: titleItems)
|
|
|
|
self.arrowNode = InstantPageDetailsArrowNode(color: theme.controlColor, open: item.open)
|
|
self.separatorNode = ASDisplayNode()
|
|
|
|
self.contentNode = InstantPageDetailsContentNode(account: account, strings: strings, theme: theme, items: item.items, contentSize: CGSize(width: item.frame.width, height: item.frame.height))
|
|
|
|
self.opened = item.open
|
|
|
|
super.init()
|
|
|
|
self.clipsToBounds = true
|
|
|
|
self.addSubnode(self.highlightedBackgroundNode)
|
|
self.addSubnode(self.buttonNode)
|
|
self.addSubnode(self.titleTileNode)
|
|
self.addSubnode(self.arrowNode)
|
|
self.addSubnode(self.separatorNode)
|
|
self.addSubnode(self.contentNode)
|
|
|
|
let lineSize = CGSize(width: frame.width - detailsInset, height: UIScreenPixel)
|
|
self.separatorNode.frame = CGRect(origin: CGPoint(x: item.rtl ? 0.0 : detailsInset, y: detailsHeaderHeight - lineSize.height), size: lineSize)
|
|
|
|
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
|
|
if strongSelf.separatorNode.frame.minY < strongSelf.highlightedBackgroundNode.frame.maxY {
|
|
strongSelf.separatorNode.alpha = 0.0
|
|
}
|
|
} else {
|
|
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
|
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
|
if strongSelf.separatorNode.alpha < 1.0 {
|
|
strongSelf.separatorNode.alpha = 1.0
|
|
strongSelf.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.update(strings: strings, theme: theme)
|
|
}
|
|
|
|
@objc func buttonPressed() {
|
|
self.setOpened(!self.opened, animated: true)
|
|
}
|
|
|
|
func setOpened(_ opened: Bool, animated: Bool) {
|
|
self.opened = opened
|
|
self.arrowNode.setOpen(opened, animated: animated)
|
|
self.updateOpened(0, opened)
|
|
}
|
|
|
|
override func layout() {
|
|
super.layout()
|
|
|
|
let size = self.bounds.size
|
|
|
|
self.titleTileNode.frame = self.titleTile.frame
|
|
self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: detailsHeaderHeight + UIScreenPixel))
|
|
self.buttonNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: detailsHeaderHeight))
|
|
self.arrowNode.frame = CGRect(x: detailsInset, y: floorToScreenPixels((detailsHeaderHeight - 8.0) / 2.0) + 1.0, width: 13.0, height: 8.0)
|
|
self.contentNode.frame = CGRect(x: 0.0, y: detailsHeaderHeight, width: size.width, height: self.item.frame.height - detailsHeaderHeight)
|
|
|
|
self.contentNode.updateVisibleItems()
|
|
}
|
|
|
|
func updateIsVisible(_ isVisible: Bool) {
|
|
|
|
}
|
|
|
|
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? {
|
|
return nil
|
|
}
|
|
|
|
func updateHiddenMedia(media: InstantPageMedia?) {
|
|
|
|
}
|
|
|
|
func update(strings: PresentationStrings, theme: InstantPageTheme) {
|
|
self.arrowNode.color = theme.controlColor
|
|
self.separatorNode.backgroundColor = theme.controlColor
|
|
self.highlightedBackgroundNode.backgroundColor = theme.panelHighlightedBackgroundColor
|
|
}
|
|
}
|
|
|
|
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: RunLoopMode.commonModes)
|
|
}
|
|
|
|
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: 6.0 - 5.0 * parameters.progress))
|
|
context.addLine(to: CGPoint(x: 6.0, y: 1.0 + 5.0 * parameters.progress))
|
|
context.addLine(to: CGPoint(x: 11.0, y: 6.0 - 5.0 * parameters.progress))
|
|
context.strokePath()
|
|
}
|
|
}
|
|
}
|