mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 14:20:20 +00:00
Story folders
This commit is contained in:
@@ -10,12 +10,14 @@ swift_library(
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/Components/MultilineTextWithEntitiesComponent",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/AccountContext"
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/TelegramPresentationData",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import PlainButtonComponent
|
||||
import MultilineTextWithEntitiesComponent
|
||||
import TextFormat
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
|
||||
public final class TabSelectorComponent: Component {
|
||||
public final class ItemEnvironment: Equatable {
|
||||
@@ -57,53 +59,83 @@ public final class TabSelectorComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public struct Item: Equatable {
|
||||
public final class Item: Equatable {
|
||||
public enum Content: Equatable {
|
||||
case text(String)
|
||||
case component(AnyComponent<ItemEnvironment>)
|
||||
}
|
||||
|
||||
public var id: AnyHashable
|
||||
public var content: Content
|
||||
public let id: AnyHashable
|
||||
public let content: Content
|
||||
public let isReorderable: Bool
|
||||
public let contextAction: ((ASDisplayNode, ContextGesture) -> Void)?
|
||||
|
||||
public init(
|
||||
id: AnyHashable,
|
||||
content: Content
|
||||
content: Content,
|
||||
isReorderable: Bool = false,
|
||||
contextAction: ((ASDisplayNode, ContextGesture) -> Void)? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.content = content
|
||||
self.isReorderable = isReorderable
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
|
||||
public init(
|
||||
convenience public init(
|
||||
id: AnyHashable,
|
||||
title: String
|
||||
title: String,
|
||||
isReorderable: Bool = false,
|
||||
contextAction: ((ASDisplayNode, ContextGesture) -> Void)? = nil
|
||||
) {
|
||||
self.init(id: id, content: .text(title))
|
||||
self.init(id: id, content: .text(title), isReorderable: isReorderable, contextAction: contextAction)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
if lhs.id != rhs.id {
|
||||
return false
|
||||
}
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
if lhs.isReorderable != rhs.isReorderable {
|
||||
return false
|
||||
}
|
||||
if (lhs.contextAction == nil) != (rhs.contextAction == nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public let context: AccountContext?
|
||||
public let colors: Colors
|
||||
public let theme: PresentationTheme
|
||||
public let customLayout: CustomLayout?
|
||||
public let items: [Item]
|
||||
public let selectedId: AnyHashable?
|
||||
public let reorderItem: ((AnyHashable, AnyHashable) -> Void)?
|
||||
public let setSelectedId: (AnyHashable) -> Void
|
||||
public let transitionFraction: CGFloat?
|
||||
|
||||
public init(
|
||||
context: AccountContext? = nil,
|
||||
colors: Colors,
|
||||
theme: PresentationTheme,
|
||||
customLayout: CustomLayout? = nil,
|
||||
items: [Item],
|
||||
selectedId: AnyHashable?,
|
||||
reorderItem: ((AnyHashable, AnyHashable) -> Void)? = nil,
|
||||
setSelectedId: @escaping (AnyHashable) -> Void,
|
||||
transitionFraction: CGFloat? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.colors = colors
|
||||
self.theme = theme
|
||||
self.customLayout = customLayout
|
||||
self.items = items
|
||||
self.selectedId = selectedId
|
||||
self.reorderItem = reorderItem
|
||||
self.setSelectedId = setSelectedId
|
||||
self.transitionFraction = transitionFraction
|
||||
}
|
||||
@@ -115,6 +147,9 @@ public final class TabSelectorComponent: Component {
|
||||
if lhs.colors != rhs.colors {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.customLayout != rhs.customLayout {
|
||||
return false
|
||||
}
|
||||
@@ -124,16 +159,188 @@ public final class TabSelectorComponent: Component {
|
||||
if lhs.selectedId != rhs.selectedId {
|
||||
return false
|
||||
}
|
||||
if (lhs.reorderItem == nil) != (rhs.reorderItem == nil) {
|
||||
return false
|
||||
}
|
||||
if lhs.transitionFraction != rhs.transitionFraction {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private final class VisibleItem {
|
||||
final class VisibleItem: UIView {
|
||||
let action: () -> Void
|
||||
let contextAction: (ASDisplayNode, ContextGesture) -> Void
|
||||
|
||||
let extractedContainerNode: ContextExtractedContentContainingNode
|
||||
let containerNode: ContextControllerSourceNode
|
||||
|
||||
let containerButton: UIView
|
||||
var extractedBackgroundView: UIImageView?
|
||||
|
||||
let title = ComponentView<Empty>()
|
||||
|
||||
init() {
|
||||
var item: Item?
|
||||
|
||||
var tapGesture: UITapGestureRecognizer?
|
||||
var theme: PresentationTheme?
|
||||
var size: CGSize?
|
||||
var isReordering: Bool = false
|
||||
|
||||
init(action: @escaping () -> Void, contextAction: @escaping (ASDisplayNode, ContextGesture) -> Void) {
|
||||
self.action = action
|
||||
self.contextAction = contextAction
|
||||
|
||||
self.extractedContainerNode = ContextExtractedContentContainingNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
|
||||
self.containerButton = UIView()
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.extractedContainerNode.contentNode.view.addSubview(self.containerButton)
|
||||
|
||||
self.containerNode.addSubnode(self.extractedContainerNode)
|
||||
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
|
||||
self.addSubview(self.containerNode.view)
|
||||
|
||||
//self.containerButton.addSubview(self.iconContainer)
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:)))
|
||||
self.tapGesture = tapGesture
|
||||
self.containerButton.addGestureRecognizer(tapGesture)
|
||||
tapGesture.isEnabled = false
|
||||
|
||||
self.containerNode.activated = { [weak self] gesture, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.contextAction(self.extractedContainerNode, gesture)
|
||||
}
|
||||
|
||||
self.extractedContainerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
|
||||
guard let self, let theme = self.theme, let size = self.size else {
|
||||
return
|
||||
}
|
||||
|
||||
if isExtracted {
|
||||
let extractedBackgroundView: UIImageView
|
||||
if let current = self.extractedBackgroundView {
|
||||
extractedBackgroundView = current
|
||||
} else {
|
||||
extractedBackgroundView = UIImageView(image: generateStretchableFilledCircleImage(diameter: size.height, color: theme.contextMenu.backgroundColor))
|
||||
self.extractedBackgroundView = extractedBackgroundView
|
||||
self.extractedContainerNode.contentNode.view.insertSubview(extractedBackgroundView, at: 0)
|
||||
extractedBackgroundView.frame = self.extractedContainerNode.contentNode.bounds.insetBy(dx: 0.0, dy: 0.0)
|
||||
extractedBackgroundView.alpha = 0.0
|
||||
}
|
||||
transition.updateAlpha(layer: extractedBackgroundView.layer, alpha: 1.0)
|
||||
} else if let extractedBackgroundView = self.extractedBackgroundView {
|
||||
self.extractedBackgroundView = nil
|
||||
let alphaTransition: ContainedViewLayoutTransition
|
||||
if transition.isAnimated {
|
||||
alphaTransition = .animated(duration: 0.18, curve: .easeInOut)
|
||||
} else {
|
||||
alphaTransition = .immediate
|
||||
}
|
||||
alphaTransition.updateAlpha(layer: extractedBackgroundView.layer, alpha: 0.0, completion: { [weak extractedBackgroundView] _ in
|
||||
extractedBackgroundView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
self.containerNode.isGestureEnabled = false
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateIsShaking(animated: Bool) {
|
||||
if self.isReordering {
|
||||
if self.containerButton.layer.animation(forKey: "shaking_position") == nil {
|
||||
let degreesToRadians: (_ x: CGFloat) -> CGFloat = { x in
|
||||
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.containerButton.layer.add(position, forKey: "shaking_position")
|
||||
self.containerButton.layer.add(transform, forKey: "shaking_rotation")
|
||||
}
|
||||
} else if self.containerButton.layer.animation(forKey: "shaking_position") != nil {
|
||||
if let presentationLayer = self.containerButton.layer.presentation() {
|
||||
let transition: ComponentTransition = .easeInOut(duration: 0.1)
|
||||
if presentationLayer.position != self.containerButton.layer.position {
|
||||
transition.animatePosition(layer: self.containerButton.layer, from: CGPoint(x: presentationLayer.position.x - self.containerButton.layer.position.x, y: presentationLayer.position.y - self.containerButton.layer.position.y), to: CGPoint(), additive: true)
|
||||
}
|
||||
if !CATransform3DIsIdentity(presentationLayer.transform) {
|
||||
transition.setTransform(layer: self.containerButton.layer, transform: CATransform3DIdentity)
|
||||
}
|
||||
}
|
||||
|
||||
self.containerButton.layer.removeAnimation(forKey: "shaking_position")
|
||||
self.containerButton.layer.removeAnimation(forKey: "shaking_rotation")
|
||||
}
|
||||
}
|
||||
|
||||
func update(theme: PresentationTheme, size: CGSize, item: Item, isReordering: Bool, transition: ComponentTransition) {
|
||||
self.theme = theme
|
||||
self.size = size
|
||||
self.isReordering = isReordering
|
||||
self.item = item
|
||||
|
||||
self.containerNode.isGestureEnabled = item.contextAction != nil && !isReordering
|
||||
self.tapGesture?.isEnabled = !isReordering
|
||||
|
||||
transition.setFrame(view: self.containerButton, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
self.updateIsShaking(animated: !transition.animation.isImmediate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +353,10 @@ public final class TabSelectorComponent: Component {
|
||||
|
||||
private var didInitiallyScroll = false
|
||||
|
||||
private var reorderRecognizer: ReorderGestureRecognizer?
|
||||
private weak var reorderingItem: VisibleItem?
|
||||
private var reorderingItemPosition: (initial: CGFloat, offset: CGFloat) = (0.0, 0.0)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.selectionView = UIImageView()
|
||||
|
||||
@@ -161,6 +372,53 @@ public final class TabSelectorComponent: Component {
|
||||
self.clipsToBounds = false
|
||||
|
||||
self.addSubview(self.selectionView)
|
||||
|
||||
let reorderRecognizer = ReorderGestureRecognizer(
|
||||
shouldBegin: { [weak self] point in
|
||||
guard let self, let component = self.component, component.reorderItem != nil else {
|
||||
return (allowed: false, requiresLongPress: false, item: nil)
|
||||
}
|
||||
|
||||
var item: VisibleItem?
|
||||
for (_, visibleItem) in self.visibleItems {
|
||||
if visibleItem.bounds.contains(self.convert(point, to: visibleItem)) {
|
||||
item = visibleItem
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let item, let itemValue = item.item, itemValue.isReorderable {
|
||||
return (allowed: true, requiresLongPress: false, item: item)
|
||||
} else {
|
||||
return (allowed: false, requiresLongPress: false, item: nil)
|
||||
}
|
||||
},
|
||||
willBegin: { point in
|
||||
},
|
||||
began: { [weak self] item in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.setReorderingItem(item: item)
|
||||
},
|
||||
ended: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.setReorderingItem(item: nil)
|
||||
},
|
||||
moved: { [weak self] distance in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.moveReorderingItem(distance: distance.x)
|
||||
},
|
||||
isActiveUpdated: { _ in
|
||||
}
|
||||
)
|
||||
self.reorderRecognizer = reorderRecognizer
|
||||
self.addGestureRecognizer(reorderRecognizer)
|
||||
reorderRecognizer.isEnabled = false
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@@ -174,12 +432,65 @@ public final class TabSelectorComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
private func setReorderingItem(item: VisibleItem?) {
|
||||
self.reorderingItem = item
|
||||
if let item {
|
||||
self.reorderingItemPosition.initial = item.frame.minX
|
||||
self.reorderingItemPosition.offset = 0.0
|
||||
} else {
|
||||
self.reorderingItemPosition = (0.0, 0.0)
|
||||
}
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
|
||||
private func moveReorderingItem(distance: CGFloat) {
|
||||
guard let reorderingItem = self.reorderingItem else {
|
||||
return
|
||||
}
|
||||
let previousPosition = self.reorderingItemPosition.initial + self.reorderingItemPosition.offset + reorderingItem.bounds.width * 0.5
|
||||
self.reorderingItemPosition.offset = distance
|
||||
let updatedPosition = self.reorderingItemPosition.initial + self.reorderingItemPosition.offset + reorderingItem.bounds.width * 0.5
|
||||
|
||||
self.state?.updated(transition: .immediate)
|
||||
|
||||
if let component = self.component, let reorderItem = component.reorderItem {
|
||||
var currentId: AnyHashable?
|
||||
var reorderToId: AnyHashable?
|
||||
for (id, item) in self.visibleItems {
|
||||
if item === reorderingItem {
|
||||
currentId = id
|
||||
continue
|
||||
}
|
||||
guard let targetItem = item.item else {
|
||||
continue
|
||||
}
|
||||
if !targetItem.isReorderable {
|
||||
continue
|
||||
}
|
||||
if reorderToId != nil {
|
||||
continue
|
||||
}
|
||||
let itemCenter = item.center.x
|
||||
if previousPosition < itemCenter && updatedPosition > itemCenter {
|
||||
reorderToId = id
|
||||
} else if previousPosition > itemCenter && updatedPosition < itemCenter {
|
||||
reorderToId = id
|
||||
}
|
||||
}
|
||||
if let currentId, let reorderToId {
|
||||
reorderItem(currentId, reorderToId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: TabSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let selectionColorUpdated = component.colors.selection != self.component?.colors.selection
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
self.reorderRecognizer?.isEnabled = component.reorderItem != nil
|
||||
|
||||
let baseHeight: CGFloat = 28.0
|
||||
|
||||
var verticalInset: CGFloat = 0.0
|
||||
@@ -231,7 +542,24 @@ public final class TabSelectorComponent: Component {
|
||||
if let current = self.visibleItems[item.id] {
|
||||
itemView = current
|
||||
} else {
|
||||
itemView = VisibleItem()
|
||||
let itemId = item.id
|
||||
itemView = VisibleItem(action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let item = component.items.first(where: { $0.id == itemId }) else {
|
||||
return
|
||||
}
|
||||
component.setSelectedId(item.id)
|
||||
}, contextAction: { [weak self] sourceNode, gesture in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let item = component.items.first(where: { $0.id == itemId }) else {
|
||||
return
|
||||
}
|
||||
item.contextAction?(sourceNode, gesture)
|
||||
})
|
||||
self.visibleItems[item.id] = itemView
|
||||
itemTransition = itemTransition.withAnimation(.none)
|
||||
}
|
||||
@@ -261,32 +589,18 @@ public final class TabSelectorComponent: Component {
|
||||
|
||||
let itemSize = itemView.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(ItemComponent(
|
||||
context: component.context,
|
||||
content: item.content,
|
||||
font: itemFont,
|
||||
color: component.colors.foreground,
|
||||
selectedColor: component.colors.selection,
|
||||
selectionFraction: useSelectionFraction ? selectionFraction : 0.0
|
||||
)),
|
||||
effectAlignment: .center,
|
||||
minSize: nil,
|
||||
action: { [weak self, weak itemView] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.setSelectedId(itemId)
|
||||
|
||||
if let view = itemView?.title.view, allowScroll && self.contentSize.width > self.bounds.width {
|
||||
self.scrollRectToVisible(view.frame.insetBy(dx: -64.0, dy: 0.0), animated: true)
|
||||
}
|
||||
},
|
||||
animateScale: !isLineSelection
|
||||
component: AnyComponent(ItemComponent(
|
||||
context: component.context,
|
||||
content: item.content,
|
||||
font: itemFont,
|
||||
color: component.colors.foreground,
|
||||
selectedColor: component.colors.selection,
|
||||
selectionFraction: useSelectionFraction ? selectionFraction : 0.0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: 100.0)
|
||||
)
|
||||
|
||||
innerContentWidth += itemSize.width
|
||||
itemViews[item.id] = (itemView, itemSize, itemTransition)
|
||||
index += 1
|
||||
@@ -302,6 +616,7 @@ public final class TabSelectorComponent: Component {
|
||||
var previousBackgroundRect: CGRect?
|
||||
var selectedBackgroundRect: CGRect?
|
||||
var nextBackgroundRect: CGRect?
|
||||
var selectedItemIsReordering = false
|
||||
|
||||
for item in component.items {
|
||||
guard let (itemView, itemSize, itemTransition) = itemViews[item.id] else {
|
||||
@@ -310,10 +625,18 @@ public final class TabSelectorComponent: Component {
|
||||
if contentWidth > spacing {
|
||||
contentWidth += spacing
|
||||
}
|
||||
let itemTitleFrame = CGRect(origin: CGPoint(x: contentWidth + innerInset, y: verticalInset + floor((baseHeight - itemSize.height) * 0.5)), size: itemSize)
|
||||
let itemBackgroundRect = CGRect(origin: CGPoint(x: contentWidth, y: verticalInset), size: CGSize(width: innerInset + itemSize.width + innerInset, height: baseHeight))
|
||||
let baseItemTitleFrame = CGRect(origin: CGPoint(x: contentWidth + innerInset, y: verticalInset + floor((baseHeight - itemSize.height) * 0.5)), size: itemSize)
|
||||
var itemBackgroundRect = CGRect(origin: CGPoint(x: contentWidth, y: verticalInset), size: CGSize(width: innerInset + itemSize.width + innerInset, height: baseHeight))
|
||||
let itemTitleFrame = CGRect(origin: CGPoint(x: baseItemTitleFrame.minX - itemBackgroundRect.minX, y: baseItemTitleFrame.minY - itemBackgroundRect.minY), size: baseItemTitleFrame.size)
|
||||
contentWidth = itemBackgroundRect.maxX
|
||||
|
||||
if self.reorderingItem === itemView {
|
||||
itemBackgroundRect.origin.x = self.reorderingItemPosition.initial + self.reorderingItemPosition.offset
|
||||
if item.id == component.selectedId {
|
||||
selectedItemIsReordering = true
|
||||
}
|
||||
}
|
||||
|
||||
if item.id == component.selectedId {
|
||||
selectedBackgroundRect = itemBackgroundRect
|
||||
}
|
||||
@@ -323,14 +646,40 @@ public final class TabSelectorComponent: Component {
|
||||
nextBackgroundRect = itemBackgroundRect
|
||||
}
|
||||
|
||||
if itemView.superview == nil {
|
||||
self.addSubview(itemView)
|
||||
}
|
||||
|
||||
if let itemTitleView = itemView.title.view {
|
||||
if itemTitleView.superview == nil {
|
||||
itemTitleView.layer.anchorPoint = CGPoint()
|
||||
self.addSubview(itemTitleView)
|
||||
itemTitleView.isUserInteractionEnabled = false
|
||||
itemView.containerButton.addSubview(itemTitleView)
|
||||
}
|
||||
itemTransition.setPosition(view: itemTitleView, position: itemTitleFrame.origin)
|
||||
|
||||
itemTransition.setPosition(view: itemView, position: itemBackgroundRect.center)
|
||||
itemTransition.setBounds(view: itemView, bounds: CGRect(origin: CGPoint(), size: itemBackgroundRect.size))
|
||||
|
||||
if self.reorderingItem === itemView {
|
||||
itemTransition.setTransform(view: itemView, transform: CATransform3DMakeScale(1.1, 1.1, 1.0))
|
||||
} else {
|
||||
itemTransition.setTransform(view: itemView, transform: CATransform3DIdentity)
|
||||
}
|
||||
|
||||
itemView.update(theme: component.theme, size: itemBackgroundRect.size, item: item, isReordering: item.isReorderable && component.reorderItem != nil, transition: itemTransition)
|
||||
|
||||
itemTransition.setPosition(view: itemTitleView, position: CGPoint(x: itemTitleFrame.minX, y: itemTitleFrame.minY))
|
||||
itemTransition.setBounds(view: itemTitleView, bounds: CGRect(origin: CGPoint(), size: itemTitleFrame.size))
|
||||
itemTransition.setAlpha(view: itemTitleView, alpha: item.id == component.selectedId || isLineSelection || component.colors.simple ? 1.0 : 0.4)
|
||||
|
||||
var itemAlpha: CGFloat = item.id == component.selectedId || isLineSelection || component.colors.simple ? 1.0 : 0.4
|
||||
if component.reorderItem != nil && !item.isReorderable {
|
||||
itemAlpha *= 0.5
|
||||
itemView.isUserInteractionEnabled = false
|
||||
} else {
|
||||
itemView.isUserInteractionEnabled = true
|
||||
}
|
||||
|
||||
itemTransition.setAlpha(view: itemTitleView, alpha: itemAlpha)
|
||||
}
|
||||
}
|
||||
contentWidth += spacing
|
||||
@@ -339,7 +688,7 @@ public final class TabSelectorComponent: Component {
|
||||
for (id, itemView) in self.visibleItems {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
itemView.title.view?.removeFromSuperview()
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
@@ -366,9 +715,17 @@ public final class TabSelectorComponent: Component {
|
||||
var mappedSelectionFrame = effectiveBackgroundRect.insetBy(dx: innerInset, dy: 0.0)
|
||||
mappedSelectionFrame.origin.y = mappedSelectionFrame.maxY + 6.0
|
||||
mappedSelectionFrame.size.height = 3.0
|
||||
transition.setFrame(view: self.selectionView, frame: mappedSelectionFrame)
|
||||
transition.setPosition(view: self.selectionView, position: mappedSelectionFrame.center)
|
||||
transition.setBounds(view: self.selectionView, bounds: CGRect(origin: CGPoint(), size: mappedSelectionFrame.size))
|
||||
transition.setTransform(view: self.selectionView, transform: CATransform3DIdentity)
|
||||
} else {
|
||||
transition.setFrame(view: self.selectionView, frame: selectedBackgroundRect)
|
||||
transition.setPosition(view: self.selectionView, position: selectedBackgroundRect.center)
|
||||
transition.setBounds(view: self.selectionView, bounds: CGRect(origin: CGPoint(), size: selectedBackgroundRect.size))
|
||||
if selectedItemIsReordering {
|
||||
transition.setTransform(view: self.selectionView, transform: CATransform3DMakeScale(1.1, 1.1, 1.0))
|
||||
} else {
|
||||
transition.setTransform(view: self.selectionView, transform: CATransform3DIdentity)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.selectionView.alpha = 0.0
|
||||
@@ -528,3 +885,197 @@ private final class ItemComponent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ReorderGestureRecognizer: UIGestureRecognizer {
|
||||
private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: TabSelectorComponent.VisibleItem?)
|
||||
private let willBegin: (CGPoint) -> Void
|
||||
private let began: (TabSelectorComponent.VisibleItem) -> Void
|
||||
private let ended: () -> Void
|
||||
private let moved: (CGPoint) -> Void
|
||||
private let isActiveUpdated: (Bool) -> Void
|
||||
|
||||
private var initialLocation: CGPoint?
|
||||
private var longTapTimer: Foundation.Timer?
|
||||
private var longPressTimer: Foundation.Timer?
|
||||
|
||||
private var itemView: TabSelectorComponent.VisibleItem?
|
||||
|
||||
public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: TabSelectorComponent.VisibleItem?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (TabSelectorComponent.VisibleItem) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) {
|
||||
self.shouldBegin = shouldBegin
|
||||
self.willBegin = willBegin
|
||||
self.began = began
|
||||
self.ended = ended
|
||||
self.moved = moved
|
||||
self.isActiveUpdated = isActiveUpdated
|
||||
|
||||
super.init(target: nil, action: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longPressTimer?.invalidate()
|
||||
}
|
||||
|
||||
private func startLongTapTimer() {
|
||||
self.longTapTimer?.invalidate()
|
||||
let longTapTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false, block: { [weak self] _ in
|
||||
self?.longTapTimerFired()
|
||||
})
|
||||
self.longTapTimer = longTapTimer
|
||||
}
|
||||
|
||||
private func stopLongTapTimer() {
|
||||
self.itemView = nil
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
}
|
||||
|
||||
private func startLongPressTimer() {
|
||||
self.longPressTimer?.invalidate()
|
||||
let longPressTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.6, repeats: false, block: { [weak self] _ in
|
||||
self?.longPressTimerFired()
|
||||
})
|
||||
self.longPressTimer = longPressTimer
|
||||
}
|
||||
|
||||
private func stopLongPressTimer() {
|
||||
self.itemView = nil
|
||||
self.longPressTimer?.invalidate()
|
||||
self.longPressTimer = nil
|
||||
}
|
||||
|
||||
override public func reset() {
|
||||
super.reset()
|
||||
|
||||
self.itemView = nil
|
||||
self.stopLongTapTimer()
|
||||
self.stopLongPressTimer()
|
||||
self.initialLocation = nil
|
||||
|
||||
self.isActiveUpdated(false)
|
||||
}
|
||||
|
||||
private func longTapTimerFired() {
|
||||
guard let location = self.initialLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
|
||||
self.willBegin(location)
|
||||
}
|
||||
|
||||
private func longPressTimerFired() {
|
||||
guard let _ = self.initialLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isActiveUpdated(true)
|
||||
self.state = .began
|
||||
self.longPressTimer?.invalidate()
|
||||
self.longPressTimer = nil
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
if let itemView = self.itemView {
|
||||
self.began(itemView)
|
||||
}
|
||||
self.isActiveUpdated(true)
|
||||
}
|
||||
|
||||
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
if self.numberOfTouches > 1 {
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
self.ended()
|
||||
return
|
||||
}
|
||||
|
||||
if self.state == .possible {
|
||||
if let location = touches.first?.location(in: self.view) {
|
||||
let (allowed, requiresLongPress, itemView) = self.shouldBegin(location)
|
||||
if allowed {
|
||||
self.isActiveUpdated(true)
|
||||
|
||||
self.itemView = itemView
|
||||
self.initialLocation = location
|
||||
if requiresLongPress {
|
||||
self.startLongTapTimer()
|
||||
self.startLongPressTimer()
|
||||
} else {
|
||||
self.state = .began
|
||||
if let itemView = self.itemView {
|
||||
self.began(itemView)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
} else {
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
self.initialLocation = nil
|
||||
|
||||
self.stopLongTapTimer()
|
||||
if self.longPressTimer != nil {
|
||||
self.stopLongPressTimer()
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
if self.state == .began || self.state == .changed {
|
||||
self.isActiveUpdated(false)
|
||||
self.ended()
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
self.initialLocation = nil
|
||||
|
||||
self.stopLongTapTimer()
|
||||
if self.longPressTimer != nil {
|
||||
self.isActiveUpdated(false)
|
||||
self.stopLongPressTimer()
|
||||
self.state = .failed
|
||||
}
|
||||
if self.state == .began || self.state == .changed {
|
||||
self.isActiveUpdated(false)
|
||||
self.ended()
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
|
||||
self.state = .changed
|
||||
let offset = CGPoint(x: location.x - initialLocation.x, y: 0.0)
|
||||
self.moved(offset)
|
||||
} else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil {
|
||||
let touchLocation = touch.location(in: self.view)
|
||||
let dX = touchLocation.x - initialTapLocation.x
|
||||
|
||||
if dX > 3.0 {
|
||||
self.stopLongTapTimer()
|
||||
self.stopLongPressTimer()
|
||||
self.initialLocation = nil
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user