Swiftgram/TelegramUI/InstantPageDetailsNode.swift
Ilya Laktyushin 3cb29347d1 Merge branch 'master' of github.com:peter-iakovlev/TelegramUI
# Conflicts:
#	TelegramUI/PresentationStrings.swift
2018-11-02 20:32:05 +04:00

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()
}
}
}