mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
477 lines
22 KiB
Swift
477 lines
22 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import TelegramPresentationData
|
|
import ItemListUI
|
|
import PresentationDataUtils
|
|
|
|
class FormEditableBlockItemNode<Item: FormControllerItem>: ASDisplayNode, FormControllerItemNode, FormBlockItemNodeProto, UIGestureRecognizerDelegate {
|
|
private let topSeparatorInset: FormBlockItemInset
|
|
|
|
private let highlightedBackgroundNode: ASDisplayNode
|
|
let backgroundNode: ASDisplayNode
|
|
private let topSeparatorNode: ASDisplayNode
|
|
private let bottomSeparatorNode: ASDisplayNode
|
|
private let selectionButtonNode: HighlightTrackingButtonNode
|
|
|
|
private var leftRevealNode: ItemListRevealOptionsNode?
|
|
private var rightRevealNode: ItemListRevealOptionsNode?
|
|
private var revealOptions: (left: [ItemListRevealOption], right: [ItemListRevealOption]) = ([], [])
|
|
|
|
private var initialRevealOffset: CGFloat = 0.0
|
|
public private(set) var revealOffset: CGFloat = 0.0
|
|
|
|
private var recognizer: ItemListRevealOptionsGestureRecognizer?
|
|
private var hapticFeedback: HapticFeedback?
|
|
|
|
private var allowAnyDirection = false
|
|
|
|
private var validLayout: (CGSize, CGFloat, CGFloat)?
|
|
|
|
var isDisplayingRevealedOptions: Bool {
|
|
return !self.revealOffset.isZero
|
|
}
|
|
|
|
var canBeSelected: Bool {
|
|
return !self.isDisplayingRevealedOptions
|
|
}
|
|
|
|
init(selectable: Bool, topSeparatorInset: FormBlockItemInset) {
|
|
self.topSeparatorInset = topSeparatorInset
|
|
|
|
self.backgroundNode = ASDisplayNode()
|
|
self.backgroundNode.isLayerBacked = true
|
|
self.topSeparatorNode = ASDisplayNode()
|
|
self.topSeparatorNode.isLayerBacked = true
|
|
self.bottomSeparatorNode = ASDisplayNode()
|
|
self.bottomSeparatorNode.isLayerBacked = true
|
|
self.highlightedBackgroundNode = ASDisplayNode()
|
|
self.highlightedBackgroundNode.isLayerBacked = true
|
|
self.highlightedBackgroundNode.alpha = 0.0
|
|
|
|
self.selectionButtonNode = HighlightTrackingButtonNode()
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.backgroundNode)
|
|
self.addSubnode(self.topSeparatorNode)
|
|
self.addSubnode(self.bottomSeparatorNode)
|
|
self.addSubnode(self.highlightedBackgroundNode)
|
|
|
|
if selectable {
|
|
self.selectionButtonNode.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self, strongSelf.canBeSelected {
|
|
if highlighted {
|
|
strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.highlightedBackgroundNode.alpha = 1.0
|
|
} else {
|
|
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
|
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
|
}
|
|
}
|
|
}
|
|
self.addSubnode(self.selectionButtonNode)
|
|
self.selectionButtonNode.addTarget(self, action: #selector(self.selectionButtonPressed), forControlEvents: .touchUpInside)
|
|
}
|
|
|
|
let recognizer = ItemListRevealOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:)))
|
|
self.recognizer = recognizer
|
|
recognizer.allowAnyDirection = self.allowAnyDirection
|
|
self.view.addGestureRecognizer(recognizer)
|
|
}
|
|
|
|
func setRevealOptions(_ options: (left: [ItemListRevealOption], right: [ItemListRevealOption])) {
|
|
if self.revealOptions == options {
|
|
return
|
|
}
|
|
let previousOptions = self.revealOptions
|
|
let wasEmpty = self.revealOptions.left.isEmpty && self.revealOptions.right.isEmpty
|
|
self.revealOptions = options
|
|
let isEmpty = options.left.isEmpty && options.right.isEmpty
|
|
if options.left.isEmpty {
|
|
if let _ = self.leftRevealNode {
|
|
self.recognizer?.becomeCancelled()
|
|
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
|
|
}
|
|
} else if previousOptions.left != options.left {
|
|
/*if let _ = self.leftRevealNode {
|
|
self.revealOptionsInteractivelyClosed()
|
|
self.recognizer?.becomeCancelled()
|
|
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
|
|
}*/
|
|
}
|
|
if options.right.isEmpty {
|
|
if let _ = self.rightRevealNode {
|
|
self.recognizer?.becomeCancelled()
|
|
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
|
|
}
|
|
} else if previousOptions.right != options.right {
|
|
if let _ = self.rightRevealNode {
|
|
/*self.revealOptionsInteractivelyClosed()
|
|
self.recognizer?.becomeCancelled()
|
|
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))*/
|
|
}
|
|
}
|
|
if wasEmpty != isEmpty {
|
|
self.recognizer?.isEnabled = !isEmpty
|
|
}
|
|
let allowAnyDirection = !options.left.isEmpty || !self.revealOffset.isZero
|
|
if allowAnyDirection != self.allowAnyDirection {
|
|
self.allowAnyDirection = allowAnyDirection
|
|
self.recognizer?.allowAnyDirection = allowAnyDirection
|
|
if self.isNodeLoaded {
|
|
self.view.disablesInteractiveTransitionGestureRecognizer = allowAnyDirection
|
|
}
|
|
}
|
|
}
|
|
|
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
return true
|
|
}
|
|
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
if let recognizer = self.recognizer, otherGestureRecognizer == recognizer {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
@objc func revealGesture(_ recognizer: ItemListRevealOptionsGestureRecognizer) {
|
|
guard let (size, _, _) = self.validLayout else {
|
|
return
|
|
}
|
|
switch recognizer.state {
|
|
case .began:
|
|
if let leftRevealNode = self.leftRevealNode {
|
|
let revealSize = leftRevealNode.bounds.size
|
|
let location = recognizer.location(in: self.view)
|
|
if location.x < revealSize.width {
|
|
recognizer.becomeCancelled()
|
|
} else {
|
|
self.initialRevealOffset = self.revealOffset
|
|
}
|
|
} else if let rightRevealNode = self.rightRevealNode {
|
|
let revealSize = rightRevealNode.bounds.size
|
|
let location = recognizer.location(in: self.view)
|
|
if location.x > size.width - revealSize.width {
|
|
recognizer.becomeCancelled()
|
|
} else {
|
|
self.initialRevealOffset = self.revealOffset
|
|
}
|
|
} else {
|
|
if self.revealOptions.left.isEmpty && self.revealOptions.right.isEmpty {
|
|
recognizer.becomeCancelled()
|
|
}
|
|
self.initialRevealOffset = self.revealOffset
|
|
}
|
|
case .changed:
|
|
var translation = recognizer.translation(in: self.view)
|
|
translation.x += self.initialRevealOffset
|
|
if self.revealOptions.left.isEmpty {
|
|
translation.x = min(0.0, translation.x)
|
|
}
|
|
if self.leftRevealNode == nil && CGFloat(0.0).isLess(than: translation.x) {
|
|
self.setupAndAddLeftRevealNode()
|
|
self.revealOptionsInteractivelyOpened()
|
|
} else if self.rightRevealNode == nil && translation.x.isLess(than: 0.0) {
|
|
self.setupAndAddRightRevealNode()
|
|
self.revealOptionsInteractivelyOpened()
|
|
}
|
|
self.updateRevealOffsetInternal(offset: translation.x, transition: .immediate)
|
|
if self.leftRevealNode == nil && self.rightRevealNode == nil {
|
|
self.revealOptionsInteractivelyClosed()
|
|
}
|
|
case .ended, .cancelled:
|
|
guard let recognizer = self.recognizer else {
|
|
break
|
|
}
|
|
|
|
if let leftRevealNode = self.leftRevealNode {
|
|
let velocity = recognizer.velocity(in: self.view)
|
|
let revealSize = leftRevealNode.bounds.size
|
|
var reveal = false
|
|
if abs(velocity.x) < 100.0 {
|
|
if self.initialRevealOffset.isZero && self.revealOffset > 0.0 {
|
|
reveal = true
|
|
} else if self.revealOffset > revealSize.width {
|
|
reveal = true
|
|
} else {
|
|
reveal = false
|
|
}
|
|
} else {
|
|
if velocity.x > 0.0 {
|
|
reveal = true
|
|
} else {
|
|
reveal = false
|
|
}
|
|
}
|
|
|
|
var selectedOption: ItemListRevealOption?
|
|
if reveal && leftRevealNode.isDisplayingExtendedAction() {
|
|
reveal = false
|
|
selectedOption = self.revealOptions.left.first
|
|
} else {
|
|
self.updateRevealOffsetInternal(offset: reveal ?revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring))
|
|
}
|
|
|
|
if let selectedOption = selectedOption {
|
|
self.revealOptionSelected(selectedOption, animated: true)
|
|
} else {
|
|
if !reveal {
|
|
self.revealOptionsInteractivelyClosed()
|
|
}
|
|
}
|
|
} else if let rightRevealNode = self.rightRevealNode {
|
|
let velocity = recognizer.velocity(in: self.view)
|
|
let revealSize = rightRevealNode.bounds.size
|
|
var reveal = false
|
|
if abs(velocity.x) < 100.0 {
|
|
if self.initialRevealOffset.isZero && self.revealOffset < 0.0 {
|
|
reveal = true
|
|
} else if self.revealOffset < -revealSize.width {
|
|
reveal = true
|
|
} else {
|
|
reveal = false
|
|
}
|
|
} else {
|
|
if velocity.x < 0.0 {
|
|
reveal = true
|
|
} else {
|
|
reveal = false
|
|
}
|
|
}
|
|
self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring))
|
|
if !reveal {
|
|
self.revealOptionsInteractivelyClosed()
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
private func setupAndAddLeftRevealNode() {
|
|
if !self.revealOptions.left.isEmpty {
|
|
let revealNode = ItemListRevealOptionsNode(optionSelected: { [weak self] option in
|
|
self?.revealOptionSelected(option, animated: false)
|
|
}, tapticAction: { [weak self] in
|
|
self?.hapticImpact()
|
|
})
|
|
revealNode.setOptions(self.revealOptions.left, isLeft: true, enableAnimations: true)
|
|
self.leftRevealNode = revealNode
|
|
|
|
if let (size, leftInset, _) = self.validLayout {
|
|
var revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height))
|
|
revealSize.width += leftInset
|
|
|
|
revealNode.frame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize)
|
|
revealNode.updateRevealOffset(offset: 0.0, sideInset: leftInset, transition: .immediate)
|
|
}
|
|
|
|
self.addSubnode(revealNode)
|
|
}
|
|
}
|
|
|
|
private func setupAndAddRightRevealNode() {
|
|
if !self.revealOptions.right.isEmpty {
|
|
let revealNode = ItemListRevealOptionsNode(optionSelected: { [weak self] option in
|
|
self?.revealOptionSelected(option, animated: false)
|
|
}, tapticAction: { [weak self] in
|
|
self?.hapticImpact()
|
|
})
|
|
revealNode.setOptions(self.revealOptions.right, isLeft: false, enableAnimations: true)
|
|
self.rightRevealNode = revealNode
|
|
|
|
if let (size, _, rightInset) = self.validLayout {
|
|
var revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height))
|
|
revealSize.width += rightInset
|
|
|
|
revealNode.frame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize)
|
|
revealNode.updateRevealOffset(offset: 0.0, sideInset: -rightInset, transition: .immediate)
|
|
}
|
|
|
|
self.addSubnode(revealNode)
|
|
}
|
|
}
|
|
|
|
final func updateInternal(item: Item, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
|
|
let (preLayout, apply) = self.update(item: item, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, width: width, previousNeighbor: previousNeighbor, nextNeighbor: nextNeighbor, transition: transition)
|
|
return (preLayout, { params in
|
|
self.backgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor
|
|
self.topSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
|
|
self.bottomSeparatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor
|
|
self.highlightedBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor
|
|
|
|
let height = apply(params)
|
|
|
|
let topSeparatorInset: CGFloat
|
|
switch previousNeighbor {
|
|
case let .item(item) where item is FormBlockItemNodeProto:
|
|
switch self.topSeparatorInset {
|
|
case .regular:
|
|
topSeparatorInset = 16.0
|
|
case let .custom(value):
|
|
topSeparatorInset = value
|
|
}
|
|
default:
|
|
topSeparatorInset = 0.0
|
|
}
|
|
|
|
switch nextNeighbor {
|
|
case let .item(item) where item is FormBlockItemNodeProto:
|
|
self.bottomSeparatorNode.isHidden = true
|
|
default:
|
|
self.bottomSeparatorNode.isHidden = false
|
|
}
|
|
|
|
if let leftRevealNode = self.leftRevealNode {
|
|
let revealSize = leftRevealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: height))
|
|
//revealSize.width += leftInset
|
|
leftRevealNode.frame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize)
|
|
}
|
|
|
|
if let rightRevealNode = self.rightRevealNode {
|
|
let revealSize = rightRevealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: height))
|
|
//revealSize.width += rightInset
|
|
rightRevealNode.frame = CGRect(origin: CGPoint(x: width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize)
|
|
}
|
|
|
|
self.validLayout = (CGSize(width: width, height: height), 0.0, 0.0)
|
|
|
|
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
|
|
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
|
|
transition.updateFrame(node: self.selectionButtonNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
|
|
transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(origin: CGPoint(x: topSeparatorInset, y: 0.0), size: CGSize(width: width - topSeparatorInset, height: UIScreenPixel)))
|
|
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel)))
|
|
|
|
return height
|
|
})
|
|
}
|
|
|
|
func updateRevealOffsetInternal(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
self.revealOffset = offset
|
|
guard let (size, leftInset, rightInset) = self.validLayout else {
|
|
return
|
|
}
|
|
|
|
if let leftRevealNode = self.leftRevealNode {
|
|
let revealSize = leftRevealNode.bounds.size
|
|
|
|
let revealFrame = CGRect(origin: CGPoint(x: min(self.revealOffset - revealSize.width, 0.0), y: 0.0), size: revealSize)
|
|
//let revealNodeOffset = max(-self.revealOffset, revealSize.width)
|
|
let revealNodeOffset = -self.revealOffset
|
|
leftRevealNode.updateRevealOffset(offset: revealNodeOffset, sideInset: leftInset, transition: transition)
|
|
|
|
if CGFloat(offset).isLessThanOrEqualTo(0.0) {
|
|
self.leftRevealNode = nil
|
|
transition.updateFrame(node: leftRevealNode, frame: revealFrame, completion: { [weak leftRevealNode] _ in
|
|
leftRevealNode?.removeFromSupernode()
|
|
})
|
|
} else {
|
|
transition.updateFrame(node: leftRevealNode, frame: revealFrame)
|
|
}
|
|
}
|
|
if let rightRevealNode = self.rightRevealNode {
|
|
let revealSize = rightRevealNode.bounds.size
|
|
|
|
let revealFrame = CGRect(origin: CGPoint(x: size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize)
|
|
let revealNodeOffset = -max(self.revealOffset, -revealSize.width)
|
|
rightRevealNode.updateRevealOffset(offset: revealNodeOffset, sideInset: -rightInset, transition: transition)
|
|
|
|
if CGFloat(0.0).isLessThanOrEqualTo(offset) {
|
|
self.rightRevealNode = nil
|
|
transition.updateFrame(node: rightRevealNode, frame: revealFrame, completion: { [weak rightRevealNode] _ in
|
|
rightRevealNode?.removeFromSupernode()
|
|
})
|
|
} else {
|
|
transition.updateFrame(node: rightRevealNode, frame: revealFrame)
|
|
}
|
|
}
|
|
let allowAnyDirection = !self.revealOptions.left.isEmpty || !offset.isZero
|
|
if allowAnyDirection != self.allowAnyDirection {
|
|
self.allowAnyDirection = allowAnyDirection
|
|
self.recognizer?.allowAnyDirection = allowAnyDirection
|
|
self.view.disablesInteractiveTransitionGestureRecognizer = allowAnyDirection
|
|
}
|
|
|
|
self.updateRevealOffset(offset: offset, transition: transition)
|
|
}
|
|
|
|
func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
|
|
}
|
|
|
|
func revealOptionsInteractivelyOpened() {
|
|
|
|
}
|
|
|
|
func revealOptionsInteractivelyClosed() {
|
|
|
|
}
|
|
|
|
func setRevealOptionsOpened(_ value: Bool, animated: Bool) {
|
|
if value != !self.revealOffset.isZero {
|
|
if !self.revealOffset.isZero {
|
|
self.recognizer?.becomeCancelled()
|
|
}
|
|
let transition: ContainedViewLayoutTransition
|
|
if animated {
|
|
transition = .animated(duration: 0.3, curve: .spring)
|
|
} else {
|
|
transition = .immediate
|
|
}
|
|
if value {
|
|
if self.rightRevealNode == nil {
|
|
self.setupAndAddRightRevealNode()
|
|
if let rightRevealNode = self.rightRevealNode, rightRevealNode.isNodeLoaded, let _ = self.validLayout {
|
|
rightRevealNode.layout()
|
|
let revealSize = rightRevealNode.bounds.size
|
|
self.updateRevealOffsetInternal(offset: -revealSize.width, transition: transition)
|
|
}
|
|
}
|
|
} else if !self.revealOffset.isZero {
|
|
self.updateRevealOffsetInternal(offset: 0.0, transition: transition)
|
|
}
|
|
}
|
|
}
|
|
|
|
func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
|
|
}
|
|
|
|
private func hapticImpact() {
|
|
if self.hapticFeedback == nil {
|
|
self.hapticFeedback = HapticFeedback()
|
|
}
|
|
self.hapticFeedback?.impact(.medium)
|
|
}
|
|
|
|
func update(item: Item, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, width: CGFloat, previousNeighbor: FormControllerItemNeighbor, nextNeighbor: FormControllerItemNeighbor, transition: ContainedViewLayoutTransition) -> (FormControllerItemPreLayout, (FormControllerItemLayoutParams) -> CGFloat) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
@objc private func selectionButtonPressed() {
|
|
if self.canBeSelected {
|
|
self.selected()
|
|
} else {
|
|
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
|
|
self.revealOptionsInteractivelyClosed()
|
|
}
|
|
}
|
|
|
|
func selected() {
|
|
}
|
|
|
|
var preventsTouchesToOtherItems: Bool {
|
|
return self.isDisplayingRevealedOptions
|
|
}
|
|
|
|
func touchesToOtherItemsPrevented() {
|
|
if self.isDisplayingRevealedOptions {
|
|
self.setRevealOptionsOpened(false, animated: true)
|
|
}
|
|
}
|
|
}
|
|
|