2019-12-09 20:56:20 +04:00

783 lines
37 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SyncCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
private struct StickerPackPreviewGridEntry: Comparable, Identifiable {
let index: Int
let stickerItem: StickerPackItem
var stableId: MediaId {
return self.stickerItem.file.fileId
}
static func <(lhs: StickerPackPreviewGridEntry, rhs: StickerPackPreviewGridEntry) -> Bool {
return lhs.index < rhs.index
}
func item(account: Account, interaction: StickerPackPreviewInteraction) -> StickerPackPreviewGridItem {
return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction)
}
}
private struct StickerPackPreviewGridTransaction {
let deletions: [Int]
let insertions: [GridNodeInsertItem]
let updates: [GridNodeUpdateItem]
init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction) {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list)
self.deletions = deleteIndices
self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction), previousIndex: $0.2) }
self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction)) }
}
}
private enum StickerPackAction {
case add
case remove
}
private enum StickerPackNextAction {
case navigatedNext
case dismiss
}
private final class StickerPackContainer: ASDisplayNode {
private let context: AccountContext
private var presentationData: PresentationData
private let stickerPack: StickerPackReference
private let decideNextAction: (StickerPackContainer, StickerPackAction) -> StickerPackNextAction
private let requestDismiss: () -> Void
private let presentInGlobalOverlay: (ViewController, Any?) -> Void
private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?
private let backgroundNode: ASImageNode
private let gridNode: GridNode
private let actionAreaBackgroundNode: ASDisplayNode
private let actionAreaSeparatorNode: ASDisplayNode
private let buttonNode: HighlightableButtonNode
private let titleNode: ImmediateTextNode
private let titleContainer: ASDisplayNode
private let titleSeparatorNode: ASDisplayNode
private(set) var validLayout: (ContainerViewLayout, CGRect, CGFloat, UIEdgeInsets)?
private var currentEntries: [StickerPackPreviewGridEntry] = []
private var enqueuedTransactions: [StickerPackPreviewGridTransaction] = []
private var itemsDisposable: Disposable?
private(set) var currentStickerPack: (StickerPackCollectionInfo, [ItemCollectionItem], Bool)?
private let isReadyValue = Promise<Bool>()
private var didSetReady = false
var isReady: Signal<Bool, NoError> {
return self.isReadyValue.get()
}
var expandProgress: CGFloat = 0.0
var modalProgress: CGFloat = 0.0
let expandProgressUpdated: (StickerPackContainer, ContainedViewLayoutTransition) -> Void
private let interaction: StickerPackPreviewInteraction
init(context: AccountContext, presentationData: PresentationData, stickerPack: StickerPackReference, decideNextAction: @escaping (StickerPackContainer, StickerPackAction) -> StickerPackNextAction, requestDismiss: @escaping () -> Void, expandProgressUpdated: @escaping (StickerPackContainer, ContainedViewLayoutTransition) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) {
self.context = context
self.presentationData = presentationData
self.stickerPack = stickerPack
self.decideNextAction = decideNextAction
self.requestDismiss = requestDismiss
self.presentInGlobalOverlay = presentInGlobalOverlay
self.expandProgressUpdated = expandProgressUpdated
self.sendSticker = sendSticker
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = true
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor)
self.gridNode = GridNode()
self.gridNode.scrollView.alwaysBounceVertical = true
self.gridNode.scrollView.showsVerticalScrollIndicator = false
self.actionAreaBackgroundNode = ASDisplayNode()
self.actionAreaBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor
self.actionAreaSeparatorNode = ASDisplayNode()
self.actionAreaSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor
self.buttonNode = HighlightableButtonNode()
self.titleNode = ImmediateTextNode()
self.titleContainer = ASDisplayNode()
self.titleSeparatorNode = ASDisplayNode()
self.titleSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor
self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: true)
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.gridNode)
self.addSubnode(self.actionAreaBackgroundNode)
self.addSubnode(self.actionAreaSeparatorNode)
self.addSubnode(self.buttonNode)
self.titleContainer.addSubnode(self.titleNode)
self.addSubnode(self.titleContainer)
self.addSubnode(self.titleSeparatorNode)
self.gridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in
self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition)
}
self.gridNode.interactiveScrollingEnded = { [weak self] in
guard let strongSelf = self else {
return
}
let contentOffset = strongSelf.gridNode.scrollView.contentOffset
let insets = strongSelf.gridNode.scrollView.contentInset
if contentOffset.y <= -insets.top - 30.0 {
DispatchQueue.main.async {
self?.requestDismiss()
}
}
}
self.itemsDisposable = (loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: stickerPack, forceActualized: false)
|> deliverOnMainQueue).start(next: { [weak self] contents in
guard let strongSelf = self else {
return
}
strongSelf.updateStickerPackContents(contents)
})
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonNode.alpha = 0.8
} else {
strongSelf.buttonNode.alpha = 1.0
strongSelf.buttonNode.layer.animateAlpha(from: 0.8, to: 1.0, duration: 0.3)
}
}
}
}
deinit {
self.itemsDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
self.gridNode.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>? in
if let strongSelf = self {
if let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem {
return strongSelf.context.account.postbox.transaction { transaction -> Bool in
return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId)
}
|> deliverOnMainQueue
|> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in
if let strongSelf = self {
var menuItems: [PeekControllerMenuItem] = []
if let (info, _, _) = strongSelf.currentStickerPack, info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
if strongSelf.sendSticker != nil {
menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, font: .bold, action: { node, rect in
if let strongSelf = self {
return strongSelf.sendSticker?(.standalone(media: item.file), node, rect) ?? false
} else {
return false
}
}))
}
menuItems.append(PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in
if let strongSelf = self {
if isStarred {
let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start()
} else {
let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start()
}
}
return true
}))
menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }))
}
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems))
} else {
return nil
}
}
}
}
return nil
}, present: { [weak self] content, sourceNode in
if let strongSelf = self {
let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.presentationData.theme), content: content, sourceNode: {
return sourceNode
})
strongSelf.presentInGlobalOverlay(controller, nil)
return controller
}
return nil
}, updateContent: { [weak self] content in
if let strongSelf = self {
var item: StickerPreviewPeekItem?
if let content = content as? StickerPreviewPeekContent {
item = content.item
}
strongSelf.updatePreviewingItem(item: item, animated: true)
}
}, activateBySingleTap: true))
}
@objc func buttonPressed() {
guard let (info, items, installed) = currentStickerPack else {
return
}
let _ = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings])
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] sharedData in
guard let strongSelf = self else {
return
}
var stickerSettings = StickerSettings.defaultSettings
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings] as? StickerSettings {
stickerSettings = value
}
if installed {
let _ = removeStickerPackInteractively(postbox: strongSelf.context.account.postbox, id: info.id, option: .delete).start()
} else {
let _ = addStickerPackInteractively(postbox: strongSelf.context.account.postbox, info: info, items: items).start()
}
switch strongSelf.decideNextAction(strongSelf, installed ? .remove : .add) {
case .dismiss:
strongSelf.requestDismiss()
case .navigatedNext:
strongSelf.updateStickerPackContents(.result(info: info, items: items, installed: !installed))
}
})
}
private func updateStickerPackContents(_ contents: LoadedStickerPack) {
var entries: [StickerPackPreviewGridEntry] = []
var updateLayout = false
switch contents {
case .fetching:
entries = []
case .none:
entries = []
case let .result(info, items, installed):
self.currentStickerPack = (info, items, installed)
if installed {
let text: String
if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
text = self.presentationData.strings.StickerPack_RemoveStickerCount(info.count)
} else {
text = self.presentationData.strings.StickerPack_RemoveMaskCount(info.count)
}
self.buttonNode.setTitle(text.uppercased(), with: Font.semibold(17.0), with: self.presentationData.theme.list.itemDestructiveColor, for: .normal)
self.buttonNode.setBackgroundImage(nil, for: [])
} else {
let text: String
if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
text = self.presentationData.strings.StickerPack_AddStickerCount(info.count)
} else {
text = self.presentationData.strings.StickerPack_AddMaskCount(info.count)
}
self.buttonNode.setTitle(text.uppercased(), with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal)
let roundedAccentBackground = generateImage(CGSize(width: 50.0, height: 50.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(self.presentationData.theme.list.itemCheckColors.fillColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
})?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25)
self.buttonNode.setBackgroundImage(roundedAccentBackground, for: [])
}
self.titleNode.attributedText = NSAttributedString(string: info.title, font: Font.semibold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
updateLayout = true
for item in items {
guard let item = item as? StickerPackItem else {
continue
}
entries.append(StickerPackPreviewGridEntry(index: entries.count, stickerItem: item))
}
}
let previousEntries = self.currentEntries
self.currentEntries = entries
if updateLayout, let (layout, _, _, _) = self.validLayout {
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - 12.0 * 2.0, height: .greatestFiniteMagnitude))
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((-titleSize.width) / 2.0), y: floor((-titleSize.height) / 2.0)), size: titleSize)
}
let transaction = StickerPackPreviewGridTransaction(previousList: previousEntries, list: entries, account: self.context.account, interaction: self.interaction)
self.enqueueTransaction(transaction)
}
var topContentInset: CGFloat {
guard let (_, gridFrame, titleAreaInset, gridInsets) = self.validLayout else {
return 0.0
}
return gridFrame.minY + gridInsets.top - titleAreaInset
}
func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
var insets = layout.insets(options: [.statusBar])
insets.top += 10.0
let buttonHeight: CGFloat = 50.0
let actionAreaTopInset: CGFloat = 12.0
let buttonSideInset: CGFloat = 10.0
let titleAreaInset: CGFloat = 50.0
var actionAreaHeight: CGFloat = 0.0
actionAreaHeight += insets.bottom + 12.0
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: layout.size.height - actionAreaHeight - buttonHeight), size: CGSize(width: layout.size.width - buttonSideInset * 2.0, height: buttonHeight)))
actionAreaHeight += buttonHeight
actionAreaHeight += actionAreaTopInset
transition.updateFrame(node: self.actionAreaBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: actionAreaHeight)))
transition.updateFrame(node: self.actionAreaSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
let gridFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top + titleAreaInset), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - titleAreaInset))
let itemsPerRow = 4
let fillingWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0)
let itemWidth = floor(fillingWidth / CGFloat(itemsPerRow))
let gridLeftInset = floor((layout.size.width - fillingWidth) / 2.0)
let initialRevealedRowCount: CGFloat = 4.5
let topInset = max(0.0, layout.size.height - floor(initialRevealedRowCount * itemWidth) - insets.top - actionAreaHeight - titleAreaInset)
let gridInsets = UIEdgeInsets(top: insets.top + topInset, left: gridLeftInset, bottom: actionAreaHeight, right: layout.size.width - fillingWidth - gridLeftInset)
let firstTime = self.validLayout == nil
self.validLayout = (layout, gridFrame, titleAreaInset, gridInsets)
transition.updateFrame(node: self.gridNode, frame: gridFrame)
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridFrame.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: 200.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf.isReadyValue.set(.single(true))
}
})
if firstTime {
while !self.enqueuedTransactions.isEmpty {
self.dequeueTransaction()
}
}
}
private func gridPresentationLayoutUpdated(_ presentationLayout: GridNodeCurrentPresentationLayout, transition: ContainedViewLayoutTransition) {
guard let (layout, gridFrame, titleAreaInset, gridInsets) = self.validLayout else {
return
}
let minBackgroundY = gridFrame.minY - titleAreaInset
let unclippedBackgroundY = gridFrame.minY - presentationLayout.contentOffset.y - titleAreaInset
let offsetFromInitialPosition = presentationLayout.contentOffset.y + gridInsets.top
let expandHeight: CGFloat = 100.0
let expandProgress = max(0.0, min(1.0, offsetFromInitialPosition / expandHeight))
var expandProgressTransition = transition
var expandUpdated = false
let modalProgress: CGFloat = unclippedBackgroundY < minBackgroundY ? 1.0 : 0.0
if abs(self.modalProgress - modalProgress) > CGFloat.ulpOfOne {
self.modalProgress = modalProgress
expandUpdated = true
expandProgressTransition = .animated(duration: 0.3, curve: .easeInOut)
}
if abs(self.expandProgress - expandProgress) > CGFloat.ulpOfOne {
self.expandProgress = expandProgress
expandUpdated = true
}
if expandUpdated {
self.expandProgressUpdated(self, expandProgressTransition)
}
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: max(minBackgroundY, unclippedBackgroundY)), size: CGSize(width: layout.size.width, height: layout.size.height))
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
transition.updateFrame(node: self.titleContainer, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: backgroundFrame.minY + floor((50.0) / 2.0)), size: CGSize()))
transition.updateFrame(node: self.titleSeparatorNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY + 50.0 - UIScreenPixel), size: CGSize(width: backgroundFrame.width, height: UIScreenPixel)))
self.titleSeparatorNode.alpha = unclippedBackgroundY < minBackgroundY ? 1.0 : 0.0
}
private func enqueueTransaction(_ transaction: StickerPackPreviewGridTransaction) {
self.enqueuedTransactions.append(transaction)
if let _ = self.validLayout {
self.dequeueTransaction()
}
}
private func dequeueTransaction() {
if self.enqueuedTransactions.isEmpty {
return
}
let transaction = self.enqueuedTransactions.removeFirst()
self.gridNode.transaction(GridNodeTransaction(deleteItems: transaction.deletions, insertItems: transaction.insertions, updateItems: transaction.updates, scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) {
if !self.backgroundNode.bounds.contains(self.convert(point, to: self.backgroundNode)) {
return nil
}
}
let result = super.hitTest(point, with: event)
return result
}
private func updatePreviewingItem(item: StickerPreviewPeekItem?, animated: Bool) {
if self.interaction.previewedItem != item {
self.interaction.previewedItem = item
self.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? StickerPackPreviewGridItemNode {
itemNode.updatePreviewing(animated: animated)
}
}
}
}
}
private final class StickerPackScreenNode: ViewControllerTracingNode {
private let context: AccountContext
private var presentationData: PresentationData
private let stickerPacks: [StickerPackReference]
private let modalProgressUpdated: (CGFloat, ContainedViewLayoutTransition) -> Void
private let dismissed: () -> Void
private let dimNode: ASDisplayNode
private let containerContainingNode: ASDisplayNode
private var containers: [StickerPackContainer] = []
private var selectedStickerPackIndex: Int
private var relativeToSelectedStickerPackTransition: CGFloat = 0.0
private var validLayout: ContainerViewLayout?
private var isDismissed: Bool = false
private let _ready = Promise<Bool>()
var ready: Promise<Bool> {
return self._ready
}
init(context: AccountContext, stickerPacks: [StickerPackReference], initialSelectedStickerPackIndex: Int, modalProgressUpdated: @escaping (CGFloat, ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.stickerPacks = stickerPacks
self.selectedStickerPackIndex = initialSelectedStickerPackIndex
self.modalProgressUpdated = modalProgressUpdated
self.dismissed = dismissed
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.dimNode.alpha = 0.0
self.containerContainingNode = ASDisplayNode()
super.init()
self.containers = self.stickerPacks.map { stickerPack in
return StickerPackContainer(context: context, presentationData: self.presentationData, stickerPack: stickerPack, decideNextAction: { [weak self] container, action in
guard let strongSelf = self, let layout = strongSelf.validLayout, let index = strongSelf.containers.index(where: { $0 === container }) else {
return .dismiss
}
if index == strongSelf.containers.count - 1 {
return .dismiss
} else {
switch action {
case .add:
var allAdded = true
for i in index + 1 ..< strongSelf.containers.count {
if let (_, _, installed) = strongSelf.containers[i].currentStickerPack {
if !installed {
allAdded = false
}
} else {
allAdded = false
}
}
if allAdded {
return .dismiss
}
case .remove:
if strongSelf.containers.count == 1 {
return .dismiss
}
}
}
strongSelf.selectedStickerPackIndex = strongSelf.selectedStickerPackIndex + 1
strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.3, curve: .spring))
return .navigatedNext
}, requestDismiss: { [weak self] in
self?.dismiss()
}, expandProgressUpdated: { [weak self] container, transition in
guard let strongSelf = self, let layout = strongSelf.validLayout, let index = strongSelf.containers.index(where: { $0 === container }) else {
return
}
if index == strongSelf.selectedStickerPackIndex {
strongSelf.containerLayoutUpdated(layout, transition: transition)
}
}, presentInGlobalOverlay: presentInGlobalOverlay,
sendSticker: sendSticker)
}
for container in self.containers {
self.containerContainingNode.addSubnode(container)
}
self.addSubnode(self.dimNode)
self.addSubnode(self.containerContainingNode)
self._ready.set(combineLatest(self.containers.map { $0.isReady })
|> map { values -> Bool in
for value in values {
if !value {
return false
}
}
return true
})
}
override func didLoad() {
super.didLoad()
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTapGesture(_:))))
self.containerContainingNode.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
let firstTime = self.validLayout == nil
self.validLayout = layout
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.containerContainingNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let expandProgress: CGFloat
if self.containers.count == 1 {
expandProgress = 1.0
} else {
expandProgress = self.containers[self.selectedStickerPackIndex].expandProgress
}
let scaledInset: CGFloat = 16.0
let scaledDistance: CGFloat = 4.0
let minScale = (layout.size.width - scaledInset * 2.0) / layout.size.width
let containerScale = expandProgress * 1.0 + (1.0 - expandProgress) * minScale
let containerVerticalOffset: CGFloat = (1.0 - expandProgress) * scaledInset * 2.0
for i in 0 ..< self.containers.count {
let container = self.containers[i]
let indexOffset = i - self.selectedStickerPackIndex
var scaledOffset: CGFloat = 0.0
scaledOffset = -CGFloat(indexOffset) * (1.0 - expandProgress) * (scaledInset * 2.0) + CGFloat(indexOffset) * scaledDistance
transition.updateFrame(node: container, frame: CGRect(origin: CGPoint(x: CGFloat(indexOffset) * layout.size.width + self.relativeToSelectedStickerPackTransition + scaledOffset, y: containerVerticalOffset), size: layout.size))
transition.updateSublayerTransformScale(node: container, scale: containerScale)
if container.validLayout?.0 != layout {
container.updateLayout(layout: layout, transition: transition)
}
}
let modalProgress = self.containers[self.selectedStickerPackIndex].modalProgress
self.modalProgressUpdated(modalProgress, transition)
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
break
case .changed:
let translation = recognizer.translation(in: self.view)
self.relativeToSelectedStickerPackTransition = translation.x
if self.selectedStickerPackIndex == 0 {
self.relativeToSelectedStickerPackTransition = min(0.0, self.relativeToSelectedStickerPackTransition)
}
if self.selectedStickerPackIndex == self.containers.count - 1 {
self.relativeToSelectedStickerPackTransition = max(0.0, self.relativeToSelectedStickerPackTransition)
}
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .immediate)
}
case .ended, .cancelled:
let translation = recognizer.translation(in: self.view)
let velocity = recognizer.velocity(in: self.view)
if abs(translation.x) > 30.0 {
let deltaIndex = translation.x > 0 ? -1 : 1
self.selectedStickerPackIndex = max(0, min(self.containers.count - 1, Int(self.selectedStickerPackIndex + deltaIndex)))
} else if abs(velocity.x) > 100.0 {
let deltaIndex = velocity.x > 0 ? -1 : 1
self.selectedStickerPackIndex = max(0, min(self.containers.count - 1, Int(self.selectedStickerPackIndex + deltaIndex)))
}
self.relativeToSelectedStickerPackTransition = 0.0
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.2, curve: .easeInOut))
}
default:
break
}
}
func animateIn() {
self.dimNode.alpha = 1.0
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let minInset: CGFloat = (self.containers.map { container -> CGFloat in container.topContentInset }).max() ?? 0.0
self.containerContainingNode.layer.animatePosition(from: CGPoint(x: 0.0, y: self.containerContainingNode.bounds.height - minInset), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
func animateOut(completion: @escaping () -> Void) {
self.dimNode.alpha = 0.0
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
let minInset: CGFloat = (self.containers.map { container -> CGFloat in container.topContentInset }).max() ?? 0.0
self.containerContainingNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.containerContainingNode.bounds.height - minInset), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
}
func dismiss() {
if self.isDismissed {
return
}
self.isDismissed = true
self.animateOut(completion: { [weak self] in
self?.dismissed()
})
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
let selectedContainer = self.containers[self.selectedStickerPackIndex]
if selectedContainer.hitTest(self.view.convert(point, to: selectedContainer.view), with: event) == nil {
return self.dimNode.view
}
let result = super.hitTest(point, with: event)
return result
}
@objc private func dimNodeTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.dismiss()
}
}
}
public final class StickerPackScreen: ViewController {
private let context: AccountContext
private let stickerPacks: [StickerPackReference]
private let initialSelectedStickerPackIndex: Int
private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?
private var controllerNode: StickerPackScreenNode {
return self.displayNode as! StickerPackScreenNode
}
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private var alreadyDidAppear: Bool = false
public init(context: AccountContext, stickerPacks: [StickerPackReference], selectedStickerPackIndex: Int = 0, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) {
self.context = context
self.stickerPacks = stickerPacks
self.initialSelectedStickerPackIndex = selectedStickerPackIndex
self.sendSticker = sendSticker
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Ignore
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = StickerPackScreenNode(context: self.context, stickerPacks: self.stickerPacks, initialSelectedStickerPackIndex: self.initialSelectedStickerPackIndex, modalProgressUpdated: { [weak self] value, transition in
DispatchQueue.main.async {
guard let strongSelf = self else {
return
}
strongSelf.updateModalStyleOverlayTransitionFactor(value, transition: transition)
}
}, dismissed: { [weak self] in
self?.dismiss()
}, presentInGlobalOverlay: { [weak self] c, a in
self?.presentInGlobalOverlay(c, with: a)
}, sendSticker: self.sendSticker.flatMap { [weak self] sendSticker in
return { file, sourceNode, sourceRect in
if sendSticker(file, sourceNode, sourceRect) {
self?.dismiss()
return true
} else {
return false
}
}
})
self._ready.set(self.controllerNode.ready.get())
super.displayNodeDidLoad()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.alreadyDidAppear {
self.alreadyDidAppear = true
self.controllerNode.animateIn()
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, transition: transition)
}
}