mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1075 lines
54 KiB
Swift
1075 lines
54 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import Postbox
|
|
import TelegramCore
|
|
import TelegramPresentationData
|
|
import SGSimpleSettings
|
|
import AccountContext
|
|
import TextNodeWithEntities
|
|
|
|
private final class ItemNodeDeleteButtonNode: HighlightableButtonNode {
|
|
private let pressed: () -> Void
|
|
|
|
private let contentImageNode: ASImageNode
|
|
|
|
private var theme: PresentationTheme?
|
|
|
|
init(pressed: @escaping () -> Void) {
|
|
self.pressed = pressed
|
|
|
|
self.contentImageNode = ASImageNode()
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.contentImageNode)
|
|
|
|
self.addTarget(self, action: #selector(self.pressedEvent), forControlEvents: .touchUpInside)
|
|
}
|
|
|
|
@objc private func pressedEvent() {
|
|
self.pressed()
|
|
}
|
|
|
|
func update(theme: PresentationTheme) -> CGSize {
|
|
let size = CGSize(width: 18.0, height: 18.0)
|
|
if self.theme !== theme {
|
|
self.theme = theme
|
|
self.contentImageNode.image = generateImage(size, rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
context.setFillColor(UIColor(rgb: 0xbbbbbb).cgColor)
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
|
context.setStrokeColor(UIColor(rgb: 0xffffff).cgColor)
|
|
context.setLineWidth(1.5)
|
|
context.setLineCap(.round)
|
|
context.move(to: CGPoint(x: 6.38, y: 6.38))
|
|
context.addLine(to: CGPoint(x: 11.63, y: 11.63))
|
|
context.strokePath()
|
|
context.move(to: CGPoint(x: 6.38, y: 11.63))
|
|
context.addLine(to: CGPoint(x: 11.63, y: 6.38))
|
|
context.strokePath()
|
|
})
|
|
}
|
|
|
|
self.contentImageNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
private final class ItemNode: ASDisplayNode {
|
|
private let context: AccountContext
|
|
private let pressed: () -> Void
|
|
private let requestedDeletion: () -> Void
|
|
|
|
private let extractedContainerNode: ContextExtractedContentContainingNode
|
|
private let containerNode: ContextControllerSourceNode
|
|
|
|
private let extractedBackgroundNode: ASImageNode
|
|
private let titleNode: ImmediateTextNodeWithEntities
|
|
private let shortTitleNode: ImmediateTextNodeWithEntities
|
|
private let badgeContainerNode: ASDisplayNode
|
|
private let badgeTextNode: ImmediateTextNode
|
|
private let badgeBackgroundActiveNode: ASImageNode
|
|
private let badgeBackgroundInactiveNode: ASImageNode
|
|
|
|
private var deleteButtonNode: ItemNodeDeleteButtonNode?
|
|
private let buttonNode: HighlightTrackingButtonNode
|
|
|
|
private var isSelected: Bool = false
|
|
private(set) var unreadCount: Int = 0
|
|
|
|
private var isReordering: Bool = false
|
|
|
|
private var theme: PresentationTheme?
|
|
private var currentTitle: (ChatFolderTitle, ChatFolderTitle)?
|
|
|
|
init(context: AccountContext, pressed: @escaping () -> Void, requestedDeletion: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture) -> Void) {
|
|
self.context = context
|
|
self.pressed = pressed
|
|
self.requestedDeletion = requestedDeletion
|
|
|
|
self.extractedContainerNode = ContextExtractedContentContainingNode()
|
|
self.containerNode = ContextControllerSourceNode()
|
|
|
|
self.extractedBackgroundNode = ASImageNode()
|
|
self.extractedBackgroundNode.alpha = 0.0
|
|
|
|
let titleInset: CGFloat = 4.0
|
|
|
|
self.titleNode = ImmediateTextNodeWithEntities()
|
|
self.titleNode.displaysAsynchronously = false
|
|
self.titleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
|
|
self.titleNode.resetEmojiToFirstFrameAutomatically = true
|
|
|
|
self.shortTitleNode = ImmediateTextNodeWithEntities()
|
|
self.shortTitleNode.displaysAsynchronously = false
|
|
self.shortTitleNode.alpha = 0.0
|
|
self.shortTitleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
|
|
self.shortTitleNode.resetEmojiToFirstFrameAutomatically = true
|
|
|
|
self.badgeContainerNode = ASDisplayNode()
|
|
|
|
self.badgeTextNode = ImmediateTextNode()
|
|
self.badgeTextNode.displaysAsynchronously = false
|
|
|
|
self.badgeBackgroundActiveNode = ASImageNode()
|
|
self.badgeBackgroundActiveNode.displaysAsynchronously = false
|
|
self.badgeBackgroundActiveNode.displayWithoutProcessing = true
|
|
|
|
self.badgeBackgroundInactiveNode = ASImageNode()
|
|
self.badgeBackgroundInactiveNode.displaysAsynchronously = false
|
|
self.badgeBackgroundInactiveNode.displayWithoutProcessing = true
|
|
self.badgeBackgroundInactiveNode.isHidden = true
|
|
|
|
self.buttonNode = HighlightTrackingButtonNode()
|
|
|
|
super.init()
|
|
|
|
self.extractedContainerNode.contentNode.addSubnode(self.extractedBackgroundNode)
|
|
self.extractedContainerNode.contentNode.addSubnode(self.titleNode)
|
|
self.extractedContainerNode.contentNode.addSubnode(self.shortTitleNode)
|
|
self.badgeContainerNode.addSubnode(self.badgeBackgroundActiveNode)
|
|
self.badgeContainerNode.addSubnode(self.badgeBackgroundInactiveNode)
|
|
self.badgeContainerNode.addSubnode(self.badgeTextNode)
|
|
self.extractedContainerNode.contentNode.addSubnode(self.badgeContainerNode)
|
|
self.extractedContainerNode.contentNode.addSubnode(self.buttonNode)
|
|
|
|
self.containerNode.addSubnode(self.extractedContainerNode)
|
|
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
|
|
self.addSubnode(self.containerNode)
|
|
|
|
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
|
|
|
self.containerNode.activated = { [weak self] gesture, _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
contextGesture(strongSelf.extractedContainerNode, gesture)
|
|
}
|
|
|
|
self.extractedContainerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
if isExtracted {
|
|
strongSelf.extractedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 32.0, color: strongSelf.isSelected ? UIColor(rgb: 0xbbbbbb) : UIColor(rgb: 0xf1f1f1))
|
|
}
|
|
transition.updateAlpha(node: strongSelf.extractedBackgroundNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
|
|
if !isExtracted {
|
|
self?.extractedBackgroundNode.image = nil
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
@objc private func buttonPressed() {
|
|
self.pressed()
|
|
}
|
|
|
|
func updateText(title: ChatFolderTitle, shortTitle: ChatFolderTitle, unreadCount: Int, unreadHasUnmuted: Bool, isNoFilter: Bool, isSelected: Bool, isEditing: Bool, isAllChats: Bool, isReordering: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
|
|
|
|
var themeUpdated = false
|
|
if self.theme !== presentationData.theme {
|
|
self.theme = presentationData.theme
|
|
|
|
self.badgeBackgroundActiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chatList.unreadBadgeActiveBackgroundColor)
|
|
self.badgeBackgroundInactiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chatList.unreadBadgeInactiveBackgroundColor)
|
|
|
|
themeUpdated = true
|
|
}
|
|
// MARK: Swiftgram
|
|
var titleUpdated = false
|
|
if self.currentTitle?.0 != title || self.currentTitle?.1 != shortTitle {
|
|
self.currentTitle = (title, shortTitle)
|
|
|
|
titleUpdated = true
|
|
}
|
|
//
|
|
|
|
self.containerNode.isGestureEnabled = !isEditing && !isReordering
|
|
self.buttonNode.isUserInteractionEnabled = !isEditing && !isReordering
|
|
|
|
self.isSelected = isSelected
|
|
self.unreadCount = unreadCount
|
|
|
|
transition.updateAlpha(node: self.containerNode, alpha: isReordering && isAllChats ? 0.5 : 1.0)
|
|
|
|
if isReordering && !isAllChats {
|
|
if self.deleteButtonNode == nil {
|
|
let deleteButtonNode = ItemNodeDeleteButtonNode(pressed: { [weak self] in
|
|
self?.requestedDeletion()
|
|
})
|
|
self.extractedContainerNode.contentNode.addSubnode(deleteButtonNode)
|
|
self.deleteButtonNode = deleteButtonNode
|
|
if case .animated = transition {
|
|
deleteButtonNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.25)
|
|
deleteButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
|
}
|
|
}
|
|
} else if let deleteButtonNode = self.deleteButtonNode {
|
|
self.deleteButtonNode = nil
|
|
transition.updateTransformScale(node: deleteButtonNode, scale: 0.1)
|
|
transition.updateAlpha(node: deleteButtonNode, alpha: 0.0, completion: { [weak deleteButtonNode] _ in
|
|
deleteButtonNode?.removeFromSupernode()
|
|
})
|
|
}
|
|
|
|
transition.updateAlpha(node: self.badgeContainerNode, alpha: (isReordering || unreadCount == 0) ? 0.0 : 1.0)
|
|
// MARK: Swiftgram
|
|
let titleArguments = TextNodeWithEntities.Arguments(
|
|
context: self.context,
|
|
cache: self.context.animationCache,
|
|
renderer: self.context.animationRenderer,
|
|
placeholderColor: presentationData.theme.list.mediaPlaceholderColor,
|
|
attemptSynchronous: false
|
|
)
|
|
|
|
self.titleNode.arguments = titleArguments
|
|
self.shortTitleNode.arguments = titleArguments
|
|
|
|
self.titleNode.visibility = title.enableAnimations
|
|
self.shortTitleNode.visibility = title.enableAnimations
|
|
|
|
if themeUpdated || titleUpdated {
|
|
self.titleNode.attributedText = title.attributedString(font: Font.bold(17.0), textColor: isSelected ? presentationData.theme.contextMenu.badgeForegroundColor : presentationData.theme.list.itemSecondaryTextColor)
|
|
self.shortTitleNode.attributedText = shortTitle.attributedString(font: Font.bold(17.0), textColor: isSelected ? presentationData.theme.contextMenu.badgeForegroundColor : presentationData.theme.list.itemSecondaryTextColor)
|
|
|
|
}
|
|
//
|
|
|
|
if unreadCount != 0 {
|
|
self.badgeTextNode.attributedText = NSAttributedString(string: "\(unreadCount)", font: Font.regular(14.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor)
|
|
self.badgeBackgroundActiveNode.isHidden = !isSelected && !unreadHasUnmuted
|
|
self.badgeBackgroundInactiveNode.isHidden = isSelected || unreadHasUnmuted
|
|
}
|
|
|
|
if self.isReordering != isReordering {
|
|
self.isReordering = isReordering
|
|
if self.isReordering && !isAllChats {
|
|
self.startShaking()
|
|
} else {
|
|
self.layer.removeAnimation(forKey: "shaking_position")
|
|
self.layer.removeAnimation(forKey: "shaking_rotation")
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateLayout(height: CGFloat, transition: ContainedViewLayoutTransition) -> (width: CGFloat, shortWidth: CGFloat) {
|
|
let titleSize = self.titleNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude))
|
|
self.titleNode.frame = CGRect(origin: CGPoint(x: -self.titleNode.insets.left, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
|
|
|
|
let shortTitleSize = self.shortTitleNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude))
|
|
self.shortTitleNode.frame = CGRect(origin: CGPoint(x: -self.shortTitleNode.insets.left, y: floor((height - shortTitleSize.height) / 2.0)), size: shortTitleSize)
|
|
|
|
if let deleteButtonNode = self.deleteButtonNode {
|
|
if let theme = self.theme {
|
|
let deleteButtonSize = deleteButtonNode.update(theme: theme)
|
|
deleteButtonNode.frame = CGRect(origin: CGPoint(x: -deleteButtonSize.width + 7.0, y: 5.0), size: deleteButtonSize)
|
|
}
|
|
}
|
|
|
|
let badgeSize = self.badgeTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
|
|
let badgeInset: CGFloat = 4.0
|
|
let badgeBackgroundFrame = CGRect(origin: CGPoint(x: titleSize.width - self.titleNode.insets.left - self.titleNode.insets.right + 5.0, y: floor((height - 18.0) / 2.0)), size: CGSize(width: max(18.0, badgeSize.width + badgeInset * 2.0), height: 18.0))
|
|
self.badgeContainerNode.frame = badgeBackgroundFrame
|
|
self.badgeBackgroundActiveNode.frame = CGRect(origin: CGPoint(), size: badgeBackgroundFrame.size)
|
|
self.badgeBackgroundInactiveNode.frame = CGRect(origin: CGPoint(), size: badgeBackgroundFrame.size)
|
|
self.badgeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((badgeBackgroundFrame.width - badgeSize.width) / 2.0), y: floor((badgeBackgroundFrame.height - badgeSize.height) / 2.0)), size: badgeSize)
|
|
|
|
let width: CGFloat
|
|
if self.unreadCount == 0 || self.isReordering {
|
|
if !self.isReordering {
|
|
self.badgeContainerNode.alpha = 0.0
|
|
}
|
|
width = titleSize.width - self.titleNode.insets.left - self.titleNode.insets.right
|
|
} else {
|
|
if !self.isReordering {
|
|
self.badgeContainerNode.alpha = 1.0
|
|
}
|
|
width = badgeBackgroundFrame.maxX
|
|
}
|
|
|
|
return (width, shortTitleSize.width - self.shortTitleNode.insets.left - self.shortTitleNode.insets.right)
|
|
}
|
|
|
|
func updateArea(size: CGSize, sideInset: CGFloat, useShortTitle: Bool, transition: ContainedViewLayoutTransition) {
|
|
transition.updateAlpha(node: self.titleNode, alpha: useShortTitle ? 0.0 : 1.0)
|
|
transition.updateAlpha(node: self.shortTitleNode, alpha: useShortTitle ? 1.0 : 0.0)
|
|
|
|
self.buttonNode.frame = CGRect(origin: CGPoint(x: -sideInset, y: 0.0), size: CGSize(width: size.width + sideInset * 2.0, height: size.height))
|
|
|
|
self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: self.extractedBackgroundNode.frame.minX, y: 0.0), size: CGSize(width: self.extractedBackgroundNode.frame.width, height: size.height))
|
|
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
|
|
self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -sideInset, bottom: 0.0, right: -sideInset)
|
|
self.extractedContainerNode.hitTestSlop = self.hitTestSlop
|
|
self.extractedContainerNode.contentNode.hitTestSlop = self.hitTestSlop
|
|
self.containerNode.hitTestSlop = self.hitTestSlop
|
|
|
|
let extractedBackgroundHeight: CGFloat = 32.0
|
|
let extractedBackgroundInset: CGFloat = 14.0
|
|
self.extractedBackgroundNode.frame = CGRect(origin: CGPoint(x: -extractedBackgroundInset, y: floor((size.height - extractedBackgroundHeight) / 2.0)), size: CGSize(width: size.width + extractedBackgroundInset * 2.0, height: extractedBackgroundHeight))
|
|
}
|
|
|
|
func animateBadgeIn() {
|
|
if !self.isReordering {
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
|
self.badgeContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
|
ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 0.1)
|
|
transition.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 1.0)
|
|
}
|
|
}
|
|
|
|
func animateBadgeOut() {
|
|
if !self.isReordering {
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
|
self.badgeContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
|
|
ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 1.0)
|
|
transition.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 0.1)
|
|
}
|
|
}
|
|
|
|
private func startShaking() {
|
|
func degreesToRadians(_ x: CGFloat) -> CGFloat {
|
|
return .pi * x / 180.0
|
|
}
|
|
|
|
let duration: Double = 0.4
|
|
let displacement: CGFloat = 1.0
|
|
let degreesRotation: CGFloat = 2.0
|
|
|
|
let negativeDisplacement = -1.0 * displacement
|
|
let position = CAKeyframeAnimation.init(keyPath: "position")
|
|
position.beginTime = 0.8
|
|
position.duration = duration
|
|
position.values = [
|
|
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)),
|
|
NSValue(cgPoint: CGPoint(x: 0, y: 0)),
|
|
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)),
|
|
NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)),
|
|
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement))
|
|
]
|
|
position.calculationMode = .linear
|
|
position.isRemovedOnCompletion = false
|
|
position.repeatCount = Float.greatestFiniteMagnitude
|
|
position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
|
|
position.isAdditive = true
|
|
|
|
let transform = CAKeyframeAnimation.init(keyPath: "transform")
|
|
transform.beginTime = 2.6
|
|
transform.duration = 0.3
|
|
transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ)
|
|
transform.values = [
|
|
degreesToRadians(-1.0 * degreesRotation),
|
|
degreesToRadians(degreesRotation),
|
|
degreesToRadians(-1.0 * degreesRotation)
|
|
]
|
|
transform.calculationMode = .linear
|
|
transform.isRemovedOnCompletion = false
|
|
transform.repeatCount = Float.greatestFiniteMagnitude
|
|
transform.isAdditive = true
|
|
transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
|
|
|
|
self.layer.add(position, forKey: "shaking_position")
|
|
self.layer.add(transform, forKey: "shaking_rotation")
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if let deleteButtonNode = self.deleteButtonNode {
|
|
if deleteButtonNode.frame.insetBy(dx: -4.0, dy: -4.0).contains(point) {
|
|
return deleteButtonNode.view
|
|
}
|
|
}
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
}
|
|
|
|
private final class ItemNodePair {
|
|
let regular: ItemNode
|
|
let highlighted: ItemNode
|
|
|
|
init(regular: ItemNode, highlighted: ItemNode) {
|
|
self.regular = regular
|
|
self.highlighted = highlighted
|
|
}
|
|
}
|
|
|
|
public final class AppleStyleFoldersNode: ASDisplayNode {
|
|
private let context: AccountContext
|
|
private let scrollNode: ASScrollNode
|
|
private let itemsBackgroundView: UIVisualEffectView
|
|
private let itemsBackgroundTintNode: ASImageNode
|
|
|
|
private let selectedBackgroundNode: ASImageNode
|
|
private var itemNodePairs: [ChatListFilterTabEntryId: ItemNodePair] = [:]
|
|
private var itemsContainer: ASDisplayNode
|
|
private var highlightedItemsClippingContainer: ASDisplayNode
|
|
private var highlightedItemsContainer: ASDisplayNode
|
|
|
|
var tabSelected: ((ChatListFilterTabEntryId, Bool) -> Void)?
|
|
var tabRequestedDeletion: ((ChatListFilterTabEntryId) -> Void)?
|
|
var addFilter: (() -> Void)?
|
|
var contextGesture: ((Int32?, ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void)?
|
|
|
|
private var reorderingGesture: ReorderingGestureRecognizer?
|
|
private var reorderingItem: ChatListFilterTabEntryId?
|
|
private var reorderingItemPosition: (initial: CGFloat, offset: CGFloat)?
|
|
private var reorderingAutoScrollAnimator: ConstantDisplayLinkAnimator?
|
|
private var reorderedItemIds: [ChatListFilterTabEntryId]?
|
|
private lazy var hapticFeedback = { HapticFeedback() }()
|
|
|
|
private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, transitionFraction: CGFloat, presentationData: PresentationData)?
|
|
|
|
var reorderedFilterIds: [Int32]? {
|
|
return self.reorderedItemIds.flatMap {
|
|
$0.compactMap {
|
|
switch $0 {
|
|
case .all:
|
|
return 0
|
|
case let .filter(id):
|
|
return id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
init(context: AccountContext) {
|
|
self.context = context
|
|
self.scrollNode = ASScrollNode()
|
|
|
|
self.itemsBackgroundView = UIVisualEffectView()
|
|
self.itemsBackgroundView.clipsToBounds = true
|
|
self.itemsBackgroundView.layer.cornerRadius = 20.0
|
|
|
|
self.itemsBackgroundTintNode = ASImageNode()
|
|
self.itemsBackgroundTintNode.displaysAsynchronously = false
|
|
self.itemsBackgroundTintNode.displayWithoutProcessing = true
|
|
|
|
self.selectedBackgroundNode = ASImageNode()
|
|
self.selectedBackgroundNode.displaysAsynchronously = false
|
|
self.selectedBackgroundNode.displayWithoutProcessing = true
|
|
|
|
self.itemsContainer = ASDisplayNode()
|
|
|
|
self.highlightedItemsClippingContainer = ASDisplayNode()
|
|
self.highlightedItemsClippingContainer.clipsToBounds = true
|
|
self.highlightedItemsClippingContainer.layer.cornerRadius = 16.0
|
|
|
|
self.highlightedItemsContainer = ASDisplayNode()
|
|
|
|
super.init()
|
|
|
|
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
|
self.scrollNode.view.scrollsToTop = false
|
|
self.scrollNode.view.delaysContentTouches = false
|
|
self.scrollNode.view.canCancelContentTouches = true
|
|
if #available(iOS 11.0, *) {
|
|
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
|
}
|
|
|
|
self.addSubnode(self.scrollNode)
|
|
self.scrollNode.view.addSubview(self.itemsBackgroundView)
|
|
self.scrollNode.addSubnode(self.itemsBackgroundTintNode)
|
|
self.scrollNode.addSubnode(self.itemsContainer)
|
|
self.scrollNode.addSubnode(self.selectedBackgroundNode)
|
|
self.scrollNode.addSubnode(self.highlightedItemsClippingContainer)
|
|
self.highlightedItemsClippingContainer.addSubnode(self.highlightedItemsContainer)
|
|
|
|
let reorderingGesture = ReorderingGestureRecognizer(shouldBegin: { [weak self] point in
|
|
guard let strongSelf = self else {
|
|
return false
|
|
}
|
|
for (id, itemNodePair) in strongSelf.itemNodePairs {
|
|
if itemNodePair.regular.view.convert(itemNodePair.regular.bounds, to: strongSelf.view).contains(point) {
|
|
if case .all = id {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}, began: { [weak self] point in
|
|
guard let strongSelf = self, let _ = strongSelf.currentParams else {
|
|
return
|
|
}
|
|
for (id, itemNodePair) in strongSelf.itemNodePairs {
|
|
let itemFrame = itemNodePair.regular.view.convert(itemNodePair.regular.bounds, to: strongSelf.view)
|
|
if itemFrame.contains(point) {
|
|
strongSelf.hapticFeedback.impact()
|
|
|
|
strongSelf.reorderingItem = id
|
|
itemNodePair.regular.frame = itemFrame
|
|
strongSelf.reorderingAutoScrollAnimator = ConstantDisplayLinkAnimator(update: {
|
|
guard let strongSelf = self, let currentLocation = strongSelf.reorderingGesture?.currentLocation else {
|
|
return
|
|
}
|
|
let edgeWidth: CGFloat = 20.0
|
|
if currentLocation.x <= edgeWidth {
|
|
var contentOffset = strongSelf.scrollNode.view.contentOffset
|
|
contentOffset.x = max(0.0, contentOffset.x - 3.0)
|
|
strongSelf.scrollNode.view.setContentOffset(contentOffset, animated: false)
|
|
} else if currentLocation.x >= strongSelf.bounds.width - edgeWidth {
|
|
var contentOffset = strongSelf.scrollNode.view.contentOffset
|
|
contentOffset.x = max(0.0, min(strongSelf.scrollNode.view.contentSize.width - strongSelf.scrollNode.bounds.width, contentOffset.x + 3.0))
|
|
strongSelf.scrollNode.view.setContentOffset(contentOffset, animated: false)
|
|
}
|
|
})
|
|
strongSelf.reorderingAutoScrollAnimator?.isPaused = false
|
|
strongSelf.addSubnode(itemNodePair.regular)
|
|
|
|
strongSelf.reorderingItemPosition = (itemNodePair.regular.frame.minX, 0.0)
|
|
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams {
|
|
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut))
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}, ended: { [weak self] in
|
|
guard let strongSelf = self, let reorderingItem = strongSelf.reorderingItem else {
|
|
return
|
|
}
|
|
if let itemNodePair = strongSelf.itemNodePairs[reorderingItem] {
|
|
let projectedItemFrame = itemNodePair.regular.view.convert(itemNodePair.regular.bounds, to: strongSelf.scrollNode.view)
|
|
itemNodePair.regular.frame = projectedItemFrame
|
|
strongSelf.itemsContainer.addSubnode(itemNodePair.regular)
|
|
}
|
|
|
|
strongSelf.reorderingItem = nil
|
|
strongSelf.reorderingItemPosition = nil
|
|
strongSelf.reorderingAutoScrollAnimator?.invalidate()
|
|
strongSelf.reorderingAutoScrollAnimator = nil
|
|
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams {
|
|
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut))
|
|
}
|
|
}, moved: { [weak self] offset in
|
|
guard let strongSelf = self, let reorderingItem = strongSelf.reorderingItem else {
|
|
return
|
|
}
|
|
if let reorderingItemNodePair = strongSelf.itemNodePairs[reorderingItem], let (initial, _) = strongSelf.reorderingItemPosition, let reorderedItemIds = strongSelf.reorderedItemIds, let currentItemIndex = reorderedItemIds.firstIndex(of: reorderingItem) {
|
|
for (id, itemNodePair) in strongSelf.itemNodePairs {
|
|
guard let itemIndex = reorderedItemIds.firstIndex(of: id) else {
|
|
continue
|
|
}
|
|
if id != reorderingItem {
|
|
let itemFrame = itemNodePair.regular.view.convert(itemNodePair.regular.bounds, to: strongSelf.view)
|
|
if reorderingItemNodePair.regular.frame.intersects(itemFrame) {
|
|
let targetIndex: Int
|
|
if reorderingItemNodePair.regular.frame.midX < itemFrame.midX {
|
|
targetIndex = max(1, itemIndex - 1)
|
|
} else {
|
|
targetIndex = max(1, min(reorderedItemIds.count - 1, itemIndex))
|
|
}
|
|
if targetIndex != currentItemIndex {
|
|
strongSelf.hapticFeedback.tap()
|
|
|
|
var updatedReorderedItemIds = reorderedItemIds
|
|
if targetIndex > currentItemIndex {
|
|
updatedReorderedItemIds.insert(reorderingItem, at: targetIndex + 1)
|
|
updatedReorderedItemIds.remove(at: currentItemIndex)
|
|
} else {
|
|
updatedReorderedItemIds.remove(at: currentItemIndex)
|
|
updatedReorderedItemIds.insert(reorderingItem, at: targetIndex)
|
|
}
|
|
strongSelf.reorderedItemIds = updatedReorderedItemIds
|
|
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams {
|
|
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut))
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
strongSelf.reorderingItemPosition = (initial, offset)
|
|
}
|
|
if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams {
|
|
strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .immediate)
|
|
}
|
|
})
|
|
self.reorderingGesture = reorderingGesture
|
|
self.view.addGestureRecognizer(reorderingGesture)
|
|
reorderingGesture.isEnabled = false
|
|
}
|
|
|
|
private var previousSelectedAbsFrame: CGRect?
|
|
private var previousSelectedFrame: CGRect?
|
|
|
|
func cancelAnimations() {
|
|
self.selectedBackgroundNode.layer.removeAllAnimations()
|
|
self.scrollNode.layer.removeAllAnimations()
|
|
self.highlightedItemsContainer.layer.removeAllAnimations()
|
|
self.highlightedItemsClippingContainer.layer.removeAllAnimations()
|
|
}
|
|
|
|
func update(size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, transitionFraction: CGFloat, presentationData: PresentationData, transition proposedTransition: ContainedViewLayoutTransition) {
|
|
let isFirstTime = self.currentParams == nil
|
|
let transition: ContainedViewLayoutTransition = isFirstTime ? .immediate : proposedTransition
|
|
|
|
var focusOnSelectedFilter = self.currentParams?.selectedFilter != selectedFilter
|
|
let previousScrollBounds = self.scrollNode.bounds
|
|
let previousContentWidth = self.scrollNode.view.contentSize.width
|
|
|
|
if self.currentParams?.presentationData.theme !== presentationData.theme {
|
|
if presentationData.theme.rootController.keyboardColor == .dark {
|
|
self.itemsBackgroundView.effect = UIBlurEffect(style: .dark)
|
|
} else {
|
|
self.itemsBackgroundView.effect = UIBlurEffect(style: .light)
|
|
}
|
|
self.itemsBackgroundTintNode.image = generateStretchableFilledCircleImage(diameter: 40.0, color: presentationData.theme.rootController.tabBar.backgroundColor)
|
|
|
|
let selectedFilterColor: UIColor
|
|
if presentationData.theme.rootController.keyboardColor == .dark {
|
|
selectedFilterColor = presentationData.theme.list.itemAccentColor
|
|
} else {
|
|
selectedFilterColor = presentationData.theme.chatList.unreadBadgeInactiveBackgroundColor
|
|
}
|
|
self.selectedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 32.0, color: selectedFilterColor)
|
|
}
|
|
|
|
if isReordering {
|
|
if let reorderedItemIds = self.reorderedItemIds {
|
|
let currentIds = Set(reorderedItemIds)
|
|
if currentIds != Set(filters.map { $0.id }) {
|
|
var updatedReorderedItemIds = reorderedItemIds.filter { id in
|
|
return filters.contains(where: { $0.id == id })
|
|
}
|
|
for filter in filters {
|
|
if !currentIds.contains(filter.id) {
|
|
updatedReorderedItemIds.append(filter.id)
|
|
}
|
|
}
|
|
self.reorderedItemIds = updatedReorderedItemIds
|
|
}
|
|
} else {
|
|
self.reorderedItemIds = filters.map { $0.id }
|
|
}
|
|
} else if self.reorderedItemIds != nil {
|
|
self.reorderedItemIds = nil
|
|
}
|
|
|
|
self.currentParams = (size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering, isEditing, transitionFraction, presentationData: presentationData)
|
|
|
|
self.reorderingGesture?.isEnabled = isEditing || isReordering
|
|
|
|
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
|
|
|
|
enum BadgeAnimation {
|
|
case `in`
|
|
case out
|
|
}
|
|
|
|
var badgeAnimations: [ChatListFilterTabEntryId: BadgeAnimation] = [:]
|
|
|
|
var reorderedFilters: [ChatListFilterTabEntry] = filters
|
|
if let reorderedItemIds = self.reorderedItemIds {
|
|
reorderedFilters = reorderedItemIds.compactMap { id -> ChatListFilterTabEntry? in
|
|
if let index = filters.firstIndex(where: { $0.id == id }) {
|
|
return filters[index]
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
for filter in reorderedFilters {
|
|
let itemNodePair: ItemNodePair
|
|
var itemNodeTransition = transition
|
|
var wasAdded = false
|
|
if let current = self.itemNodePairs[filter.id] {
|
|
itemNodePair = current
|
|
} else {
|
|
itemNodeTransition = .immediate
|
|
wasAdded = true
|
|
itemNodePair = ItemNodePair(regular: ItemNode(context: self.context, pressed: { [weak self] in
|
|
self?.tabSelected?(filter.id, false)
|
|
}, requestedDeletion: { [weak self] in
|
|
self?.tabRequestedDeletion?(filter.id)
|
|
}, contextGesture: { [weak self] sourceNode, gesture in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.scrollNode.view.panGestureRecognizer.isEnabled = false
|
|
strongSelf.scrollNode.view.panGestureRecognizer.isEnabled = true
|
|
strongSelf.scrollNode.view.setContentOffset(strongSelf.scrollNode.view.contentOffset, animated: false)
|
|
switch filter {
|
|
case let .filter(id, _, _):
|
|
strongSelf.contextGesture?(id, sourceNode, gesture, false)
|
|
default:
|
|
strongSelf.contextGesture?(nil, sourceNode, gesture, false)
|
|
}
|
|
}), highlighted: ItemNode(context: self.context, pressed: { [weak self] in
|
|
self?.tabSelected?(filter.id, false)
|
|
}, requestedDeletion: { [weak self] in
|
|
self?.tabRequestedDeletion?(filter.id)
|
|
}, contextGesture: { [weak self] sourceNode, gesture in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
switch filter {
|
|
case let .filter(id, _, _):
|
|
strongSelf.scrollNode.view.panGestureRecognizer.isEnabled = false
|
|
strongSelf.scrollNode.view.panGestureRecognizer.isEnabled = true
|
|
strongSelf.scrollNode.view.setContentOffset(strongSelf.scrollNode.view.contentOffset, animated: false)
|
|
strongSelf.contextGesture?(id, sourceNode, gesture, false)
|
|
default:
|
|
strongSelf.contextGesture?(nil, sourceNode, gesture, false)
|
|
}
|
|
}))
|
|
self.itemNodePairs[filter.id] = itemNodePair
|
|
}
|
|
let unreadCount: Int
|
|
let unreadHasUnmuted: Bool
|
|
var isNoFilter: Bool = false
|
|
switch filter {
|
|
case let .all(count):
|
|
unreadCount = count
|
|
unreadHasUnmuted = true
|
|
isNoFilter = true
|
|
case let .filter(_, _, unread):
|
|
unreadCount = unread.value
|
|
unreadHasUnmuted = unread.hasUnmuted
|
|
}
|
|
if !wasAdded && (itemNodePair.regular.unreadCount != 0) != (unreadCount != 0) {
|
|
badgeAnimations[filter.id] = (unreadCount != 0) ? .in : .out
|
|
}
|
|
itemNodePair.regular.updateText(title: filter.title(strings: presentationData.strings), shortTitle: filter.shortTitle(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, isSelected: false, isEditing: false, isAllChats: isNoFilter, isReordering: isEditing || isReordering, presentationData: presentationData, transition: itemNodeTransition)
|
|
itemNodePair.highlighted.updateText(title: filter.title(strings: presentationData.strings), shortTitle: filter.shortTitle(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, isSelected: true, isEditing: false, isAllChats: isNoFilter, isReordering: isEditing || isReordering, presentationData: presentationData, transition: itemNodeTransition)
|
|
}
|
|
var removeKeys: [ChatListFilterTabEntryId] = []
|
|
for (id, _) in self.itemNodePairs {
|
|
if !filters.contains(where: { $0.id == id }) {
|
|
removeKeys.append(id)
|
|
}
|
|
}
|
|
for id in removeKeys {
|
|
if let itemNodePair = self.itemNodePairs.removeValue(forKey: id) {
|
|
let regular = itemNodePair.regular
|
|
let highlighted = itemNodePair.highlighted
|
|
transition.updateAlpha(node: regular, alpha: 0.0, completion: { [weak regular] _ in
|
|
regular?.removeFromSupernode()
|
|
})
|
|
transition.updateTransformScale(node: regular, scale: 0.1)
|
|
transition.updateAlpha(node: highlighted, alpha: 0.0, completion: { [weak highlighted] _ in
|
|
highlighted?.removeFromSupernode()
|
|
})
|
|
transition.updateTransformScale(node: highlighted, scale: 0.1)
|
|
}
|
|
}
|
|
|
|
var tabSizes: [(ChatListFilterTabEntryId, CGSize, CGSize, ItemNodePair, Bool)] = []
|
|
var totalRawTabSize: CGFloat = 0.0
|
|
var selectionFrames: [CGRect] = []
|
|
|
|
for filter in reorderedFilters {
|
|
guard let itemNodePair = self.itemNodePairs[filter.id] else {
|
|
continue
|
|
}
|
|
let wasAdded = itemNodePair.regular.supernode == nil
|
|
var itemNodeTransition = transition
|
|
if wasAdded {
|
|
itemNodeTransition = .immediate
|
|
self.itemsContainer.addSubnode(itemNodePair.regular)
|
|
self.highlightedItemsContainer.addSubnode(itemNodePair.highlighted)
|
|
}
|
|
let (paneNodeWidth, paneNodeShortWidth) = itemNodePair.regular.updateLayout(height: size.height, transition: itemNodeTransition)
|
|
let _ = itemNodePair.highlighted.updateLayout(height: size.height, transition: itemNodeTransition)
|
|
let paneNodeSize = CGSize(width: paneNodeWidth, height: size.height)
|
|
let paneNodeShortSize = CGSize(width: paneNodeShortWidth, height: size.height)
|
|
tabSizes.append((filter.id, paneNodeSize, paneNodeShortSize, itemNodePair, wasAdded))
|
|
totalRawTabSize += paneNodeSize.width
|
|
|
|
if case .animated = transition, let badgeAnimation = badgeAnimations[filter.id] {
|
|
switch badgeAnimation {
|
|
case .in:
|
|
itemNodePair.regular.animateBadgeIn()
|
|
itemNodePair.highlighted.animateBadgeIn()
|
|
case .out:
|
|
itemNodePair.regular.animateBadgeOut()
|
|
itemNodePair.highlighted.animateBadgeOut()
|
|
}
|
|
}
|
|
}
|
|
// TODO(swiftgram): Support compact layout
|
|
let minSpacing: CGFloat = 30.0
|
|
|
|
let resolvedInitialSideInset: CGFloat = 8.0 + 14.0 + 4.0 + sideInset
|
|
|
|
var longTitlesWidth: CGFloat = 0.0
|
|
var shortTitlesWidth: CGFloat = 0.0
|
|
for i in 0 ..< tabSizes.count {
|
|
let (_, paneNodeSize, paneNodeShortSize, _, _) = tabSizes[i]
|
|
longTitlesWidth += paneNodeSize.width
|
|
shortTitlesWidth += paneNodeShortSize.width
|
|
}
|
|
let totalSpacing = CGFloat(tabSizes.count - 1) * minSpacing
|
|
let useShortTitles = (longTitlesWidth + totalSpacing + resolvedInitialSideInset * 2.0) > size.width
|
|
|
|
var rawContentWidth = useShortTitles ? shortTitlesWidth : longTitlesWidth
|
|
rawContentWidth += totalSpacing
|
|
|
|
let resolvedSideInset = max(resolvedInitialSideInset, floor((size.width - rawContentWidth) / 2.0))
|
|
|
|
var leftOffset: CGFloat = resolvedSideInset
|
|
|
|
let itemsBackgroundLeftX = leftOffset - 14.0 - 4.0
|
|
|
|
for i in 0 ..< tabSizes.count {
|
|
let (itemId, paneNodeLongSize, paneNodeShortSize, itemNodePair, wasAdded) = tabSizes[i]
|
|
var itemNodeTransition = transition
|
|
if wasAdded {
|
|
itemNodeTransition = .immediate
|
|
}
|
|
|
|
let useShortTitle = itemId == .all && sgUseShortAllChatsTitle(useShortTitles)
|
|
let paneNodeSize = useShortTitle ? paneNodeShortSize : paneNodeLongSize
|
|
|
|
let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize)
|
|
|
|
if itemId == self.reorderingItem, let (initial, offset) = self.reorderingItemPosition {
|
|
itemNodeTransition.updateSublayerTransformScale(node: itemNodePair.regular, scale: 1.2)
|
|
itemNodeTransition.updateAlpha(node: itemNodePair.regular, alpha: 0.9)
|
|
let offsetFrame = CGRect(origin: CGPoint(x: initial + offset, y: paneFrame.minY), size: paneFrame.size)
|
|
itemNodeTransition.updateFrameAdditive(node: itemNodePair.regular, frame: offsetFrame)
|
|
selectionFrames.append(offsetFrame)
|
|
} else {
|
|
itemNodeTransition.updateSublayerTransformScale(node: itemNodePair.regular, scale: 1.0)
|
|
itemNodeTransition.updateAlpha(node: itemNodePair.regular, alpha: 1.0)
|
|
if wasAdded {
|
|
itemNodePair.regular.frame = paneFrame
|
|
itemNodePair.regular.alpha = 0.0
|
|
itemNodeTransition.updateAlpha(node: itemNodePair.regular, alpha: 1.0)
|
|
} else {
|
|
itemNodeTransition.updateFrameAdditive(node: itemNodePair.regular, frame: paneFrame)
|
|
}
|
|
selectionFrames.append(paneFrame)
|
|
}
|
|
|
|
if wasAdded {
|
|
itemNodePair.highlighted.frame = paneFrame
|
|
itemNodePair.highlighted.alpha = 0.0
|
|
itemNodeTransition.updateAlpha(node: itemNodePair.highlighted, alpha: 1.0)
|
|
} else {
|
|
itemNodeTransition.updateFrameAdditive(node: itemNodePair.highlighted, frame: paneFrame)
|
|
}
|
|
|
|
itemNodePair.regular.updateArea(size: paneFrame.size, sideInset: minSpacing / 2.0, useShortTitle: useShortTitle, transition: itemNodeTransition)
|
|
itemNodePair.regular.hitTestSlop = UIEdgeInsets(top: 0.0, left: -minSpacing / 2.0, bottom: 0.0, right: -minSpacing / 2.0)
|
|
|
|
itemNodePair.highlighted.updateArea(size: paneFrame.size, sideInset: minSpacing / 2.0, useShortTitle: useShortTitle, transition: itemNodeTransition)
|
|
itemNodePair.highlighted.hitTestSlop = UIEdgeInsets(top: 0.0, left: -minSpacing / 2.0, bottom: 0.0, right: -minSpacing / 2.0)
|
|
|
|
leftOffset += paneNodeSize.width + minSpacing
|
|
}
|
|
leftOffset -= minSpacing
|
|
let itemsBackgroundRightX = leftOffset + 14.0 + 4.0
|
|
|
|
leftOffset += resolvedSideInset
|
|
|
|
let backgroundFrame = CGRect(origin: CGPoint(x: itemsBackgroundLeftX, y: 0.0), size: CGSize(width: itemsBackgroundRightX - itemsBackgroundLeftX, height: size.height))
|
|
transition.updateFrame(view: self.itemsBackgroundView, frame: backgroundFrame)
|
|
transition.updateFrame(node: self.itemsBackgroundTintNode, frame: backgroundFrame)
|
|
|
|
self.scrollNode.view.contentSize = CGSize(width: itemsBackgroundRightX + 8.0, height: size.height)
|
|
|
|
var selectedFrame: CGRect?
|
|
if let selectedFilter = selectedFilter, let currentIndex = reorderedFilters.firstIndex(where: { $0.id == selectedFilter }) {
|
|
func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect {
|
|
return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t)))
|
|
}
|
|
|
|
if currentIndex != 0 && transitionFraction > 0.0 {
|
|
let currentFrame = selectionFrames[currentIndex]
|
|
let previousFrame = selectionFrames[currentIndex - 1]
|
|
selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction))
|
|
} else if currentIndex != filters.count - 1 && transitionFraction < 0.0 {
|
|
let currentFrame = selectionFrames[currentIndex]
|
|
let previousFrame = selectionFrames[currentIndex + 1]
|
|
selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction))
|
|
} else {
|
|
selectedFrame = selectionFrames[currentIndex]
|
|
}
|
|
}
|
|
|
|
transition.updateFrame(node: self.itemsContainer, frame: CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize))
|
|
|
|
if let selectedFrame = selectedFrame {
|
|
let wasAdded = self.selectedBackgroundNode.isHidden
|
|
self.selectedBackgroundNode.isHidden = false
|
|
let lineFrame = CGRect(origin: CGPoint(x: selectedFrame.minX - 14.0, y: floor((size.height - 32.0) / 2.0)), size: CGSize(width: selectedFrame.width + 14.0 * 2.0, height: 32.0))
|
|
if wasAdded {
|
|
self.selectedBackgroundNode.frame = lineFrame
|
|
self.selectedBackgroundNode.alpha = 0.0
|
|
} else {
|
|
transition.updateFrame(node: self.selectedBackgroundNode, frame: lineFrame)
|
|
}
|
|
transition.updateFrame(node: self.highlightedItemsClippingContainer, frame: lineFrame)
|
|
transition.updateFrame(node: self.highlightedItemsContainer, frame: CGRect(origin: CGPoint(x: -lineFrame.minX, y: -lineFrame.minY), size: self.scrollNode.view.contentSize))
|
|
transition.updateAlpha(node: self.selectedBackgroundNode, alpha: isReordering ? 0.0 : 1.0)
|
|
transition.updateAlpha(node: self.highlightedItemsClippingContainer, alpha: isReordering ? 0.0 : 1.0)
|
|
|
|
if let previousSelectedFrame = self.previousSelectedFrame {
|
|
let previousContentOffsetX = max(0.0, min(previousContentWidth - previousScrollBounds.width, floor(previousSelectedFrame.midX - previousScrollBounds.width / 2.0)))
|
|
if abs(previousContentOffsetX - previousScrollBounds.minX) < 1.0 {
|
|
focusOnSelectedFilter = true
|
|
}
|
|
}
|
|
|
|
if focusOnSelectedFilter && self.reorderingItem == nil {
|
|
let updatedBounds: CGRect
|
|
if transitionFraction.isZero && selectedFilter == reorderedFilters.first?.id {
|
|
updatedBounds = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)
|
|
} else if transitionFraction.isZero && selectedFilter == reorderedFilters.last?.id {
|
|
updatedBounds = CGRect(origin: CGPoint(x: max(0.0, self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width), y: 0.0), size: self.scrollNode.bounds.size)
|
|
} else {
|
|
let contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(selectedFrame.midX - self.scrollNode.bounds.width / 2.0)))
|
|
updatedBounds = CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size)
|
|
}
|
|
self.scrollNode.bounds = updatedBounds
|
|
}
|
|
transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX)
|
|
|
|
self.previousSelectedAbsFrame = selectedFrame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0)
|
|
self.previousSelectedFrame = selectedFrame
|
|
} else {
|
|
self.selectedBackgroundNode.isHidden = true
|
|
self.previousSelectedAbsFrame = nil
|
|
self.previousSelectedFrame = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private class ReorderingGestureRecognizerTimerTarget: NSObject {
|
|
private let f: () -> Void
|
|
|
|
init(_ f: @escaping () -> Void) {
|
|
self.f = f
|
|
|
|
super.init()
|
|
}
|
|
|
|
@objc func timerEvent() {
|
|
self.f()
|
|
}
|
|
}
|
|
|
|
private final class ReorderingGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate {
|
|
private let shouldBegin: (CGPoint) -> Bool
|
|
private let began: (CGPoint) -> Void
|
|
private let ended: () -> Void
|
|
private let moved: (CGFloat) -> Void
|
|
|
|
private var initialLocation: CGPoint?
|
|
private var delayTimer: Foundation.Timer?
|
|
|
|
var currentLocation: CGPoint?
|
|
|
|
init(shouldBegin: @escaping (CGPoint) -> Bool, began: @escaping (CGPoint) -> Void, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void) {
|
|
self.shouldBegin = shouldBegin
|
|
self.began = began
|
|
self.ended = ended
|
|
self.moved = moved
|
|
|
|
super.init(target: nil, action: nil)
|
|
|
|
self.delegate = self
|
|
}
|
|
|
|
override func reset() {
|
|
super.reset()
|
|
|
|
self.initialLocation = nil
|
|
self.delayTimer?.invalidate()
|
|
self.delayTimer = nil
|
|
self.currentLocation = nil
|
|
}
|
|
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
if otherGestureRecognizer is UIPanGestureRecognizer {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesBegan(touches, with: event)
|
|
|
|
guard let location = touches.first?.location(in: self.view) else {
|
|
self.state = .failed
|
|
return
|
|
}
|
|
|
|
if self.state == .possible {
|
|
if self.delayTimer == nil {
|
|
if !self.shouldBegin(location) {
|
|
self.state = .failed
|
|
return
|
|
}
|
|
self.initialLocation = location
|
|
let timer = Foundation.Timer(timeInterval: 0.2, target: ReorderingGestureRecognizerTimerTarget { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.delayTimer = nil
|
|
strongSelf.state = .began
|
|
strongSelf.began(location)
|
|
}, selector: #selector(ReorderingGestureRecognizerTimerTarget.timerEvent), userInfo: nil, repeats: false)
|
|
self.delayTimer = timer
|
|
RunLoop.main.add(timer, forMode: .common)
|
|
} else {
|
|
self.state = .failed
|
|
}
|
|
}
|
|
}
|
|
|
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesEnded(touches, with: event)
|
|
|
|
self.delayTimer?.invalidate()
|
|
|
|
if self.state == .began || self.state == .changed {
|
|
self.ended()
|
|
}
|
|
|
|
self.state = .failed
|
|
}
|
|
|
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesCancelled(touches, with: event)
|
|
|
|
if self.state == .began || self.state == .changed {
|
|
self.delayTimer?.invalidate()
|
|
self.ended()
|
|
self.state = .failed
|
|
}
|
|
}
|
|
|
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesMoved(touches, with: event)
|
|
|
|
guard let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) else {
|
|
return
|
|
}
|
|
let offset = location.x - initialLocation.x
|
|
self.currentLocation = location
|
|
|
|
if self.delayTimer != nil {
|
|
if abs(offset) > 4.0 {
|
|
self.delayTimer?.invalidate()
|
|
self.state = .failed
|
|
return
|
|
}
|
|
} else {
|
|
if self.state == .began || self.state == .changed {
|
|
self.state = .changed
|
|
self.moved(offset)
|
|
}
|
|
}
|
|
}
|
|
}
|