mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
327 lines
12 KiB
Swift
327 lines
12 KiB
Swift
import Foundation
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import Lottie
|
|
|
|
enum ItemListRevealOptionIcon: Equatable {
|
|
case none
|
|
case image(image: UIImage)
|
|
case animation(animation: String, keysToColor: [String]?)
|
|
|
|
public static func ==(lhs: ItemListRevealOptionIcon, rhs: ItemListRevealOptionIcon) -> Bool {
|
|
switch lhs {
|
|
case .none:
|
|
if case .none = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .image(lhsImage):
|
|
if case let .image(rhsImage) = rhs, lhsImage == rhsImage {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .animation(lhsAnimation, lhsKeysToColor):
|
|
if case let .animation(rhsAnimation, rhsKeysToColor) = rhs, lhsAnimation == rhsAnimation, lhsKeysToColor == rhsKeysToColor {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ItemListRevealOption: Equatable {
|
|
let key: Int32
|
|
let title: String
|
|
let icon: ItemListRevealOptionIcon
|
|
let color: UIColor
|
|
let textColor: UIColor
|
|
|
|
static func ==(lhs: ItemListRevealOption, rhs: ItemListRevealOption) -> Bool {
|
|
if lhs.key != rhs.key {
|
|
return false
|
|
}
|
|
if lhs.title != rhs.title {
|
|
return false
|
|
}
|
|
if !lhs.color.isEqual(rhs.color) {
|
|
return false
|
|
}
|
|
if !lhs.textColor.isEqual(rhs.textColor) {
|
|
return false
|
|
}
|
|
if lhs.icon != rhs.icon {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
private final class ItemListRevealAnimationNode : ASDisplayNode {
|
|
var played = false
|
|
|
|
init(animation: String, keysToColor: [String]?, color: UIColor) {
|
|
super.init()
|
|
|
|
self.setViewBlock({
|
|
if let url = frameworkBundle.url(forResource: animation, withExtension: "json"), let composition = LOTComposition(filePath: url.path) {
|
|
let view = LOTAnimationView(model: composition, in: frameworkBundle)
|
|
|
|
if let keysToColor = keysToColor {
|
|
for key in keysToColor {
|
|
let colorCallback = LOTColorValueCallback(color: color.cgColor)
|
|
view.setValueDelegate(colorCallback, for: LOTKeypath(string: "\(key).Color"))
|
|
}
|
|
}
|
|
|
|
return view
|
|
} else {
|
|
return UIView()
|
|
}
|
|
})
|
|
}
|
|
|
|
func animationView() -> LOTAnimationView? {
|
|
return self.view as? LOTAnimationView
|
|
}
|
|
|
|
func play() {
|
|
if let animationView = animationView(), !animationView.isAnimationPlaying, !self.played {
|
|
self.played = true
|
|
animationView.play()
|
|
}
|
|
}
|
|
|
|
func reset() {
|
|
if self.played, let animationView = animationView() {
|
|
self.played = false
|
|
animationView.stop()
|
|
}
|
|
}
|
|
|
|
func preferredSize() -> CGSize? {
|
|
if let animationView = animationView(), let sceneModel = animationView.sceneModel {
|
|
return CGSize(width: sceneModel.compBounds.width * 0.3333, height: sceneModel.compBounds.height * 0.3333)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private let titleFontWithIcon = Font.medium(13.0)
|
|
private let titleFontWithoutIcon = Font.regular(17.0)
|
|
|
|
private enum ItemListRevealOptionAlignment {
|
|
case left
|
|
case right
|
|
}
|
|
|
|
private final class ItemListRevealOptionNode: ASDisplayNode {
|
|
private let titleNode: ASTextNode
|
|
private let iconNode: ASImageNode?
|
|
private let animationNode: ItemListRevealAnimationNode?
|
|
var alignment: ItemListRevealOptionAlignment?
|
|
|
|
init(title: String, icon: ItemListRevealOptionIcon, color: UIColor, textColor: UIColor) {
|
|
self.titleNode = ASTextNode()
|
|
self.titleNode.attributedText = NSAttributedString(string: title, font: icon == .none ? titleFontWithoutIcon : titleFontWithIcon, textColor: textColor)
|
|
|
|
switch icon {
|
|
case let .image(image):
|
|
let iconNode = ASImageNode()
|
|
iconNode.image = generateTintedImage(image: image, color: textColor)
|
|
self.iconNode = iconNode
|
|
self.animationNode = nil
|
|
|
|
case let .animation(animation, keysToColor):
|
|
self.iconNode = nil
|
|
self.animationNode = ItemListRevealAnimationNode(animation: animation, keysToColor: keysToColor, color: color)
|
|
break
|
|
|
|
case .none:
|
|
self.iconNode = nil
|
|
self.animationNode = nil
|
|
}
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.titleNode)
|
|
if let iconNode = self.iconNode {
|
|
self.addSubnode(iconNode)
|
|
} else if let animationNode = self.animationNode {
|
|
self.addSubnode(animationNode)
|
|
}
|
|
self.backgroundColor = color
|
|
}
|
|
|
|
func updateLayout(baseSize: CGSize, alignment: ItemListRevealOptionAlignment, extendedWidth: CGFloat, sideInset: CGFloat, transition: ContainedViewLayoutTransition, revealFactor: CGFloat) {
|
|
var animateAdditive = false
|
|
if transition.isAnimated, self.alignment != alignment {
|
|
animateAdditive = true
|
|
}
|
|
self.alignment = alignment
|
|
let titleSize = self.titleNode.calculatedSize
|
|
var contentRect = CGRect(origin: CGPoint(), size: baseSize)
|
|
switch alignment {
|
|
case .left:
|
|
contentRect.origin.x = 0.0
|
|
case .right:
|
|
contentRect.origin.x = extendedWidth - contentRect.width
|
|
}
|
|
|
|
if let animationNode = self.animationNode, let imageSize = animationNode.preferredSize() {
|
|
let iconOffset: CGFloat = -2.0
|
|
let titleIconSpacing: CGFloat = 11.0
|
|
let iconFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - imageSize.width + sideInset) / 2.0), y: contentRect.midY - imageSize.height / 2.0 + iconOffset), size: imageSize)
|
|
if animateAdditive {
|
|
transition.animatePositionAdditive(node: animationNode, offset: CGPoint(x: animationNode.frame.minX - iconFrame.minX, y: 0.0))
|
|
}
|
|
animationNode.frame = iconFrame
|
|
|
|
let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - titleSize.width + sideInset) / 2.0), y: contentRect.midY + titleIconSpacing), size: titleSize)
|
|
if animateAdditive {
|
|
transition.animatePositionAdditive(node: self.titleNode, offset: CGPoint(x: self.titleNode.frame.minX - titleFrame.minX, y: 0.0))
|
|
}
|
|
self.titleNode.frame = titleFrame
|
|
|
|
if (abs(revealFactor) >= 0.4) {
|
|
animationNode.play()
|
|
} else if abs(revealFactor) < CGFloat.ulpOfOne {
|
|
animationNode.reset()
|
|
}
|
|
}
|
|
else {
|
|
self.titleNode.frame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - titleSize.width + sideInset) / 2.0), y: contentRect.minY + floor((baseSize.height - titleSize.height) / 2.0)), size: titleSize)
|
|
}
|
|
}
|
|
|
|
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
|
let titleSize = self.titleNode.measure(constrainedSize)
|
|
var maxWidth = titleSize.width
|
|
if let iconNode = self.iconNode, let image = iconNode.image {
|
|
maxWidth = max(image.size.width, maxWidth)
|
|
}
|
|
return CGSize(width: max(74.0, maxWidth + 20.0), height: constrainedSize.height)
|
|
}
|
|
}
|
|
|
|
final class ItemListRevealOptionsNode: ASDisplayNode {
|
|
private let optionSelected: (ItemListRevealOption) -> Void
|
|
private let tapticAction: () -> Void
|
|
|
|
private var options: [ItemListRevealOption] = []
|
|
|
|
private var optionNodes: [ItemListRevealOptionNode] = []
|
|
private var revealOffset: CGFloat = 0.0
|
|
private var sideInset: CGFloat = 0.0
|
|
|
|
init(optionSelected: @escaping (ItemListRevealOption) -> Void, tapticAction: @escaping () -> Void) {
|
|
self.optionSelected = optionSelected
|
|
self.tapticAction = tapticAction
|
|
|
|
super.init()
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
|
}
|
|
|
|
func setOptions(_ options: [ItemListRevealOption]) {
|
|
if self.options != options {
|
|
self.options = options
|
|
for node in self.optionNodes {
|
|
node.removeFromSupernode()
|
|
}
|
|
self.optionNodes = options.map { option in
|
|
return ItemListRevealOptionNode(title: option.title, icon: option.icon, color: option.color, textColor: option.textColor)
|
|
}
|
|
for node in self.optionNodes {
|
|
self.addSubnode(node)
|
|
}
|
|
self.invalidateCalculatedLayout()
|
|
}
|
|
}
|
|
|
|
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
|
var maxWidth: CGFloat = 0.0
|
|
for node in self.optionNodes {
|
|
let nodeSize = node.measure(constrainedSize)
|
|
maxWidth = max(nodeSize.width, maxWidth)
|
|
}
|
|
return CGSize(width: maxWidth * CGFloat(self.optionNodes.count), height: constrainedSize.height)
|
|
}
|
|
|
|
func updateRevealOffset(offset: CGFloat, sideInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
self.revealOffset = offset
|
|
self.sideInset = sideInset
|
|
self.updateNodesLayout(transition: transition)
|
|
}
|
|
|
|
private func updateNodesLayout(transition: ContainedViewLayoutTransition) {
|
|
let size = self.bounds.size
|
|
if size.width.isLessThanOrEqualTo(0.0) || self.optionNodes.isEmpty {
|
|
return
|
|
}
|
|
let basicNodeWidth = floorToScreenPixels((size.width - abs(self.sideInset)) / CGFloat(self.optionNodes.count))
|
|
let lastNodeWidth = size.width - basicNodeWidth * CGFloat(self.optionNodes.count - 1)
|
|
let revealFactor = min(1.0, self.revealOffset / (size.width))
|
|
var leftOffset: CGFloat = 0.0
|
|
for i in 0 ..< self.optionNodes.count {
|
|
let node = self.optionNodes[i]
|
|
let nodeWidth = i == (self.optionNodes.count - 1) ? lastNodeWidth : basicNodeWidth
|
|
var extendedWidth = nodeWidth
|
|
var alignment: ItemListRevealOptionAlignment = .left
|
|
var nodeTransition = transition
|
|
if self.optionNodes.count == 1 {
|
|
extendedWidth = nodeWidth * max(1.0, abs(revealFactor))
|
|
if abs(revealFactor) > 1.7 {
|
|
alignment = .right
|
|
}
|
|
}
|
|
if let nodeAlignment = node.alignment, alignment != nodeAlignment {
|
|
nodeTransition = .animated(duration: 0.2, curve: .spring)
|
|
if alignment == .right || !transition.isAnimated {
|
|
self.tapticAction()
|
|
}
|
|
}
|
|
|
|
var sideInset: CGFloat = 0.0
|
|
if i == self.optionNodes.count - 1 {
|
|
sideInset = self.sideInset
|
|
}
|
|
|
|
transition.updateFrame(node: node, frame: CGRect(origin: CGPoint(x: floorToScreenPixels(leftOffset * revealFactor), y: 0.0), size: CGSize(width: extendedWidth, height: size.height)))
|
|
node.updateLayout(baseSize: CGSize(width: nodeWidth, height: size.height), alignment: alignment, extendedWidth: extendedWidth, sideInset: sideInset, transition: nodeTransition, revealFactor: revealFactor)
|
|
leftOffset += nodeWidth
|
|
}
|
|
}
|
|
|
|
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
let location = recognizer.location(in: self.view)
|
|
for i in 0 ..< self.optionNodes.count {
|
|
if self.optionNodes[i].frame.contains(location) {
|
|
self.optionSelected(self.options[i])
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func isDisplayingExtendedAction() -> Bool {
|
|
if self.optionNodes.count != 1 {
|
|
return false
|
|
}
|
|
for node in self.optionNodes {
|
|
if let alignment = node.alignment, case .right = alignment {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|